mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
Admins had logs and nothing else: client errors only reached the browser console. An admin can now set a Sentry-protocol address (DSN) for Bugsink, GlitchTip or Sentry under System > Monitoring, or with `ERROR_REPORTING_DSN`, `ERROR_REPORTING_DSN_FILE`, `ERROR_REPORTING_SAMPLE_RATE` and `ERROR_REPORTING_ENVIRONMENT`. Bulwark Lite reads the same from `errorReporting` in policy.json. Everything stays off while no address is set. - browsers report errors from the error boundaries, window.onerror, unhandled rejections and failed JMAP requests (method and error type only), with route and JMAP method breadcrumbs and the chunks' debug IDs - the server reports what Next hands to onRequestError and what routes log with logger.error - every report is scrubbed first: quoted text, addresses, URLs, file names, tokens and the rest after the first line are removed, and the server scrubs what browsers post to /api/errors again - reports are deduplicated and rate-limited per session and process - errors are counted per day for "errors today" on the admin page - startup messages and audit entries go through the logger, so `LOG_FORMAT=json` covers them
35 lines
1.7 KiB
TypeScript
35 lines
1.7 KiB
TypeScript
// @vitest-environment node
|
|
import { describe, expect, it } from 'vitest';
|
|
import { bound, KEEP_DAYS, MAX_PER_DAY, merge, summarizeDay, type Aggregate, type AggregateEntry } from '../aggregate';
|
|
|
|
function entry(count: number): AggregateEntry {
|
|
return { name: 'Error', frames: [], version: '1.20.0', runtime: 'browser', count };
|
|
}
|
|
|
|
describe('error aggregate', () => {
|
|
it('adds counts from several replicas instead of overwriting them', () => {
|
|
const a: Aggregate = { '2026-11-01': { f1: entry(2) } };
|
|
const b: Aggregate = { '2026-11-01': { f1: entry(3), f2: entry(1) } };
|
|
expect(merge(a, b)['2026-11-01']).toEqual({ f1: entry(5), f2: entry(1) });
|
|
});
|
|
|
|
it('keeps the most frequent fingerprints per day and only the last days', () => {
|
|
const many: Record<string, AggregateEntry> = {};
|
|
for (let i = 0; i < MAX_PER_DAY + 50; i++) many[`f${i}`] = entry(i + 1);
|
|
const old = '2026-10-01';
|
|
const bounded = bound({ '2026-11-01': many, [old]: { x: entry(1) } }, '2026-11-01');
|
|
expect(Object.keys(bounded['2026-11-01'])).toHaveLength(MAX_PER_DAY);
|
|
expect(bounded['2026-11-01'].f0).toBeUndefined();
|
|
expect(bounded['2026-11-01'][`f${MAX_PER_DAY + 49}`]).toBeDefined();
|
|
expect(bounded[old]).toBeUndefined();
|
|
const lastKept = new Date(Date.parse('2026-11-01T00:00:00Z') - (KEEP_DAYS - 1) * 86_400_000).toISOString().slice(0, 10);
|
|
expect(Object.keys(bound({ [lastKept]: { x: entry(1) } }, '2026-11-01'))).toEqual([lastKept]);
|
|
});
|
|
|
|
it('summarizes a day, most frequent first', () => {
|
|
const summary = summarizeDay({ '2026-11-01': { a: entry(1), b: entry(5) } }, '2026-11-01');
|
|
expect(summary.total).toBe(6);
|
|
expect(summary.top.map((e) => e.fingerprint)).toEqual(['b', 'a']);
|
|
});
|
|
});
|