Files
webmail/lib/error-reporting/settings.ts
T
Linus Rath e580ed24da feat: send anonymous error counts with the usage stats when the admin also agrees to it
A second switch under System > Usage stats, "Also send anonymous error
counts", or `BULWARK_TELEMETRY_ERRORS=on|off`. Off by default, off
whenever the heartbeat is off or no third-party traffic is on, and
switching the heartbeat off switches it off too. With it on the
heartbeat becomes schema 2: the v1 fields unchanged plus `errors`, up to
50 fingerprints of the last complete UTC day with the error name, the
top frames in Bulwark's bundles (path, line, column, debug ID), the
version, a bucketed count, on how many of the last 7 days it occurred,
and a message only when it is a fixed error code. Without the consent
the heartbeat stays exactly schema 1. The admin preview shows the v2
payload as soon as the switch is flipped.
2026-11-01 03:10:41 +01:00

57 lines
2.2 KiB
TypeScript

import { configManager } from '@/lib/admin/config-manager';
import { parseDsn } from './envelope';
import { metricsEnabled } from '@/lib/metrics';
import { effectiveErrorsConsent } from '@/lib/telemetry/state';
import type { ClientSettings } from './client';
/**
* The error-reporting settings as the server resolves them: admin config,
* then ERROR_REPORTING_DSN(_FILE) / ERROR_REPORTING_SAMPLE_RATE /
* ERROR_REPORTING_ENVIRONMENT, then off. Bulwark Lite reads the same three
* from policy.json (lib/error-reporting/lite.ts).
*/
export interface ErrorReportingSettings {
/** A valid DSN, or '' when none is set or the value doesn't parse. */
dsn: string;
sampleRate: number;
environment: string;
}
export function parseSampleRate(value: unknown): number {
const n = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN;
if (!Number.isFinite(n)) return 1;
return Math.min(1, Math.max(0, n));
}
export function parseEnvironment(value: unknown): string {
const text = typeof value === 'string' ? value.trim() : '';
return /^[\w.-]{1,64}$/.test(text) ? text : 'production';
}
export function errorReportingSettings(): ErrorReportingSettings {
const raw = configManager.get<string>('errorReportingDsn', '');
return {
dsn: parseDsn(raw) ? raw.trim() : '',
sampleRate: parseSampleRate(configManager.get<string>('errorReportingSampleRate', '1')),
environment: parseEnvironment(configManager.get<string>('errorReportingEnvironment', 'production')),
};
}
/**
* Whether browsers should post their errors to /api/errors: when there is a
* DSN to report to, a metrics endpoint that counts them, or consent to send
* error counts with the telemetry heartbeat.
*/
export async function shouldCollectErrors(): Promise<boolean> {
await configManager.ensureLoaded();
if (errorReportingSettings().dsn || metricsEnabled()) return true;
return (await effectiveErrorsConsent()).consent === 'on';
}
/** What /api/config hands the browser. */
export async function browserErrorSettings(): Promise<ClientSettings> {
const settings = errorReportingSettings();
return { ...settings, collect: await shouldCollectErrors() };
}