mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 09:45:39 +08:00
Refresh consent across processes, reset identity on opt-out, discard obsolete buffers and stop subsequent requests/requeues. Preserve environment override precedence and document in-flight semantics. Fixes #1869.
93 lines
5.7 KiB
TypeScript
93 lines
5.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { Telemetry } from '../src/telemetry';
|
|
|
|
describe('telemetry opt-out across running instances (#1869)', () => {
|
|
let dir: string;
|
|
let now: Date;
|
|
let sends: any[];
|
|
const make = (env = {}, fetchImpl: typeof fetch = async (_url, init) => {
|
|
sends.push(JSON.parse(String(init?.body)));
|
|
return new Response(null, { status: 204 });
|
|
}) => new Telemetry({ dir, env, fetchImpl, now: () => now, stderr: () => {}, installExitHook: false });
|
|
const queued = () => fs.readdirSync(dir).filter(n => n.startsWith('telemetry-queue'));
|
|
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-telemetry-off-')); now = new Date('2026-06-12T08:00:00Z'); sends = []; });
|
|
afterEach(() => { vi.useRealTimers(); fs.rmSync(dir, { recursive: true, force: true }); });
|
|
|
|
it('removes the identity on off and assigns a new one on on', () => {
|
|
const a = make(); a.setEnabled(true, 'cli'); const old = a.getStatus().machineId;
|
|
make().setEnabled(false, 'cli');
|
|
expect(a.getStatus()).toMatchObject({ enabled: false, machineId: null });
|
|
expect(fs.readFileSync(a.configPath, 'utf8')).not.toContain(old!);
|
|
make().setEnabled(true, 'cli');
|
|
expect(a.getStatus().machineId).toBeTruthy(); expect(a.getStatus().machineId).not.toBe(old);
|
|
});
|
|
|
|
it.each(['persist', 'flush'] as const)('drops memory and new recording after another instance opts out: %s', async action => {
|
|
const a = make(); a.setEnabled(true, 'cli'); a.recordLifecycle('install', {});
|
|
make().setEnabled(false, 'cli'); a.recordLifecycle('index', {}); a.recordUsage('mcp_tool', 'codegraph_explore', true);
|
|
if (action === 'persist') a.persistSync(); else await a.flushNow();
|
|
expect(sends).toEqual([]); expect(queued()).toEqual([]);
|
|
make().setEnabled(true, 'cli'); await a.flushNow();
|
|
expect(sends).toEqual([]); expect(queued()).toEqual([]);
|
|
});
|
|
|
|
it('observes external off at the completed-day interval', async () => {
|
|
vi.useFakeTimers(); const a = make(); a.setEnabled(true, 'cli'); a.recordUsage('cli_command', 'query', true);
|
|
a.startInterval(); await vi.advanceTimersByTimeAsync(0);
|
|
make().setEnabled(false, 'cli'); now = new Date('2026-06-13T08:00:00Z');
|
|
try { await vi.advanceTimersByTimeAsync(6 * 60 * 60_000); expect(sends).toEqual([]); expect(queued()).toEqual([]); }
|
|
finally { a.stopInterval(); }
|
|
});
|
|
|
|
it('does not reuse pre-opt-out memory after an off/on cycle the process missed', async () => {
|
|
const a = make(); a.setEnabled(true, 'cli'); a.recordLifecycle('install', {});
|
|
const b = make(); b.setEnabled(false, 'cli'); b.setEnabled(true, 'cli');
|
|
a.recordLifecycle('index', {}); await a.flushNow();
|
|
expect(sends).toHaveLength(1); expect(sends[0].events.map((e: any) => e.event)).toEqual(['index']);
|
|
expect(sends[0].machine_id).toBe(b.getStatus().machineId);
|
|
});
|
|
|
|
it.each([false, true])('in-flight failure cannot recreate old data after off (re-enable=%s)', async reEnable => {
|
|
let reject!: (error: Error) => void;
|
|
const a = make({}, async () => { sends.push('started'); return new Promise((_resolve, fail) => { reject = fail; }); });
|
|
a.setEnabled(true, 'cli'); a.recordLifecycle('install', {}); a.recordUsage('cli_command', 'query', true);
|
|
const flushing = a.flushNow(); expect(sends).toEqual(['started']);
|
|
const b = make(); b.setEnabled(false, 'cli'); if (reEnable) b.setEnabled(true, 'cli');
|
|
reject(new Error('network failure')); await flushing;
|
|
expect(queued()).toEqual([]); await b.flushNow(); expect(sends).toEqual(['started']);
|
|
});
|
|
|
|
it('checks consent before each request chunk after an in-flight request returns', async () => {
|
|
let finish!: (r: Response) => void;
|
|
const a = make({}, async () => { sends.push('started'); if (sends.length > 1) return new Response(null, { status: 204 }); return new Promise(resolve => { finish = resolve; }); });
|
|
a.setEnabled(true, 'cli'); for (let i = 0; i < 105; i++) a.recordLifecycle('index', {});
|
|
const flushing = a.flushNow(); expect(sends).toHaveLength(1);
|
|
make().setEnabled(false, 'cli'); finish(new Response(null, { status: 204 })); await flushing;
|
|
expect(sends).toHaveLength(1); expect(queued()).toEqual([]);
|
|
});
|
|
|
|
it('off removes stale claims as well as the queue so on cannot revive them', async () => {
|
|
const a = make(); a.setEnabled(true, 'cli');
|
|
const claim = path.join(dir, 'telemetry-queue.sending.98765.jsonl');
|
|
fs.writeFileSync(claim, JSON.stringify({ v: 2, ev: 'install', ts: now.toISOString(), props: {} }) + '\n');
|
|
const old = new Date(now.getTime() - 2 * 60 * 60_000); fs.utimesSync(claim, old, old);
|
|
a.setEnabled(false, 'cli'); expect(queued()).toEqual([]); a.setEnabled(true, 'cli'); await a.flushNow(); expect(sends).toEqual([]);
|
|
});
|
|
|
|
it.each(['DO_NOT_TRACK', 'CODEGRAPH_TELEMETRY'])('environment off drops pending memory: %s', async key => {
|
|
const env: NodeJS.ProcessEnv = {}; const a = make(env); a.setEnabled(true, 'cli'); a.recordLifecycle('install', {});
|
|
env[key] = key === 'DO_NOT_TRACK' ? '1' : '0'; await a.flushNow(); a.persistSync(); delete env[key]; await a.flushNow();
|
|
expect(sends).toEqual([]); expect(queued()).toEqual([]);
|
|
});
|
|
|
|
it('retains the documented explicit environment-on override without resurrecting the old identity', async () => {
|
|
const a = make(); a.setEnabled(true, 'cli'); const old = a.getStatus().machineId; a.setEnabled(false, 'cli');
|
|
const forced = make({ CODEGRAPH_TELEMETRY: '1' }); forced.recordLifecycle('index', {}); await forced.flushNow();
|
|
expect(sends).toHaveLength(1); expect(sends[0].machine_id).toMatch(/^[0-9a-f-]{36}$/); expect(sends[0].machine_id).not.toBe(old);
|
|
expect(make().isEnabled()).toBe(false);
|
|
});
|
|
});
|