Files
webmail/lib/telemetry/types.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

95 lines
2.8 KiB
TypeScript

// Schema v1 of the anonymous heartbeat, and v2 (v1 plus error counts, sent
// only with the separate error-count consent). Documented at
// https://bulwarkmail.org/docs/legal/privacy/telemetry
export type ConsentState = 'pending' | 'on' | 'off';
export type Platform = 'docker' | 'bare' | 'k8s' | 'unknown';
export type OsFamily = 'linux' | 'darwin' | 'windows' | 'unknown';
export type CountBucket = '0' | '1' | '2-5' | '6-10' | '11-50' | '51-200' | '201+';
export interface TelemetryFeatures {
calendar: boolean;
contacts: boolean;
files: boolean;
extensions: boolean;
oauth_enabled: boolean;
smime_enabled: boolean;
}
export interface TelemetryPayload {
schema: '1';
instance_id: string;
ts: string;
version: string;
build: string | null;
platform: Platform;
node_version: string;
os_family: OsFamily;
stalwart_version: string | null;
features: TelemetryFeatures;
counts: {
accounts: CountBucket;
accounts_active_7d: CountBucket;
extensions_installed: number;
themes_installed: number;
};
uptime_days: number;
}
/** At most this many errors in a v2 heartbeat, the most frequent first. */
export const MAX_TELEMETRY_ERRORS = 50;
/** Top frames per error. */
export const MAX_TELEMETRY_FRAMES = 5;
export interface TelemetryErrorFrame {
/** The bundle file without origin: `/_next/static/chunks/x.js` or `/.next/server/...`. */
path: string;
line: number | null;
col: number | null;
/** The file's debug ID, which finds its uploaded source map. */
debug_id: string | null;
}
/** One error fingerprint of the last complete UTC day. */
export interface TelemetryError {
/** The error's class name (TypeError, JmapMethodError). */
name: string;
fingerprint: string;
frames: TelemetryErrorFrame[];
version: string;
/** How often that day, bucketed like the account counts. */
count: CountBucket;
/** On how many of the last 7 days it occurred (1-7). */
days: number;
/** Only one of Bulwark's fixed error codes (`Email/set: overQuota`), never free text. */
message?: string;
}
export interface TelemetryPayloadV2 extends Omit<TelemetryPayload, 'schema'> {
schema: '2';
errors: TelemetryError[];
}
export type AnyTelemetryPayload = TelemetryPayload | TelemetryPayloadV2;
export interface TelemetryStateFile {
consent: ConsentState;
endpoint: string;
consentedAt: string | null;
lastSentAt: string | null;
/**
* With a shared store only: when the last scheduled heartbeat went out.
* Unlike lastSentAt, the admin's "send now" doesn't set it.
*/
lastScheduledSentAt?: string | null;
nextScheduledAt: string | null;
/**
* The separate consent to add error counts (schema v2). Off unless an
* admin turns it on; switching telemetry off switches it off too.
*/
errorsConsent?: 'on' | 'off';
}
export const DEFAULT_ENDPOINT = 'https://telemetry.bulwarkmail.org/v1/heartbeat';