agent-ecosystem/test/main/sentry.test.ts
2026-05-18 01:57:16 +03:00

125 lines
4.1 KiB
TypeScript

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { vi } from 'vitest';
describe('main Sentry telemetry gate', () => {
let previousDsn: string | undefined;
beforeEach(() => {
previousDsn = process.env.SENTRY_DSN;
process.env.SENTRY_DSN = 'https://public@example.com/1';
vi.resetModules();
});
afterEach(() => {
if (previousDsn === undefined) {
delete process.env.SENTRY_DSN;
} else {
process.env.SENTRY_DSN = previousDsn;
}
vi.resetModules();
});
it('does not initialize Sentry when persisted telemetry config is disabled', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-teams-sentry-config-'));
fs.writeFileSync(
path.join(tempRoot, 'agent-teams-config.json'),
JSON.stringify({ general: { telemetryEnabled: false } }),
'utf8'
);
const { setClaudeBasePathOverride } = await import('@main/utils/pathDecoder');
setClaudeBasePathOverride(tempRoot);
const sentrySdk = await import('@sentry/electron/main');
const init = vi.mocked(sentrySdk.init);
init.mockClear();
const sentry = await import('@main/sentry');
expect(sentry.readPersistedTelemetryEnabled(tempRoot)).toBe(false);
expect(init).not.toHaveBeenCalled();
expect(sentry.filterSentryEventForTelemetry({ ok: true })).toBeNull();
setClaudeBasePathOverride(null);
fs.rmSync(tempRoot, { recursive: true, force: true });
});
it('clears user scope and drops events when telemetry is disabled', async () => {
const sentry = await import('@main/sentry');
const sentryApi = {
setUser: vi.fn(),
setTags: vi.fn(),
close: vi.fn(() => Promise.resolve(true)),
};
sentry.setMainSentryApiForTesting(sentryApi);
sentry.syncTelemetryFlag(false);
expect(sentryApi.setUser).toHaveBeenCalledWith(null);
expect(sentryApi.close).toHaveBeenCalled();
expect(sentry.filterSentryEventForTelemetry({ ok: true })).toBeNull();
});
it('returns only hashed anonymous Sentry context when telemetry is enabled', async () => {
const sentry = await import('@main/sentry');
sentry.syncTelemetryFlag(true);
const context = await sentry.getCurrentSentryTelemetryContext();
expect(context?.userId).toMatch(/^[a-f0-9]{64}$/);
expect(Object.keys(context?.tags ?? {}).sort((a, b) => a.localeCompare(b))).toEqual([
'app_version',
'arch',
'identity_source',
'platform',
]);
});
it('does not attach high-cardinality breadcrumb data', async () => {
const sentry = await import('@main/sentry');
const sentryApi = {
addBreadcrumb: vi.fn(),
};
sentry.setMainSentryApiForTesting(sentryApi);
sentry.addMainBreadcrumb('team', 'launch', { teamName: 'private-team-name' });
expect(sentryApi.addBreadcrumb).toHaveBeenCalledWith({
category: 'team',
message: 'launch',
level: 'info',
});
});
it('redacts sensitive fields before allowing telemetry events', async () => {
const sentry = await import('@main/sentry');
sentry.syncTelemetryFlag(true);
const filtered = sentry.filterSentryEventForTelemetry({
message: 'Failed for user dev@example.com in /Users/alice/private-repo',
extra: {
projectPath: '/Users/alice/private-repo',
token: 'sk-testsecretsecretsecret',
accountUuid: 'd9b2d63a-582c-4d69-8a01-90e8199f532d',
},
});
const serialized = JSON.stringify(filtered);
expect(serialized).not.toContain('dev@example.com');
expect(serialized).not.toContain('alice');
expect(serialized).not.toContain('private-repo');
expect(serialized).not.toContain('sk-testsecretsecretsecret');
expect(serialized).not.toContain('d9b2d63a-582c-4d69-8a01-90e8199f532d');
});
it('only exposes safe low-cardinality telemetry tags', async () => {
const { getSafeSentryTelemetryTags } = await import('@main/sentry');
expect(
Object.keys(getSafeSentryTelemetryTags('app-data')).sort((a, b) => a.localeCompare(b))
).toEqual(['app_version', 'arch', 'identity_source', 'platform']);
});
});