@velum-labs/routekit-telemetry-core 0.16.4 → 0.16.6
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/dist/index.d.ts +361 -10
- package/dist/index.js +375 -34
- package/dist/test/telemetry-core.test.js +181 -35
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
export declare const TELEMETRY_CATEGORIES: readonly ["usage", "reliability", "adoption"];
|
|
2
|
+
export type TelemetryCategory = (typeof TELEMETRY_CATEGORIES)[number];
|
|
3
|
+
export type TelemetryCategories = Record<TelemetryCategory, boolean>;
|
|
4
|
+
export declare const DEFAULT_TELEMETRY_CATEGORIES: Readonly<TelemetryCategories>;
|
|
1
5
|
export type ConsentFile = {
|
|
2
6
|
enabled: boolean;
|
|
7
|
+
categories?: Partial<TelemetryCategories>;
|
|
3
8
|
installId?: string;
|
|
4
9
|
decidedAt?: string;
|
|
5
10
|
};
|
|
6
11
|
export type ConsentDecision = {
|
|
7
12
|
enabled: boolean;
|
|
8
13
|
source: "do-not-track" | "env" | "config" | "default";
|
|
14
|
+
categories: TelemetryCategories;
|
|
9
15
|
installId?: string;
|
|
10
16
|
};
|
|
11
17
|
export type ConsentOptions = {
|
|
@@ -15,28 +21,373 @@ export type ConsentOptions = {
|
|
|
15
21
|
now?: () => Date;
|
|
16
22
|
randomId?: () => string;
|
|
17
23
|
};
|
|
18
|
-
/**
|
|
24
|
+
/** @deprecated Use the event schema inventory instead. */
|
|
19
25
|
export declare const CLI_COMMAND_TELEMETRY_FIELDS: readonly ["command", "cli_version", "os", "arch", "node_major", "duration_bucket", "exit_kind", "is_ci"];
|
|
20
26
|
export type TelemetryFieldMap = Readonly<Record<string, readonly string[]>>;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
export type TelemetryDestination = {
|
|
28
|
+
provider: "posthog";
|
|
29
|
+
host: string;
|
|
30
|
+
configured: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type TelemetryStatus = {
|
|
26
33
|
enabled: boolean;
|
|
27
34
|
source: ConsentDecision["source"];
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
categories: TelemetryCategories;
|
|
36
|
+
installIdPresent: boolean;
|
|
37
|
+
destination: TelemetryDestination;
|
|
38
|
+
schema: TelemetrySchemaInventory;
|
|
30
39
|
};
|
|
31
40
|
export declare function createConsentManager(options: ConsentOptions): {
|
|
32
41
|
path: () => string;
|
|
33
42
|
read: () => ConsentFile | undefined;
|
|
34
43
|
resolve: (env?: NodeJS.ProcessEnv) => ConsentDecision;
|
|
35
44
|
enable(): ConsentFile;
|
|
36
|
-
disable():
|
|
45
|
+
disable(): ConsentFile;
|
|
46
|
+
setCategory(category: TelemetryCategory, enabled: boolean): ConsentFile;
|
|
47
|
+
resetIdentity(env?: NodeJS.ProcessEnv): ConsentFile | undefined;
|
|
37
48
|
clear(): void;
|
|
38
49
|
};
|
|
39
|
-
export declare
|
|
50
|
+
export declare const DURATION_BUCKETS: readonly ["<1s", "1-10s", "10-60s", "1-5m", "5-30m", ">30m"];
|
|
51
|
+
export declare function durationBucket(ms: number): (typeof DURATION_BUCKETS)[number];
|
|
52
|
+
export declare const TELEMETRY_OUTCOMES: readonly ["success", "error", "cancelled"];
|
|
53
|
+
declare const OUTCOMES: readonly ["success", "error", "cancelled"];
|
|
54
|
+
export declare const COMMAND_PATHS: readonly ["start", "stop", "status", "doctor", "usage", "usage.redeem", "leaderboard", "accounts.login", "accounts.add", "accounts.rename", "accounts.remove", "accounts.list", "accounts.status", "calls.inspect", "config.path", "config.show", "config.init", "config.edit", "config.import", "config.migrate", "providers.add", "providers.remove", "providers.status", "models.list", "models.info", "remote.add", "remote.install", "remote.list", "remote.show", "remote.use", "remote.remove", "peer.add", "peer.show", "peer.remove", "token.issue", "token.list", "token.revoke", "codex", "claude", "cursor", "opencode", "codex.install", "codex.uninstall", "claude.install", "claude.uninstall", "daemon.reload", "daemon.status", "daemon.auth.show", "daemon.service.install", "daemon.service.uninstall", "daemon.service.status", "daemon.logs", "daemon.start", "daemon.stop", "daemon.restart", "daemon.upgrade"];
|
|
55
|
+
export declare const COMMAND_EXIT_KINDS: readonly ["success", "usage_error", "command_error", "cancelled"];
|
|
56
|
+
export declare const COMMAND_TARGET_KINDS: readonly ["local", "remote", "peer"];
|
|
57
|
+
export declare const COMMAND_OS_VALUES: readonly ["darwin", "linux", "win32", "other"];
|
|
58
|
+
export declare const COMMAND_ARCH_VALUES: readonly ["arm64", "x64", "other"];
|
|
59
|
+
export declare const COMMAND_NODE_MAJOR_VALUES: readonly ["22", "23", "24", "25", "26", "other"];
|
|
60
|
+
export declare const PRODUCT_OPERATIONS: readonly ["config_update", "config_import", "config_reload", "provider_enable", "provider_disable", "account_enroll", "account_enroll_activate", "account_remove", "account_sync", "launcher_prepare", "token_issue", "token_revoke"];
|
|
61
|
+
declare const COUNT_BUCKETS: readonly ["0", "1", "2-5", "6-20", ">20"];
|
|
62
|
+
declare const TOKEN_BUCKETS: readonly ["0", "1-1k", "1k-10k", "10k-100k", ">100k", "unknown"];
|
|
63
|
+
declare const RETRY_BUCKETS: readonly ["0", "1", "2", "3+"];
|
|
64
|
+
declare const BILLING_MODES: readonly ["metered-api", "subscription", "upstream-managed", "unknown"];
|
|
65
|
+
declare const DIALECTS: readonly ["openai-chat", "openai-responses", "anthropic-messages", "openai-embeddings"];
|
|
66
|
+
declare const REQUEST_KINDS: readonly ["chat", "responses", "messages", "embeddings"];
|
|
67
|
+
declare const PREFERENCE_ACTIONS: readonly ["master", "category", "identity-reset"];
|
|
68
|
+
declare const DAEMON_ACTIONS: readonly ["started", "stopped", "restarted", "reloaded"];
|
|
69
|
+
export declare const TELEMETRY_EVENT_DEFINITIONS: {
|
|
70
|
+
readonly "routekit.command_completed": {
|
|
71
|
+
readonly category: "adoption";
|
|
72
|
+
readonly properties: {
|
|
73
|
+
readonly command: {
|
|
74
|
+
readonly type: "string";
|
|
75
|
+
readonly maxLength: 64;
|
|
76
|
+
readonly values: readonly ["start", "stop", "status", "doctor", "usage", "usage.redeem", "leaderboard", "accounts.login", "accounts.add", "accounts.rename", "accounts.remove", "accounts.list", "accounts.status", "calls.inspect", "config.path", "config.show", "config.init", "config.edit", "config.import", "config.migrate", "providers.add", "providers.remove", "providers.status", "models.list", "models.info", "remote.add", "remote.install", "remote.list", "remote.show", "remote.use", "remote.remove", "peer.add", "peer.show", "peer.remove", "token.issue", "token.list", "token.revoke", "codex", "claude", "cursor", "opencode", "codex.install", "codex.uninstall", "claude.install", "claude.uninstall", "daemon.reload", "daemon.status", "daemon.auth.show", "daemon.service.install", "daemon.service.uninstall", "daemon.service.status", "daemon.logs", "daemon.start", "daemon.stop", "daemon.restart", "daemon.upgrade"];
|
|
77
|
+
};
|
|
78
|
+
readonly cli_version: {
|
|
79
|
+
readonly type: "string";
|
|
80
|
+
readonly maxLength: 32;
|
|
81
|
+
};
|
|
82
|
+
readonly os: {
|
|
83
|
+
readonly type: "string";
|
|
84
|
+
readonly maxLength: 16;
|
|
85
|
+
readonly values: readonly ["darwin", "linux", "win32", "other"];
|
|
86
|
+
};
|
|
87
|
+
readonly arch: {
|
|
88
|
+
readonly type: "string";
|
|
89
|
+
readonly maxLength: 16;
|
|
90
|
+
readonly values: readonly ["arm64", "x64", "other"];
|
|
91
|
+
};
|
|
92
|
+
readonly node_major: {
|
|
93
|
+
readonly type: "string";
|
|
94
|
+
readonly maxLength: 8;
|
|
95
|
+
readonly values: readonly ["22", "23", "24", "25", "26", "other"];
|
|
96
|
+
};
|
|
97
|
+
readonly duration_bucket: {
|
|
98
|
+
readonly type: "string";
|
|
99
|
+
readonly maxLength: 16;
|
|
100
|
+
readonly values: readonly ["<1s", "1-10s", "10-60s", "1-5m", "5-30m", ">30m"];
|
|
101
|
+
};
|
|
102
|
+
readonly outcome: {
|
|
103
|
+
readonly type: "string";
|
|
104
|
+
readonly maxLength: 16;
|
|
105
|
+
readonly values: readonly ["success", "error", "cancelled"];
|
|
106
|
+
};
|
|
107
|
+
readonly exit_kind: {
|
|
108
|
+
readonly type: "string";
|
|
109
|
+
readonly maxLength: 24;
|
|
110
|
+
readonly values: readonly ["success", "usage_error", "command_error", "cancelled"];
|
|
111
|
+
};
|
|
112
|
+
readonly is_ci: {
|
|
113
|
+
readonly type: "boolean";
|
|
114
|
+
};
|
|
115
|
+
readonly target_kind: {
|
|
116
|
+
readonly type: "string";
|
|
117
|
+
readonly maxLength: 16;
|
|
118
|
+
readonly values: readonly ["local", "remote", "peer"];
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
readonly "routekit.product_operation_completed": {
|
|
123
|
+
readonly category: "adoption";
|
|
124
|
+
readonly properties: {
|
|
125
|
+
readonly operation: {
|
|
126
|
+
readonly type: "string";
|
|
127
|
+
readonly maxLength: 64;
|
|
128
|
+
readonly values: readonly ["config_update", "config_import", "config_reload", "provider_enable", "provider_disable", "account_enroll", "account_enroll_activate", "account_remove", "account_sync", "launcher_prepare", "token_issue", "token_revoke"];
|
|
129
|
+
};
|
|
130
|
+
readonly outcome: {
|
|
131
|
+
readonly type: "string";
|
|
132
|
+
readonly maxLength: 16;
|
|
133
|
+
readonly values: readonly ["success", "error", "cancelled"];
|
|
134
|
+
};
|
|
135
|
+
readonly duration_bucket: {
|
|
136
|
+
readonly type: "string";
|
|
137
|
+
readonly maxLength: 16;
|
|
138
|
+
readonly values: readonly ["<1s", "1-10s", "10-60s", "1-5m", "5-30m", ">30m"];
|
|
139
|
+
};
|
|
140
|
+
readonly version: {
|
|
141
|
+
readonly type: "string";
|
|
142
|
+
readonly maxLength: 32;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
readonly "routekit.daemon_lifecycle": {
|
|
147
|
+
readonly category: "reliability";
|
|
148
|
+
readonly properties: {
|
|
149
|
+
readonly action: {
|
|
150
|
+
readonly type: "string";
|
|
151
|
+
readonly maxLength: 16;
|
|
152
|
+
readonly values: readonly ["started", "stopped", "restarted", "reloaded"];
|
|
153
|
+
};
|
|
154
|
+
readonly outcome: {
|
|
155
|
+
readonly type: "string";
|
|
156
|
+
readonly maxLength: 16;
|
|
157
|
+
readonly values: readonly ["success", "error", "cancelled"];
|
|
158
|
+
};
|
|
159
|
+
readonly supervisor: {
|
|
160
|
+
readonly type: "string";
|
|
161
|
+
readonly maxLength: 16;
|
|
162
|
+
readonly values: readonly ["systemd", "launchd", "detached", "unknown"];
|
|
163
|
+
};
|
|
164
|
+
readonly version: {
|
|
165
|
+
readonly type: "string";
|
|
166
|
+
readonly maxLength: 32;
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
readonly "routekit.gateway_usage_summary": {
|
|
171
|
+
readonly category: "usage";
|
|
172
|
+
readonly properties: {
|
|
173
|
+
readonly provider: {
|
|
174
|
+
readonly type: "string";
|
|
175
|
+
readonly maxLength: 100;
|
|
176
|
+
readonly canonical: true;
|
|
177
|
+
};
|
|
178
|
+
readonly model: {
|
|
179
|
+
readonly type: "string";
|
|
180
|
+
readonly maxLength: 200;
|
|
181
|
+
readonly canonical: true;
|
|
182
|
+
};
|
|
183
|
+
readonly dialect: {
|
|
184
|
+
readonly type: "string";
|
|
185
|
+
readonly maxLength: 32;
|
|
186
|
+
readonly values: readonly ["openai-chat", "openai-responses", "anthropic-messages", "openai-embeddings"];
|
|
187
|
+
};
|
|
188
|
+
readonly request_kind: {
|
|
189
|
+
readonly type: "string";
|
|
190
|
+
readonly maxLength: 16;
|
|
191
|
+
readonly values: readonly ["chat", "responses", "messages", "embeddings"];
|
|
192
|
+
};
|
|
193
|
+
readonly stream: {
|
|
194
|
+
readonly type: "boolean";
|
|
195
|
+
};
|
|
196
|
+
readonly billing_mode: {
|
|
197
|
+
readonly type: "string";
|
|
198
|
+
readonly maxLength: 32;
|
|
199
|
+
readonly values: readonly ["metered-api", "subscription", "upstream-managed", "unknown"];
|
|
200
|
+
};
|
|
201
|
+
readonly input_token_bucket: {
|
|
202
|
+
readonly type: "string";
|
|
203
|
+
readonly maxLength: 16;
|
|
204
|
+
readonly values: readonly ["0", "1-1k", "1k-10k", "10k-100k", ">100k", "unknown"];
|
|
205
|
+
};
|
|
206
|
+
readonly output_token_bucket: {
|
|
207
|
+
readonly type: "string";
|
|
208
|
+
readonly maxLength: 16;
|
|
209
|
+
readonly values: readonly ["0", "1-1k", "1k-10k", "10k-100k", ">100k", "unknown"];
|
|
210
|
+
};
|
|
211
|
+
readonly request_count_bucket: {
|
|
212
|
+
readonly type: "string";
|
|
213
|
+
readonly maxLength: 8;
|
|
214
|
+
readonly values: readonly ["0", "1", "2-5", "6-20", ">20"];
|
|
215
|
+
};
|
|
216
|
+
readonly version: {
|
|
217
|
+
readonly type: "string";
|
|
218
|
+
readonly maxLength: 32;
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
readonly "routekit.gateway_reliability_summary": {
|
|
223
|
+
readonly category: "reliability";
|
|
224
|
+
readonly properties: {
|
|
225
|
+
readonly provider: {
|
|
226
|
+
readonly type: "string";
|
|
227
|
+
readonly maxLength: 100;
|
|
228
|
+
readonly canonical: true;
|
|
229
|
+
};
|
|
230
|
+
readonly model: {
|
|
231
|
+
readonly type: "string";
|
|
232
|
+
readonly maxLength: 200;
|
|
233
|
+
readonly canonical: true;
|
|
234
|
+
};
|
|
235
|
+
readonly dialect: {
|
|
236
|
+
readonly type: "string";
|
|
237
|
+
readonly maxLength: 32;
|
|
238
|
+
readonly values: readonly ["openai-chat", "openai-responses", "anthropic-messages", "openai-embeddings"];
|
|
239
|
+
};
|
|
240
|
+
readonly request_kind: {
|
|
241
|
+
readonly type: "string";
|
|
242
|
+
readonly maxLength: 16;
|
|
243
|
+
readonly values: readonly ["chat", "responses", "messages", "embeddings"];
|
|
244
|
+
};
|
|
245
|
+
readonly stream: {
|
|
246
|
+
readonly type: "boolean";
|
|
247
|
+
};
|
|
248
|
+
readonly outcome: {
|
|
249
|
+
readonly type: "string";
|
|
250
|
+
readonly maxLength: 16;
|
|
251
|
+
readonly values: readonly ["success", "error", "cancelled"];
|
|
252
|
+
};
|
|
253
|
+
readonly latency_bucket: {
|
|
254
|
+
readonly type: "string";
|
|
255
|
+
readonly maxLength: 16;
|
|
256
|
+
readonly values: readonly ["<1s", "1-10s", "10-60s", "1-5m", "5-30m", ">30m"];
|
|
257
|
+
};
|
|
258
|
+
readonly retry_bucket: {
|
|
259
|
+
readonly type: "string";
|
|
260
|
+
readonly maxLength: 8;
|
|
261
|
+
readonly values: readonly ["0", "1", "2", "3+"];
|
|
262
|
+
};
|
|
263
|
+
readonly failover: {
|
|
264
|
+
readonly type: "boolean";
|
|
265
|
+
};
|
|
266
|
+
readonly request_count_bucket: {
|
|
267
|
+
readonly type: "string";
|
|
268
|
+
readonly maxLength: 8;
|
|
269
|
+
readonly values: readonly ["0", "1", "2-5", "6-20", ">20"];
|
|
270
|
+
};
|
|
271
|
+
readonly version: {
|
|
272
|
+
readonly type: "string";
|
|
273
|
+
readonly maxLength: 32;
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
readonly "routekit.telemetry_preference_changed": {
|
|
278
|
+
readonly category: "adoption";
|
|
279
|
+
readonly properties: {
|
|
280
|
+
readonly action: {
|
|
281
|
+
readonly type: "string";
|
|
282
|
+
readonly maxLength: 32;
|
|
283
|
+
readonly values: readonly ["master", "category", "identity-reset"];
|
|
284
|
+
};
|
|
285
|
+
readonly category: {
|
|
286
|
+
readonly type: "string";
|
|
287
|
+
readonly maxLength: 16;
|
|
288
|
+
readonly values: readonly ["usage", "reliability", "adoption"];
|
|
289
|
+
readonly required: false;
|
|
290
|
+
};
|
|
291
|
+
readonly enabled: {
|
|
292
|
+
readonly type: "boolean";
|
|
293
|
+
};
|
|
294
|
+
readonly source: {
|
|
295
|
+
readonly type: "string";
|
|
296
|
+
readonly maxLength: 16;
|
|
297
|
+
readonly values: readonly ["do-not-track", "env", "config", "default"];
|
|
298
|
+
};
|
|
299
|
+
readonly version: {
|
|
300
|
+
readonly type: "string";
|
|
301
|
+
readonly maxLength: 32;
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
export type TelemetryEventName = keyof typeof TELEMETRY_EVENT_DEFINITIONS;
|
|
307
|
+
export type CommandCompletedProperties = {
|
|
308
|
+
command: (typeof COMMAND_PATHS)[number];
|
|
309
|
+
cli_version: string;
|
|
310
|
+
os: (typeof COMMAND_OS_VALUES)[number];
|
|
311
|
+
arch: (typeof COMMAND_ARCH_VALUES)[number];
|
|
312
|
+
node_major: (typeof COMMAND_NODE_MAJOR_VALUES)[number];
|
|
313
|
+
duration_bucket: (typeof DURATION_BUCKETS)[number];
|
|
314
|
+
outcome: (typeof OUTCOMES)[number];
|
|
315
|
+
exit_kind: (typeof COMMAND_EXIT_KINDS)[number];
|
|
316
|
+
is_ci: boolean;
|
|
317
|
+
target_kind: (typeof COMMAND_TARGET_KINDS)[number];
|
|
318
|
+
};
|
|
319
|
+
export type TelemetryEventProperties = {
|
|
320
|
+
"routekit.command_completed": CommandCompletedProperties;
|
|
321
|
+
"routekit.product_operation_completed": {
|
|
322
|
+
operation: (typeof PRODUCT_OPERATIONS)[number];
|
|
323
|
+
outcome: (typeof OUTCOMES)[number];
|
|
324
|
+
duration_bucket: (typeof DURATION_BUCKETS)[number];
|
|
325
|
+
version: string;
|
|
326
|
+
};
|
|
327
|
+
"routekit.daemon_lifecycle": {
|
|
328
|
+
action: (typeof DAEMON_ACTIONS)[number];
|
|
329
|
+
outcome: (typeof OUTCOMES)[number];
|
|
330
|
+
supervisor: "systemd" | "launchd" | "detached" | "unknown";
|
|
331
|
+
version: string;
|
|
332
|
+
};
|
|
333
|
+
/** Gateway summaries are the only families permitted to carry canonical provider/model identifiers. */
|
|
334
|
+
"routekit.gateway_usage_summary": {
|
|
335
|
+
provider: string;
|
|
336
|
+
model: string;
|
|
337
|
+
dialect: (typeof DIALECTS)[number];
|
|
338
|
+
request_kind: (typeof REQUEST_KINDS)[number];
|
|
339
|
+
stream: boolean;
|
|
340
|
+
billing_mode: (typeof BILLING_MODES)[number];
|
|
341
|
+
input_token_bucket: (typeof TOKEN_BUCKETS)[number];
|
|
342
|
+
output_token_bucket: (typeof TOKEN_BUCKETS)[number];
|
|
343
|
+
request_count_bucket: (typeof COUNT_BUCKETS)[number];
|
|
344
|
+
version: string;
|
|
345
|
+
};
|
|
346
|
+
"routekit.gateway_reliability_summary": {
|
|
347
|
+
provider: string;
|
|
348
|
+
model: string;
|
|
349
|
+
dialect: (typeof DIALECTS)[number];
|
|
350
|
+
request_kind: (typeof REQUEST_KINDS)[number];
|
|
351
|
+
stream: boolean;
|
|
352
|
+
outcome: (typeof OUTCOMES)[number];
|
|
353
|
+
latency_bucket: (typeof DURATION_BUCKETS)[number];
|
|
354
|
+
retry_bucket: (typeof RETRY_BUCKETS)[number];
|
|
355
|
+
failover: boolean;
|
|
356
|
+
request_count_bucket: (typeof COUNT_BUCKETS)[number];
|
|
357
|
+
version: string;
|
|
358
|
+
};
|
|
359
|
+
"routekit.telemetry_preference_changed": {
|
|
360
|
+
action: (typeof PREFERENCE_ACTIONS)[number];
|
|
361
|
+
category?: TelemetryCategory;
|
|
362
|
+
enabled: boolean;
|
|
363
|
+
source: ConsentDecision["source"];
|
|
364
|
+
version: string;
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
export type TelemetrySchemaInventory = Readonly<Record<TelemetryEventName, {
|
|
368
|
+
category: TelemetryCategory;
|
|
369
|
+
fields: readonly string[];
|
|
370
|
+
}>>;
|
|
371
|
+
export declare const TELEMETRY_SCHEMA_VERSION = 1;
|
|
372
|
+
export declare const TELEMETRY_SCHEMA_INVENTORY: TelemetrySchemaInventory;
|
|
373
|
+
export type BuiltTelemetryEvent = {
|
|
374
|
+
event: TelemetryEventName;
|
|
375
|
+
category: TelemetryCategory;
|
|
376
|
+
properties: Record<string, unknown> & {
|
|
377
|
+
schema_version: 1;
|
|
378
|
+
$process_person_profile: false;
|
|
379
|
+
$ip: null;
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
export declare function buildTelemetryEvent<N extends TelemetryEventName>(name: N, source: TelemetryEventProperties[N]): BuiltTelemetryEvent;
|
|
383
|
+
export declare function telemetryStatusMetadata(decision: ConsentDecision, destinationOrFields: TelemetryDestination | TelemetryFieldMap, schema?: TelemetrySchemaInventory): TelemetryStatus | {
|
|
384
|
+
enabled: boolean;
|
|
385
|
+
source: ConsentDecision["source"];
|
|
386
|
+
installId: string | null;
|
|
387
|
+
fields: TelemetryFieldMap;
|
|
388
|
+
};
|
|
389
|
+
/** @deprecated Prefer buildTelemetryEvent, which rejects unknown properties. */
|
|
40
390
|
export declare function allowlistedProperties(source: Record<string, unknown>, allow: readonly string[]): Record<string, unknown>;
|
|
41
391
|
export declare function anonymousEventProperties(properties: Record<string, unknown>): Record<string, unknown>;
|
|
42
392
|
export declare function boundedShutdown(shutdown: () => Promise<unknown>, timeoutMs?: number): Promise<void>;
|
|
393
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,13 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { writeFileAtomic } from "@velum-labs/routekit-runtime";
|
|
5
|
-
|
|
5
|
+
export const TELEMETRY_CATEGORIES = ["usage", "reliability", "adoption"];
|
|
6
|
+
export const DEFAULT_TELEMETRY_CATEGORIES = Object.freeze({
|
|
7
|
+
usage: true,
|
|
8
|
+
reliability: true,
|
|
9
|
+
adoption: true
|
|
10
|
+
});
|
|
11
|
+
/** @deprecated Use the event schema inventory instead. */
|
|
6
12
|
export const CLI_COMMAND_TELEMETRY_FIELDS = [
|
|
7
13
|
"command",
|
|
8
14
|
"cli_version",
|
|
@@ -13,68 +19,94 @@ export const CLI_COMMAND_TELEMETRY_FIELDS = [
|
|
|
13
19
|
"exit_kind",
|
|
14
20
|
"is_ci"
|
|
15
21
|
];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
20
|
-
export function telemetryStatusMetadata(decision, fields) {
|
|
22
|
+
const truthy = (value) => value !== undefined && ["1", "true", "on", "yes"].includes(value.toLowerCase());
|
|
23
|
+
const falsy = (value) => value !== undefined && ["0", "false", "off", "no"].includes(value.toLowerCase());
|
|
24
|
+
function categories(value) {
|
|
21
25
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
fields
|
|
26
|
+
usage: value?.usage ?? DEFAULT_TELEMETRY_CATEGORIES.usage,
|
|
27
|
+
reliability: value?.reliability ?? DEFAULT_TELEMETRY_CATEGORIES.reliability,
|
|
28
|
+
adoption: value?.adoption ?? DEFAULT_TELEMETRY_CATEGORIES.adoption
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
|
-
const truthy = (value) => value !== undefined && ["1", "true", "on", "yes"].includes(value.toLowerCase());
|
|
29
|
-
const falsy = (value) => value !== undefined && ["0", "false", "off", "no"].includes(value.toLowerCase());
|
|
30
31
|
export function createConsentManager(options) {
|
|
32
|
+
let ephemeralInstallId;
|
|
33
|
+
const nextId = () => (options.randomId ?? randomUUID)();
|
|
34
|
+
const decidedAt = () => (options.now ?? (() => new Date()))().toISOString();
|
|
31
35
|
const read = () => {
|
|
32
36
|
const path = options.path();
|
|
33
37
|
if (!existsSync(path))
|
|
34
38
|
return undefined;
|
|
35
39
|
try {
|
|
36
40
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
37
|
-
if (typeof parsed
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
const candidate = parsed;
|
|
44
|
+
if (typeof candidate.enabled !== "boolean")
|
|
45
|
+
return undefined;
|
|
46
|
+
const normalized = {
|
|
47
|
+
enabled: candidate.enabled,
|
|
48
|
+
categories: categories(candidate.categories),
|
|
49
|
+
...(candidate.decidedAt !== undefined ? { decidedAt: candidate.decidedAt } : {})
|
|
50
|
+
};
|
|
51
|
+
if (candidate.enabled &&
|
|
52
|
+
typeof candidate.installId === "string" &&
|
|
53
|
+
candidate.installId.length > 0) {
|
|
54
|
+
normalized.installId = candidate.installId;
|
|
41
55
|
}
|
|
56
|
+
return normalized;
|
|
42
57
|
}
|
|
43
58
|
catch {
|
|
44
|
-
|
|
59
|
+
return undefined;
|
|
45
60
|
}
|
|
46
|
-
return undefined;
|
|
47
61
|
};
|
|
48
62
|
const write = (value) => {
|
|
49
63
|
const path = options.path();
|
|
64
|
+
const normalized = {
|
|
65
|
+
enabled: value.enabled,
|
|
66
|
+
categories: categories(value.categories),
|
|
67
|
+
...(value.enabled && value.installId !== undefined ? { installId: value.installId } : {}),
|
|
68
|
+
...(value.decidedAt !== undefined ? { decidedAt: value.decidedAt } : {})
|
|
69
|
+
};
|
|
50
70
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
51
71
|
chmodSync(dirname(path), 0o700);
|
|
52
|
-
writeFileAtomic(path, `${JSON.stringify(
|
|
72
|
+
writeFileAtomic(path, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
53
73
|
chmodSync(path, 0o600);
|
|
54
74
|
};
|
|
75
|
+
const stableId = (file) => {
|
|
76
|
+
if (file?.enabled && file.installId !== undefined)
|
|
77
|
+
return file.installId;
|
|
78
|
+
ephemeralInstallId ??= nextId();
|
|
79
|
+
return ephemeralInstallId;
|
|
80
|
+
};
|
|
55
81
|
const resolve = (env = process.env) => {
|
|
82
|
+
const file = read();
|
|
83
|
+
const selectedCategories = categories(file?.categories);
|
|
56
84
|
if (truthy(env[options.doNotTrackVariable ?? "DO_NOT_TRACK"])) {
|
|
57
|
-
return { enabled: false, source: "do-not-track" };
|
|
85
|
+
return { enabled: false, source: "do-not-track", categories: selectedCategories };
|
|
58
86
|
}
|
|
59
87
|
const override = env[options.environmentVariable];
|
|
60
|
-
if (falsy(override))
|
|
61
|
-
return { enabled: false, source: "env" };
|
|
62
|
-
|
|
88
|
+
if (falsy(override)) {
|
|
89
|
+
return { enabled: false, source: "env", categories: selectedCategories };
|
|
90
|
+
}
|
|
63
91
|
if (truthy(override)) {
|
|
64
92
|
return {
|
|
65
93
|
enabled: true,
|
|
66
94
|
source: "env",
|
|
67
|
-
|
|
95
|
+
categories: selectedCategories,
|
|
96
|
+
installId: stableId(file)
|
|
68
97
|
};
|
|
69
98
|
}
|
|
70
99
|
if (file !== undefined) {
|
|
100
|
+
if (!file.enabled)
|
|
101
|
+
return { enabled: false, source: "config", categories: selectedCategories };
|
|
71
102
|
return {
|
|
72
|
-
enabled:
|
|
103
|
+
enabled: true,
|
|
73
104
|
source: "config",
|
|
74
|
-
|
|
105
|
+
categories: selectedCategories,
|
|
106
|
+
installId: stableId(file)
|
|
75
107
|
};
|
|
76
108
|
}
|
|
77
|
-
return { enabled: false, source: "default" };
|
|
109
|
+
return { enabled: false, source: "default", categories: selectedCategories };
|
|
78
110
|
};
|
|
79
111
|
return {
|
|
80
112
|
path: options.path,
|
|
@@ -84,23 +116,75 @@ export function createConsentManager(options) {
|
|
|
84
116
|
const existing = read();
|
|
85
117
|
const file = {
|
|
86
118
|
enabled: true,
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
categories: categories(existing?.categories),
|
|
120
|
+
installId: existing?.enabled ? (existing.installId ?? nextId()) : nextId(),
|
|
121
|
+
decidedAt: decidedAt()
|
|
89
122
|
};
|
|
123
|
+
ephemeralInstallId = file.installId;
|
|
90
124
|
write(file);
|
|
91
125
|
return file;
|
|
92
126
|
},
|
|
93
127
|
disable() {
|
|
94
|
-
|
|
128
|
+
ephemeralInstallId = undefined;
|
|
129
|
+
const file = {
|
|
95
130
|
enabled: false,
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
categories: categories(read()?.categories),
|
|
132
|
+
decidedAt: decidedAt()
|
|
133
|
+
};
|
|
134
|
+
write(file);
|
|
135
|
+
return file;
|
|
136
|
+
},
|
|
137
|
+
setCategory(category, enabled) {
|
|
138
|
+
const existing = read();
|
|
139
|
+
const file = {
|
|
140
|
+
enabled: existing?.enabled ?? false,
|
|
141
|
+
categories: { ...categories(existing?.categories), [category]: enabled },
|
|
142
|
+
...(existing?.enabled && existing.installId !== undefined
|
|
143
|
+
? { installId: existing.installId }
|
|
144
|
+
: {}),
|
|
145
|
+
decidedAt: decidedAt()
|
|
146
|
+
};
|
|
147
|
+
write(file);
|
|
148
|
+
return file;
|
|
149
|
+
},
|
|
150
|
+
resetIdentity(env = process.env) {
|
|
151
|
+
const decision = resolve(env);
|
|
152
|
+
if (!decision.enabled) {
|
|
153
|
+
ephemeralInstallId = undefined;
|
|
154
|
+
const existing = read();
|
|
155
|
+
if (existing?.enabled) {
|
|
156
|
+
const disabled = {
|
|
157
|
+
enabled: false,
|
|
158
|
+
categories: categories(existing.categories),
|
|
159
|
+
decidedAt: decidedAt()
|
|
160
|
+
};
|
|
161
|
+
write(disabled);
|
|
162
|
+
return disabled;
|
|
163
|
+
}
|
|
164
|
+
return existing;
|
|
165
|
+
}
|
|
166
|
+
const installId = nextId();
|
|
167
|
+
ephemeralInstallId = installId;
|
|
168
|
+
const existing = read();
|
|
169
|
+
if (existing?.enabled) {
|
|
170
|
+
const file = {
|
|
171
|
+
enabled: true,
|
|
172
|
+
categories: categories(existing.categories),
|
|
173
|
+
installId,
|
|
174
|
+
decidedAt: decidedAt()
|
|
175
|
+
};
|
|
176
|
+
write(file);
|
|
177
|
+
return file;
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
98
180
|
},
|
|
99
181
|
clear() {
|
|
182
|
+
ephemeralInstallId = undefined;
|
|
100
183
|
rmSync(options.path(), { force: true });
|
|
101
184
|
}
|
|
102
185
|
};
|
|
103
186
|
}
|
|
187
|
+
export const DURATION_BUCKETS = ["<1s", "1-10s", "10-60s", "1-5m", "5-30m", ">30m"];
|
|
104
188
|
export function durationBucket(ms) {
|
|
105
189
|
if (ms < 1_000)
|
|
106
190
|
return "<1s";
|
|
@@ -114,12 +198,269 @@ export function durationBucket(ms) {
|
|
|
114
198
|
return "5-30m";
|
|
115
199
|
return ">30m";
|
|
116
200
|
}
|
|
201
|
+
export const TELEMETRY_OUTCOMES = ["success", "error", "cancelled"];
|
|
202
|
+
const OUTCOMES = TELEMETRY_OUTCOMES;
|
|
203
|
+
export const COMMAND_PATHS = [
|
|
204
|
+
"start",
|
|
205
|
+
"stop",
|
|
206
|
+
"status",
|
|
207
|
+
"doctor",
|
|
208
|
+
"usage",
|
|
209
|
+
"usage.redeem",
|
|
210
|
+
"leaderboard",
|
|
211
|
+
"accounts.login",
|
|
212
|
+
"accounts.add",
|
|
213
|
+
"accounts.rename",
|
|
214
|
+
"accounts.remove",
|
|
215
|
+
"accounts.list",
|
|
216
|
+
"accounts.status",
|
|
217
|
+
"calls.inspect",
|
|
218
|
+
"config.path",
|
|
219
|
+
"config.show",
|
|
220
|
+
"config.init",
|
|
221
|
+
"config.edit",
|
|
222
|
+
"config.import",
|
|
223
|
+
"config.migrate",
|
|
224
|
+
"providers.add",
|
|
225
|
+
"providers.remove",
|
|
226
|
+
"providers.status",
|
|
227
|
+
"models.list",
|
|
228
|
+
"models.info",
|
|
229
|
+
"remote.add",
|
|
230
|
+
"remote.install",
|
|
231
|
+
"remote.list",
|
|
232
|
+
"remote.show",
|
|
233
|
+
"remote.use",
|
|
234
|
+
"remote.remove",
|
|
235
|
+
"peer.add",
|
|
236
|
+
"peer.show",
|
|
237
|
+
"peer.remove",
|
|
238
|
+
"token.issue",
|
|
239
|
+
"token.list",
|
|
240
|
+
"token.revoke",
|
|
241
|
+
"codex",
|
|
242
|
+
"claude",
|
|
243
|
+
"cursor",
|
|
244
|
+
"opencode",
|
|
245
|
+
"codex.install",
|
|
246
|
+
"codex.uninstall",
|
|
247
|
+
"claude.install",
|
|
248
|
+
"claude.uninstall",
|
|
249
|
+
"daemon.reload",
|
|
250
|
+
"daemon.status",
|
|
251
|
+
"daemon.auth.show",
|
|
252
|
+
"daemon.service.install",
|
|
253
|
+
"daemon.service.uninstall",
|
|
254
|
+
"daemon.service.status",
|
|
255
|
+
"daemon.logs",
|
|
256
|
+
"daemon.start",
|
|
257
|
+
"daemon.stop",
|
|
258
|
+
"daemon.restart",
|
|
259
|
+
"daemon.upgrade"
|
|
260
|
+
];
|
|
261
|
+
export const COMMAND_EXIT_KINDS = ["success", "usage_error", "command_error", "cancelled"];
|
|
262
|
+
export const COMMAND_TARGET_KINDS = ["local", "remote", "peer"];
|
|
263
|
+
export const COMMAND_OS_VALUES = ["darwin", "linux", "win32", "other"];
|
|
264
|
+
export const COMMAND_ARCH_VALUES = ["arm64", "x64", "other"];
|
|
265
|
+
export const COMMAND_NODE_MAJOR_VALUES = ["22", "23", "24", "25", "26", "other"];
|
|
266
|
+
export const PRODUCT_OPERATIONS = [
|
|
267
|
+
"config_update",
|
|
268
|
+
"config_import",
|
|
269
|
+
"config_reload",
|
|
270
|
+
"provider_enable",
|
|
271
|
+
"provider_disable",
|
|
272
|
+
"account_enroll",
|
|
273
|
+
"account_enroll_activate",
|
|
274
|
+
"account_remove",
|
|
275
|
+
"account_sync",
|
|
276
|
+
"launcher_prepare",
|
|
277
|
+
"token_issue",
|
|
278
|
+
"token_revoke"
|
|
279
|
+
];
|
|
280
|
+
const COUNT_BUCKETS = ["0", "1", "2-5", "6-20", ">20"];
|
|
281
|
+
const TOKEN_BUCKETS = ["0", "1-1k", "1k-10k", "10k-100k", ">100k", "unknown"];
|
|
282
|
+
const RETRY_BUCKETS = ["0", "1", "2", "3+"];
|
|
283
|
+
const BILLING_MODES = ["metered-api", "subscription", "upstream-managed", "unknown"];
|
|
284
|
+
const DIALECTS = [
|
|
285
|
+
"openai-chat",
|
|
286
|
+
"openai-responses",
|
|
287
|
+
"anthropic-messages",
|
|
288
|
+
"openai-embeddings"
|
|
289
|
+
];
|
|
290
|
+
const REQUEST_KINDS = ["chat", "responses", "messages", "embeddings"];
|
|
291
|
+
const PREFERENCE_ACTIONS = ["master", "category", "identity-reset"];
|
|
292
|
+
const DAEMON_ACTIONS = ["started", "stopped", "restarted", "reloaded"];
|
|
293
|
+
export const TELEMETRY_EVENT_DEFINITIONS = {
|
|
294
|
+
"routekit.command_completed": {
|
|
295
|
+
category: "adoption",
|
|
296
|
+
properties: {
|
|
297
|
+
command: { type: "string", maxLength: 64, values: COMMAND_PATHS },
|
|
298
|
+
cli_version: { type: "string", maxLength: 32 },
|
|
299
|
+
os: { type: "string", maxLength: 16, values: COMMAND_OS_VALUES },
|
|
300
|
+
arch: { type: "string", maxLength: 16, values: COMMAND_ARCH_VALUES },
|
|
301
|
+
node_major: { type: "string", maxLength: 8, values: COMMAND_NODE_MAJOR_VALUES },
|
|
302
|
+
duration_bucket: { type: "string", maxLength: 16, values: DURATION_BUCKETS },
|
|
303
|
+
outcome: { type: "string", maxLength: 16, values: OUTCOMES },
|
|
304
|
+
exit_kind: { type: "string", maxLength: 24, values: COMMAND_EXIT_KINDS },
|
|
305
|
+
is_ci: { type: "boolean" },
|
|
306
|
+
target_kind: { type: "string", maxLength: 16, values: COMMAND_TARGET_KINDS }
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"routekit.product_operation_completed": {
|
|
310
|
+
category: "adoption",
|
|
311
|
+
properties: {
|
|
312
|
+
operation: { type: "string", maxLength: 64, values: PRODUCT_OPERATIONS },
|
|
313
|
+
outcome: { type: "string", maxLength: 16, values: OUTCOMES },
|
|
314
|
+
duration_bucket: { type: "string", maxLength: 16, values: DURATION_BUCKETS },
|
|
315
|
+
version: { type: "string", maxLength: 32 }
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
"routekit.daemon_lifecycle": {
|
|
319
|
+
category: "reliability",
|
|
320
|
+
properties: {
|
|
321
|
+
action: { type: "string", maxLength: 16, values: DAEMON_ACTIONS },
|
|
322
|
+
outcome: { type: "string", maxLength: 16, values: OUTCOMES },
|
|
323
|
+
supervisor: {
|
|
324
|
+
type: "string",
|
|
325
|
+
maxLength: 16,
|
|
326
|
+
values: ["systemd", "launchd", "detached", "unknown"]
|
|
327
|
+
},
|
|
328
|
+
version: { type: "string", maxLength: 32 }
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
"routekit.gateway_usage_summary": {
|
|
332
|
+
category: "usage",
|
|
333
|
+
properties: {
|
|
334
|
+
provider: { type: "string", maxLength: 100, canonical: true },
|
|
335
|
+
model: { type: "string", maxLength: 200, canonical: true },
|
|
336
|
+
dialect: { type: "string", maxLength: 32, values: DIALECTS },
|
|
337
|
+
request_kind: { type: "string", maxLength: 16, values: REQUEST_KINDS },
|
|
338
|
+
stream: { type: "boolean" },
|
|
339
|
+
billing_mode: { type: "string", maxLength: 32, values: BILLING_MODES },
|
|
340
|
+
input_token_bucket: { type: "string", maxLength: 16, values: TOKEN_BUCKETS },
|
|
341
|
+
output_token_bucket: { type: "string", maxLength: 16, values: TOKEN_BUCKETS },
|
|
342
|
+
request_count_bucket: { type: "string", maxLength: 8, values: COUNT_BUCKETS },
|
|
343
|
+
version: { type: "string", maxLength: 32 }
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
"routekit.gateway_reliability_summary": {
|
|
347
|
+
category: "reliability",
|
|
348
|
+
properties: {
|
|
349
|
+
provider: { type: "string", maxLength: 100, canonical: true },
|
|
350
|
+
model: { type: "string", maxLength: 200, canonical: true },
|
|
351
|
+
dialect: { type: "string", maxLength: 32, values: DIALECTS },
|
|
352
|
+
request_kind: { type: "string", maxLength: 16, values: REQUEST_KINDS },
|
|
353
|
+
stream: { type: "boolean" },
|
|
354
|
+
outcome: { type: "string", maxLength: 16, values: OUTCOMES },
|
|
355
|
+
latency_bucket: { type: "string", maxLength: 16, values: DURATION_BUCKETS },
|
|
356
|
+
retry_bucket: { type: "string", maxLength: 8, values: RETRY_BUCKETS },
|
|
357
|
+
failover: { type: "boolean" },
|
|
358
|
+
request_count_bucket: { type: "string", maxLength: 8, values: COUNT_BUCKETS },
|
|
359
|
+
version: { type: "string", maxLength: 32 }
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
"routekit.telemetry_preference_changed": {
|
|
363
|
+
category: "adoption",
|
|
364
|
+
properties: {
|
|
365
|
+
action: { type: "string", maxLength: 32, values: PREFERENCE_ACTIONS },
|
|
366
|
+
category: { type: "string", maxLength: 16, values: TELEMETRY_CATEGORIES, required: false },
|
|
367
|
+
enabled: { type: "boolean" },
|
|
368
|
+
source: {
|
|
369
|
+
type: "string",
|
|
370
|
+
maxLength: 16,
|
|
371
|
+
values: ["do-not-track", "env", "config", "default"]
|
|
372
|
+
},
|
|
373
|
+
version: { type: "string", maxLength: 32 }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
export const TELEMETRY_SCHEMA_VERSION = 1;
|
|
378
|
+
export const TELEMETRY_SCHEMA_INVENTORY = Object.freeze(Object.fromEntries(Object.entries(TELEMETRY_EVENT_DEFINITIONS).map(([name, definition]) => [
|
|
379
|
+
name,
|
|
380
|
+
{ category: definition.category, fields: Object.keys(definition.properties) }
|
|
381
|
+
])));
|
|
382
|
+
const FORBIDDEN_KEYS = /(?:^|_)(?:id|label|path|body|prompt|response|error|cost|usage|timing|token|secret|key)(?:$|_)/i;
|
|
383
|
+
const FORBIDDEN_VALUE = /(?:bearer\s+|sk-[a-z0-9]|-----begin [a-z ]+private key-----|(?:^|\s)\/(?:users|home|var|tmp)\/)/i;
|
|
384
|
+
const CANONICAL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/;
|
|
385
|
+
export function buildTelemetryEvent(name, source) {
|
|
386
|
+
if (!Object.hasOwn(TELEMETRY_EVENT_DEFINITIONS, name)) {
|
|
387
|
+
throw new TypeError(`unknown telemetry event: ${name}`);
|
|
388
|
+
}
|
|
389
|
+
const eventName = name;
|
|
390
|
+
const definition = TELEMETRY_EVENT_DEFINITIONS[eventName];
|
|
391
|
+
const input = source;
|
|
392
|
+
for (const key of Object.keys(input)) {
|
|
393
|
+
if (definition.properties[key] === undefined) {
|
|
394
|
+
const sensitive = FORBIDDEN_KEYS.test(key) ? " sensitive" : "";
|
|
395
|
+
throw new TypeError(`unknown${sensitive} telemetry property ${key} for ${name}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const [key, validator] of Object.entries(definition.properties)) {
|
|
399
|
+
if (validator.required !== false && input[key] === undefined) {
|
|
400
|
+
throw new TypeError(`missing telemetry property ${key} for ${name}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const output = {};
|
|
404
|
+
for (const [key, value] of Object.entries(input)) {
|
|
405
|
+
const validator = definition.properties[key];
|
|
406
|
+
if (validator === undefined) {
|
|
407
|
+
const sensitive = FORBIDDEN_KEYS.test(key) ? " sensitive" : "";
|
|
408
|
+
throw new TypeError(`unknown${sensitive} telemetry property ${key} for ${name}`);
|
|
409
|
+
}
|
|
410
|
+
if (validator.type === "boolean") {
|
|
411
|
+
if (typeof value !== "boolean")
|
|
412
|
+
throw new TypeError(`invalid telemetry property ${key}`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
if (typeof value !== "string" || value.length === 0 || value.length > validator.maxLength) {
|
|
416
|
+
throw new TypeError(`invalid telemetry property ${key}`);
|
|
417
|
+
}
|
|
418
|
+
if (validator.values !== undefined && !validator.values.includes(value)) {
|
|
419
|
+
throw new TypeError(`invalid telemetry property ${key}`);
|
|
420
|
+
}
|
|
421
|
+
if (validator.canonical && !CANONICAL_ID.test(value)) {
|
|
422
|
+
throw new TypeError(`invalid telemetry property ${key}`);
|
|
423
|
+
}
|
|
424
|
+
if (FORBIDDEN_VALUE.test(value))
|
|
425
|
+
throw new TypeError(`forbidden telemetry value for ${key}`);
|
|
426
|
+
}
|
|
427
|
+
output[key] = value;
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
event: eventName,
|
|
431
|
+
category: definition.category,
|
|
432
|
+
properties: anonymousEventProperties({
|
|
433
|
+
...output,
|
|
434
|
+
schema_version: TELEMETRY_SCHEMA_VERSION
|
|
435
|
+
})
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
export function telemetryStatusMetadata(decision, destinationOrFields, schema = TELEMETRY_SCHEMA_INVENTORY) {
|
|
439
|
+
if (!("provider" in destinationOrFields) ||
|
|
440
|
+
!("host" in destinationOrFields) ||
|
|
441
|
+
!("configured" in destinationOrFields)) {
|
|
442
|
+
return {
|
|
443
|
+
enabled: decision.enabled,
|
|
444
|
+
source: decision.source,
|
|
445
|
+
installId: decision.installId ?? null,
|
|
446
|
+
fields: destinationOrFields
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
enabled: decision.enabled,
|
|
451
|
+
source: decision.source,
|
|
452
|
+
categories: decision.categories,
|
|
453
|
+
installIdPresent: decision.installId !== undefined,
|
|
454
|
+
destination: destinationOrFields,
|
|
455
|
+
schema
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/** @deprecated Prefer buildTelemetryEvent, which rejects unknown properties. */
|
|
117
459
|
export function allowlistedProperties(source, allow) {
|
|
118
460
|
const properties = {};
|
|
119
|
-
for (const key of allow)
|
|
461
|
+
for (const key of allow)
|
|
120
462
|
if (source[key] !== undefined)
|
|
121
463
|
properties[key] = source[key];
|
|
122
|
-
}
|
|
123
464
|
return properties;
|
|
124
465
|
}
|
|
125
466
|
export function anonymousEventProperties(properties) {
|
|
@@ -1,50 +1,196 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { buildTelemetryEvent, createConsentManager, DEFAULT_TELEMETRY_CATEGORIES, TELEMETRY_SCHEMA_INVENTORY, telemetryStatusMetadata } from "../index.js";
|
|
7
|
+
function fixture(ids = ["id-1", "id-2", "id-3"]) {
|
|
8
8
|
const root = mkdtempSync(join(tmpdir(), "routekit-telemetry-"));
|
|
9
|
+
let index = 0;
|
|
10
|
+
const manager = createConsentManager({
|
|
11
|
+
path: () => join(root, "consent.json"),
|
|
12
|
+
environmentVariable: "ROUTEKIT_TELEMETRY",
|
|
13
|
+
randomId: () => ids[index++] ?? `id-${index}`,
|
|
14
|
+
now: () => new Date("2026-01-01T00:00:00.000Z")
|
|
15
|
+
});
|
|
16
|
+
return { root, path: join(root, "consent.json"), manager };
|
|
17
|
+
}
|
|
18
|
+
test("consent defaults off and migrates old files logically", () => {
|
|
19
|
+
const { root, path, manager } = fixture();
|
|
9
20
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
now: () => new Date("2026-01-01T00:00:00.000Z")
|
|
21
|
+
assert.deepEqual(manager.resolve({}), {
|
|
22
|
+
enabled: false,
|
|
23
|
+
source: "default",
|
|
24
|
+
categories: DEFAULT_TELEMETRY_CATEGORIES
|
|
15
25
|
});
|
|
16
|
-
|
|
17
|
-
assert.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
$ip: null
|
|
26
|
+
writeFileSync(path, JSON.stringify({ enabled: true, installId: "legacy" }));
|
|
27
|
+
assert.deepEqual(manager.resolve({}), {
|
|
28
|
+
enabled: true,
|
|
29
|
+
source: "config",
|
|
30
|
+
categories: DEFAULT_TELEMETRY_CATEGORIES,
|
|
31
|
+
installId: "legacy"
|
|
23
32
|
});
|
|
33
|
+
writeFileSync(path, JSON.stringify({ enabled: false, installId: "must-disappear" }));
|
|
34
|
+
assert.equal(manager.read()?.installId, undefined);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
rmSync(root, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
test("consent truthy/falsy matrix, DNT precedence, corruption, and identity stability", () => {
|
|
41
|
+
const { root, path, manager } = fixture();
|
|
42
|
+
try {
|
|
43
|
+
manager.enable();
|
|
44
|
+
for (const value of ["1", "true", "TRUE", "on", "yes"]) {
|
|
45
|
+
assert.equal(manager.resolve({ ROUTEKIT_TELEMETRY: value }).enabled, true, value);
|
|
46
|
+
}
|
|
47
|
+
for (const value of ["0", "false", "FALSE", "off", "no"]) {
|
|
48
|
+
const decision = manager.resolve({ ROUTEKIT_TELEMETRY: value });
|
|
49
|
+
assert.equal(decision.enabled, false, value);
|
|
50
|
+
assert.equal(decision.source, "env");
|
|
51
|
+
}
|
|
52
|
+
for (const value of ["1", "true", "on", "yes"]) {
|
|
53
|
+
const decision = manager.resolve({ ROUTEKIT_TELEMETRY: "1", DO_NOT_TRACK: value });
|
|
54
|
+
assert.equal(decision.enabled, false, value);
|
|
55
|
+
assert.equal(decision.source, "do-not-track");
|
|
56
|
+
}
|
|
57
|
+
manager.disable();
|
|
58
|
+
const first = manager.resolve({ ROUTEKIT_TELEMETRY: "yes" });
|
|
59
|
+
const second = manager.resolve({ ROUTEKIT_TELEMETRY: "yes" });
|
|
60
|
+
assert.equal(first.installId, second.installId);
|
|
61
|
+
writeFileSync(path, "{broken json");
|
|
62
|
+
assert.equal(manager.resolve({}).source, "default");
|
|
63
|
+
assert.equal(manager.resolve({}).enabled, false);
|
|
24
64
|
}
|
|
25
65
|
finally {
|
|
26
66
|
rmSync(root, { recursive: true, force: true });
|
|
27
67
|
}
|
|
28
68
|
});
|
|
29
|
-
test("
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
test("category changes, disable, re-enable, and identity reset preserve invariants", () => {
|
|
70
|
+
const { root, path, manager } = fixture();
|
|
71
|
+
try {
|
|
72
|
+
assert.equal(manager.enable().installId, "id-1");
|
|
73
|
+
manager.setCategory("usage", false);
|
|
74
|
+
assert.equal(manager.resolve({}).categories.usage, false);
|
|
75
|
+
assert.equal(manager.resetIdentity({})?.installId, "id-2");
|
|
76
|
+
manager.disable();
|
|
77
|
+
assert.equal(manager.resolve({}).installId, undefined);
|
|
78
|
+
assert.equal(JSON.parse(readFileSync(path, "utf8")).installId, undefined);
|
|
79
|
+
assert.deepEqual(manager.resetIdentity({}), manager.read());
|
|
80
|
+
assert.equal(manager.enable().installId, "id-3");
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
rmSync(root, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
test("event builder validates exact schemas and privacy canaries", () => {
|
|
87
|
+
assert.deepEqual(buildTelemetryEvent("routekit.gateway_usage_summary", {
|
|
88
|
+
provider: "openai",
|
|
89
|
+
model: "openai/gpt-5.2",
|
|
90
|
+
dialect: "openai-responses",
|
|
91
|
+
request_kind: "responses",
|
|
92
|
+
stream: true,
|
|
93
|
+
billing_mode: "metered-api",
|
|
94
|
+
input_token_bucket: "1k-10k",
|
|
95
|
+
output_token_bucket: "1-1k",
|
|
96
|
+
request_count_bucket: "1",
|
|
97
|
+
version: "0.17.0"
|
|
98
|
+
}).properties.$ip, null);
|
|
99
|
+
assert.equal(buildTelemetryEvent("routekit.gateway_reliability_summary", {
|
|
100
|
+
provider: "openai",
|
|
101
|
+
model: "openai/gpt-5.2",
|
|
102
|
+
dialect: "openai-responses",
|
|
103
|
+
request_kind: "responses",
|
|
104
|
+
stream: true,
|
|
105
|
+
outcome: "success",
|
|
106
|
+
latency_bucket: "1-10s",
|
|
107
|
+
retry_bucket: "1",
|
|
108
|
+
failover: false,
|
|
109
|
+
request_count_bucket: "1",
|
|
110
|
+
version: "0.17.0"
|
|
111
|
+
}).category, "reliability");
|
|
112
|
+
assert.throws(() => buildTelemetryEvent("routekit.unknown", {}), /unknown telemetry event/);
|
|
113
|
+
assert.throws(() => buildTelemetryEvent("routekit.command_completed", { path: "/tmp/a" }), /sensitive/);
|
|
114
|
+
assert.throws(() => buildTelemetryEvent("routekit.command_completed", { command: "Bearer secret" }), /forbidden|missing/);
|
|
115
|
+
assert.throws(() => buildTelemetryEvent("routekit.gateway_usage_summary", { provider: "open ai" }), /invalid|missing/);
|
|
116
|
+
assert.throws(() => buildTelemetryEvent("routekit.gateway_usage_summary", { latency_ms: 12 }), /unknown/);
|
|
117
|
+
});
|
|
118
|
+
test("schema inventory exposes exact category-truthful event families and fields", () => {
|
|
119
|
+
assert.deepEqual(TELEMETRY_SCHEMA_INVENTORY, {
|
|
120
|
+
"routekit.command_completed": {
|
|
121
|
+
category: "adoption",
|
|
122
|
+
fields: [
|
|
123
|
+
"command",
|
|
124
|
+
"cli_version",
|
|
125
|
+
"os",
|
|
126
|
+
"arch",
|
|
127
|
+
"node_major",
|
|
128
|
+
"duration_bucket",
|
|
129
|
+
"outcome",
|
|
130
|
+
"exit_kind",
|
|
131
|
+
"is_ci",
|
|
132
|
+
"target_kind"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"routekit.product_operation_completed": {
|
|
136
|
+
category: "adoption",
|
|
137
|
+
fields: ["operation", "outcome", "duration_bucket", "version"]
|
|
138
|
+
},
|
|
139
|
+
"routekit.daemon_lifecycle": {
|
|
140
|
+
category: "reliability",
|
|
141
|
+
fields: ["action", "outcome", "supervisor", "version"]
|
|
142
|
+
},
|
|
143
|
+
"routekit.gateway_usage_summary": {
|
|
144
|
+
category: "usage",
|
|
145
|
+
fields: [
|
|
146
|
+
"provider",
|
|
147
|
+
"model",
|
|
148
|
+
"dialect",
|
|
149
|
+
"request_kind",
|
|
150
|
+
"stream",
|
|
151
|
+
"billing_mode",
|
|
152
|
+
"input_token_bucket",
|
|
153
|
+
"output_token_bucket",
|
|
154
|
+
"request_count_bucket",
|
|
155
|
+
"version"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
"routekit.gateway_reliability_summary": {
|
|
159
|
+
category: "reliability",
|
|
160
|
+
fields: [
|
|
161
|
+
"provider",
|
|
162
|
+
"model",
|
|
163
|
+
"dialect",
|
|
164
|
+
"request_kind",
|
|
165
|
+
"stream",
|
|
166
|
+
"outcome",
|
|
167
|
+
"latency_bucket",
|
|
168
|
+
"retry_bucket",
|
|
169
|
+
"failover",
|
|
170
|
+
"request_count_bucket",
|
|
171
|
+
"version"
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
"routekit.telemetry_preference_changed": {
|
|
175
|
+
category: "adoption",
|
|
176
|
+
fields: ["action", "category", "enabled", "source", "version"]
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
test("status exposes presence and exact schema without identity", () => {
|
|
181
|
+
const status = telemetryStatusMetadata({
|
|
182
|
+
enabled: true,
|
|
183
|
+
source: "config",
|
|
184
|
+
categories: { usage: true, reliability: false, adoption: true },
|
|
185
|
+
installId: "private"
|
|
186
|
+
}, { provider: "posthog", host: "https://us.i.posthog.com", configured: false });
|
|
187
|
+
assert.deepEqual(status, {
|
|
188
|
+
enabled: true,
|
|
189
|
+
source: "config",
|
|
190
|
+
categories: { usage: true, reliability: false, adoption: true },
|
|
191
|
+
installIdPresent: true,
|
|
192
|
+
destination: { provider: "posthog", host: "https://us.i.posthog.com", configured: false },
|
|
193
|
+
schema: TELEMETRY_SCHEMA_INVENTORY
|
|
49
194
|
});
|
|
195
|
+
assert.equal("installId" in status, false);
|
|
50
196
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-telemetry-core",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.16.
|
|
4
|
+
"version": "0.16.6",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"provenance": true
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@velum-labs/routekit-runtime": "0.16.
|
|
29
|
+
"@velum-labs/routekit-runtime": "0.16.6"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"routekit",
|