mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
b9c438b275
Node host now appends one JSONL event per request to ~/.pixelpipe/events.jsonl (override with --events-file or PIXELPIPE_LOG). - Size-based rotation at 100 MB (old file → events.jsonl.1). - Sync writes — overhead is ~1ms, dwarfed by upstream latency. - Failures degrade silently: a broken disk disables the tracker but never breaks a request. - Flushed and fsync'd on SIGINT/SIGTERM so the last batch isn't lost. - Opt out via --no-track or PIXELPIPE_TRACK=0. Worker host uses JsonLogTracker with console.log; Cloudflare ingests this as Workers Logs, and Logpush can ship it to R2/S3 as JSONL — same shape Node writes locally, so analysis tooling works on either. Console log line on both hosts now also includes tokens/cache_read inline (e.g. 'compressed 4799ch → 2img/12343B tokens=1234+56 cache_read=800') so cache effectiveness is visible at a glance even without parsing the JSONL. Unknown-tag canary fires a stderr warning when info.unknownStaticTags is non-empty — surfaces Claude Code releases that introduce new dynamic tags before they tank the cache hit rate. Help text and HANDOFF will be updated in the next commit; for now the new flag drops the need for --exclude-dynamic-system-prompt-sections. Tests: +5 (toTrackEvent flattening, JsonLogTracker sink behavior, error swallowing, minimal-event handling, noop).
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { toTrackEvent, JsonLogTracker, noopTracker, type TrackEvent } from '../src/core/tracker.js';
|
|
import type { ProxyEvent } from '../src/core/proxy.js';
|
|
|
|
describe('toTrackEvent', () => {
|
|
it('flattens ProxyEvent + TransformInfo + Usage into a single record', () => {
|
|
const ev: ProxyEvent = {
|
|
method: 'POST',
|
|
path: '/v1/messages',
|
|
status: 200,
|
|
durationMs: 1234,
|
|
firstByteMs: 200,
|
|
info: {
|
|
compressed: true,
|
|
origChars: 16000,
|
|
imageCount: 1,
|
|
imageBytes: 2103,
|
|
staticChars: 14000,
|
|
dynamicChars: 500,
|
|
dynamicBlockCount: 2,
|
|
systemSha8: 'a1b2c3d4',
|
|
claudeMdSha8: 'cafebabe',
|
|
firstUserSha8: 'deadbeef',
|
|
unknownStaticTags: ['recent_files'],
|
|
env: {
|
|
cwd: '/Users/me/code/pp',
|
|
isGitRepo: true,
|
|
gitBranch: 'main',
|
|
platform: 'darwin',
|
|
osVersion: 'Darwin 25.0.0',
|
|
today: '2026-05-18',
|
|
},
|
|
},
|
|
usage: {
|
|
input_tokens: 42,
|
|
output_tokens: 7,
|
|
cache_creation_input_tokens: 0,
|
|
cache_read_input_tokens: 100,
|
|
},
|
|
};
|
|
const out = toTrackEvent(ev);
|
|
// Spot-check every category of field made it across with the right
|
|
// snake_case names.
|
|
expect(out.method).toBe('POST');
|
|
expect(out.path).toBe('/v1/messages');
|
|
expect(out.status).toBe(200);
|
|
expect(out.duration_ms).toBe(1234);
|
|
expect(out.first_byte_ms).toBe(200);
|
|
expect(out.compressed).toBe(true);
|
|
expect(out.orig_chars).toBe(16000);
|
|
expect(out.static_chars).toBe(14000);
|
|
expect(out.dynamic_chars).toBe(500);
|
|
expect(out.dynamic_block_count).toBe(2);
|
|
expect(out.system_sha8).toBe('a1b2c3d4');
|
|
expect(out.claude_md_sha8).toBe('cafebabe');
|
|
expect(out.first_user_sha8).toBe('deadbeef');
|
|
expect(out.unknown_static_tags).toEqual(['recent_files']);
|
|
expect(out.cwd).toBe('/Users/me/code/pp');
|
|
expect(out.git_branch).toBe('main');
|
|
expect(out.is_git_repo).toBe(true);
|
|
expect(out.input_tokens).toBe(42);
|
|
expect(out.cache_read_tokens).toBe(100);
|
|
expect(out.cache_create_tokens).toBe(0);
|
|
// ts is ISO8601
|
|
expect(out.ts).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
});
|
|
|
|
it('handles a minimal ProxyEvent (no info, no usage) without throwing', () => {
|
|
const out = toTrackEvent({
|
|
method: 'GET',
|
|
path: '/health',
|
|
status: 200,
|
|
durationMs: 4,
|
|
});
|
|
expect(out.method).toBe('GET');
|
|
expect(out.compressed).toBeUndefined();
|
|
expect(out.cwd).toBeUndefined();
|
|
expect(out.input_tokens).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('JsonLogTracker', () => {
|
|
it('emits one JSON line per event to the sink', () => {
|
|
const lines: string[] = [];
|
|
const t = new JsonLogTracker((s) => lines.push(s));
|
|
t.emit({ ts: '2026-05-18T00:00:00Z', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 1 } as TrackEvent);
|
|
t.emit({ ts: '2026-05-18T00:00:01Z', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 2 } as TrackEvent);
|
|
expect(lines).toHaveLength(2);
|
|
const parsed = lines.map((l) => JSON.parse(l));
|
|
expect(parsed[0].duration_ms).toBe(1);
|
|
expect(parsed[1].duration_ms).toBe(2);
|
|
});
|
|
|
|
it('swallows sink errors — tracker must never break a request', () => {
|
|
const t = new JsonLogTracker(() => {
|
|
throw new Error('disk full');
|
|
});
|
|
expect(() =>
|
|
t.emit({ ts: 'x', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 1 } as TrackEvent),
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('noopTracker', () => {
|
|
it('discards events silently', () => {
|
|
expect(() =>
|
|
noopTracker.emit({ ts: 'x', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 1 } as TrackEvent),
|
|
).not.toThrow();
|
|
});
|
|
});
|