sandoichi 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/index.mjs +75 -0
- package/package.json +1 -1
- package/src/accounting-cli.mjs +1 -1
- package/src/artifact-cli.mjs +67 -0
- package/src/artifact-lifecycle.mjs +67 -0
- package/src/artifact-recovery.mjs +138 -0
- package/src/artifact-store.mjs +44 -0
- package/src/cache-attribution.mjs +13 -3
- package/src/context-audit-cli.mjs +104 -0
- package/src/context-capture.mjs +200 -0
- package/src/context-classifier.mjs +142 -0
- package/src/context-footprint.mjs +299 -0
- package/src/context-transform.mjs +146 -17
- package/src/core.mjs +300 -35
- package/src/f1-telemetry.mjs +80 -0
- package/src/f4-telemetry.mjs +183 -0
- package/src/gateway-gate-cli.mjs +88 -0
- package/src/gateway-gate.mjs +412 -0
- package/src/history-archive.mjs +80 -0
- package/src/history-disclosure.mjs +70 -0
- package/src/hook-cli.mjs +17 -1
- package/src/lazy-mcp-gateway-stdio.mjs +59 -0
- package/src/lazy-mcp-gateway.mjs +295 -0
- package/src/mcp-server.mjs +72 -11
- package/src/metrics.mjs +4 -3
- package/src/provider-usage.mjs +103 -23
- package/src/proxy.mjs +435 -19
- package/src/result-disclosure.mjs +115 -0
- package/src/slice.mjs +419 -0
- package/src/statusline.mjs +5 -9
- package/src/telemetry.mjs +155 -29
package/src/telemetry.mjs
CHANGED
|
@@ -5,16 +5,44 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
7
|
import { atomicWrite, ensureDirectory, withLock } from './provider-usage.mjs';
|
|
8
|
+
import { PLUGIN_VERSION } from './version.mjs';
|
|
8
9
|
|
|
9
|
-
const SCHEMA_VERSION =
|
|
10
|
+
export const SCHEMA_VERSION = 2;
|
|
11
|
+
const LEGACY_SCHEMA_VERSION = 1;
|
|
12
|
+
const SUPPORTED_QUEUE_SCHEMA_VERSIONS = new Set([LEGACY_SCHEMA_VERSION, SCHEMA_VERSION]);
|
|
10
13
|
const MAX_STRING_LENGTH = 32;
|
|
11
14
|
const MAX_EVENT_BYTES = 2048;
|
|
12
15
|
|
|
13
|
-
const COUNT_BUCKETS = ['zero', 'one', '2_to_5', '6_to_20', '
|
|
14
|
-
const BYTE_BUCKETS = ['lt_4k', '4_to_16k', '16_to_64k', '
|
|
16
|
+
const COUNT_BUCKETS = ['zero', 'one', '2_to_5', '6_to_20', '21_to_100', 'gt_100'];
|
|
17
|
+
const BYTE_BUCKETS = ['lt_4k', '4_to_16k', '16_to_64k', '64_to_256k', '256k_to_1m', 'gte_1m'];
|
|
18
|
+
const LOCAL_ONLY_EVENTS = new Set(['f1_footprint', 'f4_gateway']);
|
|
15
19
|
const HOSTS = ['claude', 'codex'];
|
|
16
20
|
const PROVIDERS = ['anthropic', 'openai', 'unknown'];
|
|
17
21
|
const MODES = ['enforce', 'observe', 'dry_run'];
|
|
22
|
+
const F4_HOSTS = ['claude', 'codex', 'unknown'];
|
|
23
|
+
const F4_OPERATIONS = ['catalog', 'call'];
|
|
24
|
+
const F4_OUTCOMES = ['success', 'rejected', 'timeout', 'cancelled', 'error'];
|
|
25
|
+
const F4_LATENCY_BUCKETS = ['lt_10ms', '10_to_100ms', '100_to_1000ms', 'gte_1000ms'];
|
|
26
|
+
const F4_RESULT_BUCKETS = ['zero', 'one', '2_to_5', '6_to_20', 'gt_20', 'unknown'];
|
|
27
|
+
const F1_HOSTS = ['claude', 'codex'];
|
|
28
|
+
const F1_STATUSES = ['complete', 'partial', 'unavailable'];
|
|
29
|
+
const F1_RATIO_BUCKETS = ['zero', 'lt_1pct', '1_to_10pct', 'gt_10pct', 'unavailable'];
|
|
30
|
+
const F1_SIZE_BUCKETS = [...BYTE_BUCKETS, 'unavailable'];
|
|
31
|
+
const F1_INPUT_BUCKETS = [...COUNT_BUCKETS, 'unavailable'];
|
|
32
|
+
// Reduction without coverage reads as better than it is: a day that bounds heavily on the 3% of
|
|
33
|
+
// commands it recognises looks identical to one that bounds everything. These are the reasons the
|
|
34
|
+
// shell classifier already emits, so they are a closed set and carry nothing free-form.
|
|
35
|
+
// Counts alone cannot answer "how much did it reach": 1,276 routed and 43,945 bypassed both land
|
|
36
|
+
// in `gt_100`, and so would the reverse. The ratio is the field that carries the answer.
|
|
37
|
+
export const COVERAGE_RATIO_BUCKETS = ['zero', 'lt_1pct', '1_to_10pct', '10_to_50pct', '50_to_90pct', 'gt_90pct'];
|
|
38
|
+
|
|
39
|
+
export const COVERAGE_REASONS = [
|
|
40
|
+
'ambiguous-shell', 'compound-feeds-pipeline', 'compound-has-redirect', 'compound-segment-ambiguous',
|
|
41
|
+
'grep-shape', 'head-shape', 'invalid-input', 'read-shape', 'routing-disabled', 'sed-shape',
|
|
42
|
+
'tail-unbounded-from-end', 'unsafe-cwd', 'unsafe-grep-pattern', 'unsafe-grep-target',
|
|
43
|
+
'unsafe-read-target', 'unsupported-shell', 'unsupported-tool', 'other',
|
|
44
|
+
];
|
|
45
|
+
|
|
18
46
|
export const FAILURE_STAGES = [
|
|
19
47
|
'policy', 'input', 'redaction', 'optimization', 'artifact', 'output', 'upstream', 'response',
|
|
20
48
|
];
|
|
@@ -23,6 +51,7 @@ const SHARED_FIELDS = {
|
|
|
23
51
|
schema_version: (value) => value === SCHEMA_VERSION,
|
|
24
52
|
event: (value) => [
|
|
25
53
|
'hook_summary', 'proxy_summary', 'active_day', 'hook_failure_summary', 'proxy_failure_summary',
|
|
54
|
+
'f1_footprint', 'f4_gateway', 'coverage_summary',
|
|
26
55
|
].includes(value),
|
|
27
56
|
day_utc: (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value),
|
|
28
57
|
plugin_version: (value) => typeof value === 'string' && /^\d+\.\d+(?:\.\d+)?$/.test(value) && value.length <= MAX_STRING_LENGTH,
|
|
@@ -54,12 +83,37 @@ const PROXY_FAILURE_FIELDS = {
|
|
|
54
83
|
provider: (value) => PROVIDERS.includes(value),
|
|
55
84
|
failure_stage: (value) => FAILURE_STAGES.includes(value),
|
|
56
85
|
};
|
|
86
|
+
const COVERAGE_FIELDS = {
|
|
87
|
+
host: (value) => HOSTS.includes(value),
|
|
88
|
+
routed_bucket: (value) => COUNT_BUCKETS.includes(value),
|
|
89
|
+
bypassed_bucket: (value) => COUNT_BUCKETS.includes(value),
|
|
90
|
+
coverage_ratio_bucket: (value) => COVERAGE_RATIO_BUCKETS.includes(value),
|
|
91
|
+
top_bypass_reason: (value) => COVERAGE_REASONS.includes(value),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const F4_FIELDS = {
|
|
95
|
+
f4_host: (value) => F4_HOSTS.includes(value),
|
|
96
|
+
f4_operation: (value) => F4_OPERATIONS.includes(value),
|
|
97
|
+
f4_outcome: (value) => F4_OUTCOMES.includes(value),
|
|
98
|
+
f4_latency_bucket: (value) => F4_LATENCY_BUCKETS.includes(value),
|
|
99
|
+
f4_result_bucket: (value) => F4_RESULT_BUCKETS.includes(value),
|
|
100
|
+
};
|
|
101
|
+
const F1_FIELDS = {
|
|
102
|
+
f1_host: (value) => F1_HOSTS.includes(value),
|
|
103
|
+
f1_status: (value) => F1_STATUSES.includes(value),
|
|
104
|
+
f1_unknown_ratio_bucket: (value) => F1_RATIO_BUCKETS.includes(value),
|
|
105
|
+
f1_body_size_bucket: (value) => F1_SIZE_BUCKETS.includes(value),
|
|
106
|
+
f1_input_tokens_bucket: (value) => F1_INPUT_BUCKETS.includes(value),
|
|
107
|
+
};
|
|
57
108
|
|
|
58
109
|
function fieldsForEvent(eventType) {
|
|
110
|
+
if (eventType === 'f1_footprint') return F1_FIELDS;
|
|
59
111
|
if (eventType === 'hook_summary') return HOOK_FIELDS;
|
|
60
112
|
if (eventType === 'proxy_summary') return PROXY_FIELDS;
|
|
61
113
|
if (eventType === 'active_day') return ACTIVE_DAY_FIELDS;
|
|
62
114
|
if (eventType === 'hook_failure_summary') return HOOK_FAILURE_FIELDS;
|
|
115
|
+
if (eventType === 'f4_gateway') return F4_FIELDS;
|
|
116
|
+
if (eventType === 'coverage_summary') return COVERAGE_FIELDS;
|
|
63
117
|
return PROXY_FAILURE_FIELDS;
|
|
64
118
|
}
|
|
65
119
|
|
|
@@ -69,7 +123,8 @@ export function countBucket(count) {
|
|
|
69
123
|
if (count === 1) return 'one';
|
|
70
124
|
if (count <= 5) return '2_to_5';
|
|
71
125
|
if (count <= 20) return '6_to_20';
|
|
72
|
-
return '
|
|
126
|
+
if (count <= 100) return '21_to_100';
|
|
127
|
+
return 'gt_100';
|
|
73
128
|
}
|
|
74
129
|
|
|
75
130
|
export function byteBucket(bytes) {
|
|
@@ -77,7 +132,9 @@ export function byteBucket(bytes) {
|
|
|
77
132
|
if (bytes < 4096) return 'lt_4k';
|
|
78
133
|
if (bytes < 16384) return '4_to_16k';
|
|
79
134
|
if (bytes < 65536) return '16_to_64k';
|
|
80
|
-
return '
|
|
135
|
+
if (bytes < 262144) return '64_to_256k';
|
|
136
|
+
if (bytes < 1048576) return '256k_to_1m';
|
|
137
|
+
return 'gte_1m';
|
|
81
138
|
}
|
|
82
139
|
|
|
83
140
|
export function validateEvent(payload) {
|
|
@@ -106,11 +163,6 @@ export const TELEMETRY_CONFIG_VERSION = 1;
|
|
|
106
163
|
export const CONSENT_VERSION = 1;
|
|
107
164
|
export const TELEMETRY_DETAILS_URL = 'https://github.com/yuzushi-dev/Sando/blob/main/TELEMETRY.md';
|
|
108
165
|
export const CONSENT_STATES = ['unasked', 'asked', 'enabled', 'declined'];
|
|
109
|
-
// Canary phase: shared backend, fronted by a Cloudflare Tunnel so it's
|
|
110
|
-
// reachable from any of the owner's machines (see
|
|
111
|
-
// session-handoff/deploy/telemetry/). Rate-limited at nginx (30 req/min/IP).
|
|
112
|
-
// Release/broader publication is still gated on the open items in
|
|
113
|
-
// session-handoff/docs/telemetry-canary-report.md.
|
|
114
166
|
export const TELEMETRY_ENDPOINT = 'https://telemetry.yuzushi.party/v1/logs';
|
|
115
167
|
|
|
116
168
|
export function isDoNotTrack(env = process.env) {
|
|
@@ -230,16 +282,26 @@ const LEASE_MS = 5 * 60 * 1000;
|
|
|
230
282
|
const RETRY_DELAYS_MS = [60_000, 300_000, 1_800_000, 7_200_000, 21_600_000];
|
|
231
283
|
const CHILD_ENV_KEYS = ['HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE'];
|
|
232
284
|
|
|
233
|
-
function emptyCounters() { return { schema_version: TELEMETRY_CONFIG_VERSION, counters: {}, active_days: {} }; }
|
|
285
|
+
function emptyCounters() { return { schema_version: TELEMETRY_CONFIG_VERSION, counters: {}, active_days: {}, coverage_days: {} }; }
|
|
234
286
|
function readCounters(countersPath) {
|
|
235
287
|
if (!fs.existsSync(countersPath)) return emptyCounters();
|
|
236
288
|
const state = JSON.parse(fs.readFileSync(countersPath, 'utf8'));
|
|
237
|
-
return { ...state, counters: state.counters ?? {}, active_days: state.active_days ?? {} };
|
|
289
|
+
return { ...state, counters: state.counters ?? {}, active_days: state.active_days ?? {}, coverage_days: state.coverage_days ?? {} };
|
|
238
290
|
}
|
|
239
291
|
|
|
240
292
|
function readQueueRows(queuePath) {
|
|
241
293
|
if (!fs.existsSync(queuePath)) return [];
|
|
242
|
-
return fs.readFileSync(queuePath, 'utf8').split('\n').filter(Boolean).map((line) =>
|
|
294
|
+
return fs.readFileSync(queuePath, 'utf8').split('\n').filter(Boolean).map((line) => {
|
|
295
|
+
const row = JSON.parse(line);
|
|
296
|
+
assertQueueRow(row);
|
|
297
|
+
return row;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function assertQueueRow(row) {
|
|
302
|
+
if (!record(row) || !SUPPORTED_QUEUE_SCHEMA_VERSIONS.has(row.schema_version)) {
|
|
303
|
+
throw new Error('telemetry queue event schema is unsupported');
|
|
304
|
+
}
|
|
243
305
|
}
|
|
244
306
|
|
|
245
307
|
function writeQueueRows(queuePath, rows) {
|
|
@@ -253,7 +315,11 @@ function writeQueueRows(queuePath, rows) {
|
|
|
253
315
|
|
|
254
316
|
/** Enforces the bounded queue (4096 rows / 4 MiB), dropping the oldest rows first —
|
|
255
317
|
* a telemetry backlog must never grow without bound or block product behavior. */
|
|
256
|
-
function appendQueueRows(queuePath, newRows) {
|
|
318
|
+
export function appendQueueRows(queuePath, newRows) {
|
|
319
|
+
for (const row of newRows) {
|
|
320
|
+
assertQueueRow(row);
|
|
321
|
+
if (LOCAL_ONLY_EVENTS.has(row.event)) throw new Error(`${row.event}: local-only event must never reach the upload queue`);
|
|
322
|
+
}
|
|
257
323
|
ensureDirectory(path.dirname(queuePath));
|
|
258
324
|
withLock(`${queuePath}.lock`, () => {
|
|
259
325
|
let rows = readQueueRows(queuePath);
|
|
@@ -277,9 +343,10 @@ function publicRow(row) {
|
|
|
277
343
|
}
|
|
278
344
|
|
|
279
345
|
function bucketEntry(entry, pluginVersion) {
|
|
346
|
+
const recordedVersion = entry.pluginVersion ?? pluginVersion;
|
|
280
347
|
if (entry.event === 'hook_summary') {
|
|
281
348
|
return {
|
|
282
|
-
schema_version:
|
|
349
|
+
schema_version: SCHEMA_VERSION, event: 'hook_summary', day_utc: entry.day, plugin_version: recordedVersion,
|
|
283
350
|
host: entry.host, mode: entry.mode,
|
|
284
351
|
tool_calls_bucket: countBucket(entry.toolCalls ?? 0),
|
|
285
352
|
capped_outputs_bucket: countBucket(entry.cappedOutputs ?? 0),
|
|
@@ -288,41 +355,70 @@ function bucketEntry(entry, pluginVersion) {
|
|
|
288
355
|
};
|
|
289
356
|
}
|
|
290
357
|
if (entry.event === 'proxy_summary') return {
|
|
291
|
-
schema_version:
|
|
358
|
+
schema_version: SCHEMA_VERSION, event: 'proxy_summary', day_utc: entry.day, plugin_version: recordedVersion,
|
|
292
359
|
provider: entry.provider ?? 'unknown', mode: entry.mode ?? 'enforce',
|
|
293
360
|
rewrites_applied_bucket: countBucket(entry.rewritesApplied ?? 0),
|
|
294
361
|
rewrites_skipped_cache_bucket: countBucket(entry.rewritesSkippedCache ?? 0),
|
|
295
362
|
input_tokens_saved_bucket: byteBucket(entry.inputTokensSaved ?? 0),
|
|
296
363
|
};
|
|
364
|
+
if (entry.event === 'coverage_summary') {
|
|
365
|
+
const reasons = Object.entries(entry)
|
|
366
|
+
.filter(([field, count]) => field.startsWith(REASON_PREFIX) && Number.isInteger(count) && count > 0)
|
|
367
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
368
|
+
const leading = reasons[0]?.[0].slice(REASON_PREFIX.length);
|
|
369
|
+
const routed = entry.routed ?? 0;
|
|
370
|
+
const bypassed = entry.bypassed ?? 0;
|
|
371
|
+
return {
|
|
372
|
+
schema_version: SCHEMA_VERSION, event: 'coverage_summary', day_utc: entry.day, plugin_version: recordedVersion,
|
|
373
|
+
host: entry.host,
|
|
374
|
+
routed_bucket: countBucket(routed),
|
|
375
|
+
bypassed_bucket: countBucket(bypassed),
|
|
376
|
+
coverage_ratio_bucket: coverageRatioBucket(routed, routed + bypassed),
|
|
377
|
+
// A reason this build does not know travels as `other`, never as free text.
|
|
378
|
+
top_bypass_reason: COVERAGE_REASONS.includes(leading) ? leading : 'other',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
297
381
|
if (entry.event === 'hook_failure_summary') return {
|
|
298
|
-
schema_version:
|
|
382
|
+
schema_version: SCHEMA_VERSION, event: 'hook_failure_summary', day_utc: entry.day, plugin_version: recordedVersion,
|
|
299
383
|
host: entry.host, failure_stage: entry.failureStage,
|
|
300
384
|
};
|
|
301
385
|
return {
|
|
302
|
-
schema_version:
|
|
386
|
+
schema_version: SCHEMA_VERSION, event: 'proxy_failure_summary', day_utc: entry.day, plugin_version: recordedVersion,
|
|
303
387
|
provider: entry.provider, failure_stage: entry.failureStage,
|
|
304
388
|
};
|
|
305
389
|
}
|
|
306
390
|
|
|
307
391
|
/** Accumulates raw per-day counts in memory/on disk; values are only bucketed (and thus
|
|
308
392
|
* only ever leave the machine) once `closeDay` closes a finished UTC day. */
|
|
309
|
-
export function incrementCounter({ statePaths, day, event, host, provider, mode, failureStage, deltas = {} }) {
|
|
310
|
-
if (!['hook_summary', 'proxy_summary', 'hook_failure_summary', 'proxy_failure_summary'].includes(event)) {
|
|
393
|
+
export function incrementCounter({ statePaths, day, pluginVersion = PLUGIN_VERSION, event, host, provider, mode, failureStage, deltas = {} }) {
|
|
394
|
+
if (!['hook_summary', 'proxy_summary', 'hook_failure_summary', 'proxy_failure_summary', 'coverage_summary'].includes(event)) {
|
|
311
395
|
throw new Error('incrementCounter: invalid event');
|
|
312
396
|
}
|
|
313
397
|
const isProxy = event.startsWith('proxy_');
|
|
314
398
|
const dimension = isProxy ? provider : host;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
399
|
+
if (!SHARED_FIELDS.plugin_version(pluginVersion)) throw new Error('incrementCounter: invalid plugin version');
|
|
400
|
+
const suffix = event.includes('failure') ? failureStage ?? '' : mode ?? '';
|
|
401
|
+
const key = [day, pluginVersion, event, dimension, suffix].join('|');
|
|
402
|
+
const legacyKey = [day, event, dimension, suffix].join('|');
|
|
318
403
|
ensureDirectory(path.dirname(statePaths.counters));
|
|
319
404
|
withLock(`${statePaths.counters}.lock`, () => {
|
|
320
405
|
const state = readCounters(statePaths.counters);
|
|
321
|
-
const
|
|
322
|
-
|
|
406
|
+
const legacy = state.counters[legacyKey];
|
|
407
|
+
let existing = state.counters[key];
|
|
408
|
+
if (legacy && !legacy.pluginVersion) {
|
|
409
|
+
if (existing) {
|
|
410
|
+
for (const [field, value] of Object.entries(legacy)) {
|
|
411
|
+
if (Number.isInteger(value) && value >= 0) existing[field] = (existing[field] ?? 0) + value;
|
|
412
|
+
}
|
|
413
|
+
} else existing = legacy;
|
|
414
|
+
delete state.counters[legacyKey];
|
|
415
|
+
}
|
|
416
|
+
existing ??= {
|
|
417
|
+
day, pluginVersion, event, ...(isProxy ? { provider: dimension } : { host: dimension }),
|
|
323
418
|
...(event.endsWith('_summary') && !event.includes('failure') ? { mode: mode ?? null } : {}),
|
|
324
419
|
...(event.includes('failure') ? { failureStage } : {}),
|
|
325
420
|
};
|
|
421
|
+
existing.pluginVersion = pluginVersion;
|
|
326
422
|
for (const [field, value] of Object.entries(deltas)) {
|
|
327
423
|
if (!Number.isInteger(value) || value < 0) throw new Error(`incrementCounter: invalid delta ${field}`);
|
|
328
424
|
existing[field] = (existing[field] ?? 0) + value;
|
|
@@ -332,23 +428,52 @@ export function incrementCounter({ statePaths, day, event, host, provider, mode,
|
|
|
332
428
|
});
|
|
333
429
|
}
|
|
334
430
|
|
|
335
|
-
export function recordFailure({ statePaths, day, event, host, provider, failureStage }) {
|
|
431
|
+
export function recordFailure({ statePaths, day, pluginVersion = PLUGIN_VERSION, event, host, provider, failureStage }) {
|
|
336
432
|
incrementCounter({
|
|
337
|
-
statePaths, day, event, host, provider, failureStage, deltas: { count: 1 },
|
|
433
|
+
statePaths, day, pluginVersion, event, host, provider, failureStage, deltas: { count: 1 },
|
|
338
434
|
});
|
|
339
435
|
}
|
|
340
436
|
|
|
341
437
|
/** Queues a single non-aggregate activity marker for this UTC day and host. */
|
|
342
|
-
export
|
|
438
|
+
export const REASON_PREFIX = 'reason:';
|
|
439
|
+
|
|
440
|
+
export function coverageRatioBucket(routed, total) {
|
|
441
|
+
if (!Number.isInteger(routed) || !Number.isInteger(total) || routed < 0 || total < routed) {
|
|
442
|
+
throw new Error('coverageRatioBucket: invalid counts');
|
|
443
|
+
}
|
|
444
|
+
if (total === 0 || routed === 0) return 'zero';
|
|
445
|
+
const ratio = routed / total;
|
|
446
|
+
if (ratio < 0.01) return 'lt_1pct';
|
|
447
|
+
if (ratio < 0.1) return '1_to_10pct';
|
|
448
|
+
if (ratio < 0.5) return '10_to_50pct';
|
|
449
|
+
if (ratio < 0.9) return '50_to_90pct';
|
|
450
|
+
return 'gt_90pct';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** One call per classified shell command. Counters close into a single row per day, next to the
|
|
454
|
+
* reduction they qualify: a large saving on a small share of commands should not read the same as
|
|
455
|
+
* a large saving on all of them. */
|
|
456
|
+
export function recordCoverage({ statePaths, day, pluginVersion = PLUGIN_VERSION, host, routed, reason }) {
|
|
457
|
+
const deltas = routed ? { routed: 1 } : { bypassed: 1 };
|
|
458
|
+
if (!routed) {
|
|
459
|
+
const label = COVERAGE_REASONS.includes(reason) ? reason : 'other';
|
|
460
|
+
deltas[`${REASON_PREFIX}${label}`] = 1;
|
|
461
|
+
}
|
|
462
|
+
incrementCounter({ statePaths, day, pluginVersion, event: 'coverage_summary', host, deltas });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function recordActiveDay({ statePaths, day, pluginVersion = PLUGIN_VERSION, host }) {
|
|
343
466
|
const marker = {
|
|
344
467
|
schema_version: SCHEMA_VERSION, event: 'active_day', day_utc: day, plugin_version: pluginVersion, host,
|
|
345
468
|
};
|
|
346
469
|
const validatedMarker = validateEvent(marker);
|
|
347
|
-
const activeDayKey = `${day}|${host}`;
|
|
470
|
+
const activeDayKey = `${day}|${pluginVersion}|${host}`;
|
|
471
|
+
const legacyActiveDayKey = `${day}|${host}`;
|
|
348
472
|
ensureDirectory(path.dirname(statePaths.counters));
|
|
349
473
|
withLock(`${statePaths.counters}.lock`, () => {
|
|
350
474
|
const state = readCounters(statePaths.counters);
|
|
351
475
|
const activeDays = state.active_days;
|
|
476
|
+
if (Object.hasOwn(activeDays, legacyActiveDayKey)) delete activeDays[legacyActiveDayKey];
|
|
352
477
|
const cutoff = Date.parse(`${day}T00:00:00Z`) - (ACTIVE_DAY_RETENTION_DAYS - 1) * 86_400_000;
|
|
353
478
|
for (const [key, recordedDay] of Object.entries(activeDays)) {
|
|
354
479
|
if (Date.parse(`${recordedDay}T00:00:00Z`) < cutoff) delete activeDays[key];
|
|
@@ -459,6 +584,7 @@ export function toOtlpLogs(rows) {
|
|
|
459
584
|
resource: { attributes: [{ key: 'service.name', value: { stringValue: 'sando' } }] },
|
|
460
585
|
scopeLogs: [{
|
|
461
586
|
logRecords: rows.map((row) => ({
|
|
587
|
+
...(typeof row._timeUnixNano === 'string' ? { timeUnixNano: row._timeUnixNano } : {}),
|
|
462
588
|
body: { stringValue: 'sando.daily_aggregate' },
|
|
463
589
|
attributes: Object.entries(publicRow(row)).map(([key, value]) => ({ key, value: { stringValue: String(value) } })),
|
|
464
590
|
})),
|