sandoichi 0.1.1 → 0.1.2
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/package.json +1 -1
- package/src/hook-cli.mjs +8 -3
- package/src/proxy.mjs +8 -3
- package/src/telemetry.mjs +33 -0
package/package.json
CHANGED
package/src/hook-cli.mjs
CHANGED
|
@@ -5,21 +5,25 @@ import path from 'node:path';
|
|
|
5
5
|
import { createReceipt, normalizeEvent, normalizePolicy, optimizeToolOutput } from './core.mjs';
|
|
6
6
|
import { defaultMetricsPath, recordMetrics } from './metrics.mjs';
|
|
7
7
|
import {
|
|
8
|
-
defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
|
|
8
|
+
closeFinishedDays, defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
|
|
9
9
|
} from './telemetry.mjs';
|
|
10
10
|
|
|
11
|
+
const PLUGIN_VERSION = '0.1';
|
|
12
|
+
|
|
11
13
|
function todayUtc() { return new Date().toISOString().slice(0, 10); }
|
|
12
14
|
|
|
13
15
|
/** Only counts (never content, paths, or IDs) — see docs/plans/2026-08-25-sando-telemetry-design.md.
|
|
14
16
|
* `enforce` covers both `apply` and `dry-run`: both walk the real rewrite path, only
|
|
15
17
|
* `observe` collects without deciding anything. */
|
|
16
18
|
function recordHookTelemetry({ host, env, policy, optimization }) {
|
|
19
|
+
const configPath = defaultTelemetryConfigPath(env);
|
|
17
20
|
let config;
|
|
18
|
-
try { config = readTelemetryConfig(
|
|
21
|
+
try { config = readTelemetryConfig(configPath); } catch { return; }
|
|
19
22
|
if (!config.enabled) return;
|
|
20
23
|
try {
|
|
24
|
+
const statePaths = defaultTelemetryStatePaths(env);
|
|
21
25
|
incrementCounter({
|
|
22
|
-
statePaths
|
|
26
|
+
statePaths,
|
|
23
27
|
day: todayUtc(),
|
|
24
28
|
event: 'hook_summary',
|
|
25
29
|
host,
|
|
@@ -31,6 +35,7 @@ function recordHookTelemetry({ host, env, policy, optimization }) {
|
|
|
31
35
|
bytesSaved: Math.max(0, optimization.stats.inputBytes - optimization.stats.inlineBytes),
|
|
32
36
|
},
|
|
33
37
|
});
|
|
38
|
+
closeFinishedDays({ statePaths, configPath, day: todayUtc(), pluginVersion: PLUGIN_VERSION });
|
|
34
39
|
} catch { /* telemetry is best-effort and must never affect hook output */ }
|
|
35
40
|
}
|
|
36
41
|
|
package/src/proxy.mjs
CHANGED
|
@@ -4,9 +4,11 @@ import { estimateTokens } from './core.mjs';
|
|
|
4
4
|
import { detectProviderBody, listSemanticCandidates, transformProviderRequest } from './context-transform.mjs';
|
|
5
5
|
import { recordProxyRequest } from './proxy-metrics.mjs';
|
|
6
6
|
import {
|
|
7
|
-
defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
|
|
7
|
+
closeFinishedDays, defaultTelemetryConfigPath, defaultTelemetryStatePaths, incrementCounter, readTelemetryConfig,
|
|
8
8
|
} from './telemetry.mjs';
|
|
9
9
|
|
|
10
|
+
const PLUGIN_VERSION = '0.1';
|
|
11
|
+
|
|
10
12
|
function todayUtc() { return new Date().toISOString().slice(0, 10); }
|
|
11
13
|
|
|
12
14
|
/** Only counts (never request/response content) — see docs/plans/2026-08-25-sando-telemetry-design.md.
|
|
@@ -15,13 +17,15 @@ function todayUtc() { return new Date().toISOString().slice(0, 10); }
|
|
|
15
17
|
* under a fixed `claude` label — this is the one place the design doc's host/provider split is
|
|
16
18
|
* approximate, flagged for the design doc rather than silently assumed. */
|
|
17
19
|
function recordProxyTelemetry({ env, transformed, beforeText, afterText }) {
|
|
20
|
+
const configPath = defaultTelemetryConfigPath(env);
|
|
18
21
|
let config;
|
|
19
|
-
try { config = readTelemetryConfig(
|
|
22
|
+
try { config = readTelemetryConfig(configPath); } catch { return; }
|
|
20
23
|
if (!config.enabled) return;
|
|
21
24
|
const cacheWarm = transformed.stats.cacheRewriteRatio !== null;
|
|
22
25
|
try {
|
|
26
|
+
const statePaths = defaultTelemetryStatePaths(env);
|
|
23
27
|
incrementCounter({
|
|
24
|
-
statePaths
|
|
28
|
+
statePaths,
|
|
25
29
|
day: todayUtc(),
|
|
26
30
|
event: 'proxy_summary',
|
|
27
31
|
host: 'claude',
|
|
@@ -33,6 +37,7 @@ function recordProxyTelemetry({ env, transformed, beforeText, afterText }) {
|
|
|
33
37
|
cacheHitNo: cacheWarm ? 0 : 1,
|
|
34
38
|
},
|
|
35
39
|
});
|
|
40
|
+
closeFinishedDays({ statePaths, configPath, day: todayUtc(), pluginVersion: PLUGIN_VERSION });
|
|
36
41
|
} catch { /* telemetry is best-effort and must never affect the proxied response */ }
|
|
37
42
|
}
|
|
38
43
|
|
package/src/telemetry.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
|
|
5
7
|
import { atomicWrite, ensureDirectory, withLock } from './provider-usage.mjs';
|
|
6
8
|
|
|
@@ -257,6 +259,37 @@ export function closeDay({ statePaths, day, pluginVersion }) {
|
|
|
257
259
|
return closedRows;
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
function launchDetachedFlush({ statePaths, configPath, spawnImpl }) {
|
|
263
|
+
try {
|
|
264
|
+
const entryPath = fileURLToPath(new URL('./telemetry-flush-entry.mjs', import.meta.url));
|
|
265
|
+
const child = spawnImpl(process.execPath, [entryPath, '--queue', statePaths.queue, '--config', configPath], {
|
|
266
|
+
detached: true, env: {}, stdio: 'ignore', windowsHide: true,
|
|
267
|
+
});
|
|
268
|
+
child.unref();
|
|
269
|
+
} catch { /* telemetry must never affect the caller */ }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Closes every raw counter day before `day` and starts one detached uploader.
|
|
273
|
+
* The child receives only local state paths and an empty environment. */
|
|
274
|
+
export function closeFinishedDays({
|
|
275
|
+
statePaths, configPath = defaultTelemetryConfigPath(), day, pluginVersion,
|
|
276
|
+
spawnImpl = spawn,
|
|
277
|
+
} = {}) {
|
|
278
|
+
const days = new Set();
|
|
279
|
+
if (fs.existsSync(statePaths.counters)) {
|
|
280
|
+
const state = readCounters(statePaths.counters);
|
|
281
|
+
for (const entry of Object.values(state.counters)) {
|
|
282
|
+
if (typeof entry.day === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.day) && entry.day < day) days.add(entry.day);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const closedRows = [];
|
|
286
|
+
for (const closedDay of [...days].sort()) {
|
|
287
|
+
closedRows.push(...closeDay({ statePaths, day: closedDay, pluginVersion }));
|
|
288
|
+
}
|
|
289
|
+
if (closedRows.length) launchDetachedFlush({ statePaths, configPath, spawnImpl });
|
|
290
|
+
return closedRows;
|
|
291
|
+
}
|
|
292
|
+
|
|
260
293
|
export function loadBatch({ statePaths, max = DEFAULT_BATCH_MAX } = {}) {
|
|
261
294
|
return readQueueRows(statePaths.queue).slice(0, max);
|
|
262
295
|
}
|