Files
gitmost/apps/server/src/integrations/static/static.module.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

87 lines
3.0 KiB
TypeScript

import { Module, OnModuleInit } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { join } from 'path';
import * as fs from 'node:fs';
import fastifyStatic from '@fastify/static';
import { EnvironmentService } from '../environment/environment.service';
@Module({})
export class StaticModule implements OnModuleInit {
constructor(
private readonly httpAdapterHost: HttpAdapterHost,
private readonly environmentService: EnvironmentService,
) {}
public async onModuleInit() {
const httpAdapter = this.httpAdapterHost.httpAdapter;
const app = httpAdapter.getInstance();
const clientDistPath = join(
__dirname,
'..',
'..',
'..',
'..',
'client/dist',
);
const indexFilePath = join(clientDistPath, 'index.html');
if (fs.existsSync(clientDistPath) && fs.existsSync(indexFilePath)) {
const indexTemplateFilePath = join(clientDistPath, 'index-template.html');
const windowVar = '<!--window-config-->';
const configString = {
ENV: this.environmentService.getNodeEnv(),
APP_URL: this.environmentService.getAppUrl(),
CLOUD: this.environmentService.isCloud(),
COMPACT_PAGE_TREE: this.environmentService.isCompactPageTreeEnabled(),
FILE_UPLOAD_SIZE_LIMIT:
this.environmentService.getFileUploadSizeLimit(),
FILE_IMPORT_SIZE_LIMIT:
this.environmentService.getFileImportSizeLimit(),
DRAWIO_URL: this.environmentService.getDrawioUrl(),
SUBDOMAIN_HOST: this.environmentService.isCloud()
? this.environmentService.getSubdomainHost()
: undefined,
COLLAB_URL: this.environmentService.getCollabUrl(),
BILLING_TRIAL_DAYS: this.environmentService.isCloud()
? this.environmentService.getBillingTrialDays()
: undefined,
POSTHOG_HOST: this.environmentService.getPostHogHost(),
POSTHOG_KEY: this.environmentService.getPostHogKey(),
// #355 — mirrors the server-side CLIENT_TELEMETRY_ENABLED gate so the
// client only collects/sends vitals when the operator opts in.
CLIENT_TELEMETRY_ENABLED:
this.environmentService.isClientTelemetryEnabled(),
};
const windowScriptContent = `<script>window.CONFIG=${JSON.stringify(configString)};</script>`;
if (!fs.existsSync(indexTemplateFilePath)) {
fs.copyFileSync(indexFilePath, indexTemplateFilePath);
}
const html = fs.readFileSync(indexTemplateFilePath, 'utf8');
const transformedHtml = html.replace(windowVar, windowScriptContent);
fs.writeFileSync(indexFilePath, transformedHtml);
const RENDER_PATH = '*';
await app.register(fastifyStatic, {
root: clientDistPath,
wildcard: false,
});
app.get(RENDER_PATH, (req: any, res: any) => {
const stream = fs.createReadStream(indexFilePath);
res
.header('Cache-Control', 'no-cache, no-store, must-revalidate')
.type('text/html')
.send(stream);
});
}
}
}