Files
gitmost/apps/client/src/lib/config.ts
T
agent_coder d3209b5aab fix(#355 review E1=B + F1-F8): gate client telemetry OFF by default + throttler/lifecycle/overflow fixes
Maintainer resolved E1 as variant B: the public vitals sink + client collection
must be OFF by default (else client_metrics grows unbounded on a self-host deploy
with no external pruner, via an unauthenticated public endpoint).

- F1: new operator flag CLIENT_TELEMETRY_ENABLED (default OFF), SEPARATE from
  METRICS_PORT (Grafana reads the table directly, independent of the scrape port).
  ClientTelemetryModule.register() provides VitalsController ONLY when the flag is
  true (route absent otherwise); the flag reaches the client via window.CONFIG
  (config.ts isClientTelemetryEnabled), and initVitals() early-returns when off.
- F2/F3 [throttler]: this repo's ThrottlerGuard applies EVERY named throttler to
  every guarded route unless skipped. The new VITALS bucket therefore (a) newly
  bound collab-token → 429 behind shared/NAT IPs, and (b) the vitals route didn't
  skip the stricter public-share-ai (5/min) bucket → effective 5/min not 120.
  Fix (additive, global config unchanged): vitals.controller @SkipThrottle the
  other buckets + @Throttle VITALS 120/min; collab-token adds VITALS_THROTTLER to
  its existing @SkipThrottle (restoring its prior effectively-unthrottled state).
- F4: metrics node:http server is closed on shutdown (MetricsServerLifecycle
  OnModuleDestroy → closeMetricsServer(), fired by enableShutdownHooks).
- F5: docSize outside [0, int4-max] drops to null (keeping the event) instead of
  overflowing int4 and failing the WHOLE batch insert (+ 2 tests).
- F6: .env.example documents METRICS_PORT (no default — unset = subsystem OFF) +
  CLIENT_TELEMETRY_ENABLED; fixed the inaccurate "default 9464" wording.
- F7: disabled/non-sampled sessions install ZERO observers — isVitalsActive()
  (enabled && sampled) gates reportClientMetric AND the page-editor
  measurePageOpen + dispatchTransaction wrapping.
- F8: kept db.d.ts hand-added (wontfix) — this repo HAND-CURATES db.d.ts (verified
  across recent fork migrations a32fba63/8c5b57eb/fdeede00); codegen would be the
  deviation. The ClientMetrics interface maps the migration 1:1.

Gate: server tsc 0, client tsc 0, server metrics/vitals/telemetry/throttle 21
tests, client route-template 5. No new deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:00:03 +03:00

120 lines
3.2 KiB
TypeScript

import bytes from "bytes";
import { castToBoolean } from "@/lib/utils.tsx";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
import { sanitizeUrl } from "@docmost/editor-ext";
declare global {
interface Window {
CONFIG?: Record<string, string>;
}
}
export function getAppName(): string {
return "Gitmost";
}
export function getAppUrl(): string {
return `${window.location.protocol}//${window.location.host}`;
}
export function getServerAppUrl(): string {
return getConfigValue("APP_URL");
}
export function getBackendUrl(): string {
return getAppUrl() + "/api";
}
export function getCollaborationUrl(): string {
const baseUrl =
getConfigValue("COLLAB_URL") ||
(import.meta.env.DEV ? process.env.APP_URL : getAppUrl());
const collabUrl = new URL("/collab", baseUrl);
collabUrl.protocol = collabUrl.protocol === "https:" ? "wss:" : "ws:";
return collabUrl.toString();
}
export function getSubdomainHost(): string {
return getConfigValue("SUBDOMAIN_HOST");
}
export function isCloud(): boolean {
return castToBoolean(getConfigValue("CLOUD"));
}
export function isCompactPageTreeEnabled(): boolean {
return castToBoolean(getConfigValue("COMPACT_PAGE_TREE", "true"));
}
// #355 — operator toggle for client perf-telemetry. DEFAULT OFF: the server
// mirrors CLIENT_TELEMETRY_ENABLED into window.CONFIG; when off the client
// installs no observers and sends nothing (the sink endpoint doesn't exist).
export function isClientTelemetryEnabled(): boolean {
return castToBoolean(getConfigValue("CLIENT_TELEMETRY_ENABLED", "false"));
}
export function getAvatarUrl(
avatarUrl: string,
type: AvatarIconType = AvatarIconType.AVATAR,
) {
if (!avatarUrl) return null;
if (avatarUrl?.startsWith("http")) return avatarUrl;
return getBackendUrl() + `/attachments/img/${type}/` + encodeURI(avatarUrl);
}
export function getSpaceUrl(spaceSlug: string) {
return "/s/" + spaceSlug;
}
export function getFileUrl(src: string) {
if (!src) return src;
if (src.startsWith("http")) return src;
if (src.startsWith("/api/")) {
// Remove the '/api' prefix
return getBackendUrl() + src.substring(4);
}
if (src.startsWith("/files/")) {
return getBackendUrl() + src;
}
return sanitizeUrl(src);
}
export function getFileUploadSizeLimit() {
const limit = getConfigValue("FILE_UPLOAD_SIZE_LIMIT", "50mb");
return bytes(limit);
}
export function getFileImportSizeLimit() {
const limit = getConfigValue("FILE_IMPORT_SIZE_LIMIT", "200mb");
return bytes(limit);
}
export function getDrawioUrl() {
return getConfigValue("DRAWIO_URL", "https://embed.diagrams.net");
}
export function getBillingTrialDays() {
return getConfigValue("BILLING_TRIAL_DAYS");
}
export function getPostHogHost() {
return getConfigValue("POSTHOG_HOST");
}
export function isPostHogEnabled(): boolean {
return Boolean(getPostHogHost() && getPostHogKey());
}
export function getPostHogKey() {
return getConfigValue("POSTHOG_KEY");
}
function getConfigValue(key: string, defaultValue: string = undefined): string {
const rawValue = import.meta.env.DEV
? process?.env?.[key]
: window?.CONFIG?.[key];
return rawValue ?? defaultValue;
}