mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
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.
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('@/lib/admin/config-manager', () => ({
|
|
configManager: {
|
|
ensureLoaded: async () => {},
|
|
getPolicy: () => ({ features: {} }),
|
|
getAll: () => ({}),
|
|
get: (key: string, fallback: unknown) => (key === 'jmapServerUrl' ? 'https://mail.example.org' : fallback),
|
|
},
|
|
}));
|
|
vi.mock('@/lib/telemetry/state', () => ({
|
|
getInstanceId: async () => 'instance-1',
|
|
effectiveErrorsConsent: async () => ({ consent: 'off', source: 'file' }),
|
|
}));
|
|
vi.mock('@/lib/telemetry/login-tracker', () => ({ getLoginCounts: async () => ({ total: 3, active7d: 1 }) }));
|
|
vi.mock('@/lib/admin/plugin-registry', () => ({
|
|
getPluginRegistry: async () => ({ plugins: [] }),
|
|
getThemeRegistry: async () => ({ themes: [] }),
|
|
}));
|
|
// A probe, if one were made, would get past the SSRF guard.
|
|
vi.mock('@/lib/telemetry/endpoint-guard', () => ({ resolveEndpointAllowed: async () => ({ ok: true }) }));
|
|
|
|
import { buildPayload } from '@/lib/telemetry/payload';
|
|
|
|
// Stalwart (0.16.0 through 1.0) sends no Server header and publishes no
|
|
// version a client could read. The heartbeat used to read the Server header of
|
|
// /.well-known/jmap anyway: null against Stalwart itself, and the proxy's
|
|
// version (nginx/1.25.3 -> "1.25.3") behind a reverse proxy.
|
|
describe('telemetry stalwart_version', () => {
|
|
let fetchSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
delete process.env.STALWART_VERSION;
|
|
fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response(null, { status: 307, headers: { server: 'nginx/1.25.3', location: '/jmap/session' } }),
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env.STALWART_VERSION;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('is empty without STALWART_VERSION, on 0.16 and 1.0 alike, and probes nothing', async () => {
|
|
const payload = await buildPayload();
|
|
|
|
expect(payload.stalwart_version).toBeNull();
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('reports the version the admin set', async () => {
|
|
process.env.STALWART_VERSION = ' 0.16.23 ';
|
|
|
|
const payload = await buildPayload();
|
|
|
|
expect(payload.stalwart_version).toBe('0.16.23');
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('treats an empty STALWART_VERSION as unset', async () => {
|
|
process.env.STALWART_VERSION = '';
|
|
expect((await buildPayload()).stalwart_version).toBeNull();
|
|
});
|
|
});
|