@timo972/cc-router 0.12.1 → 0.12.2-rc.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/CHANGELOG.md +18 -0
- package/Dockerfile +1 -0
- package/README.md +1 -1
- package/dist/cli/cmd-accounts.js +116 -65
- package/dist/cli/cmd-setup.js +100 -30
- package/dist/cli/cmd-status.js +14 -2
- package/dist/cli/cmd-telemetry.js +43 -32
- package/dist/cli/index.js +15 -1
- package/dist/config/directory.js +14 -0
- package/dist/config/telemetry.js +192 -41
- package/dist/providers/anthropic/usage-refresher.js +35 -1
- package/dist/providers/model-discovery.js +17 -11
- package/dist/providers/openai/device-oauth.js +88 -31
- package/dist/providers/openai/token-refresher.js +12 -2
- package/dist/providers/openai/usage-fetch.js +19 -2
- package/dist/proxy/anthropic-messages-route.js +144 -5
- package/dist/proxy/anthropic-proxy.js +10 -0
- package/dist/proxy/anthropic-response-capture.js +6 -19
- package/dist/proxy/openai-ingress.js +98 -1
- package/dist/proxy/server.js +35 -22
- package/dist/proxy/token-refresher.js +12 -2
- package/dist/proxy/usage-capture.js +41 -4
- package/dist/telemetry/contracts.js +129 -0
- package/dist/telemetry/facade.js +654 -0
- package/dist/telemetry/otel-exporters.js +289 -0
- package/dist/telemetry/posthog-client.js +398 -0
- package/dist/telemetry/privacy.js +567 -0
- package/dist/telemetry/runtime.js +306 -0
- package/dist/telemetry/setup-diagnostics.js +239 -0
- package/dist/utils/token-extractor.js +79 -11
- package/dist/utils/token-validator.js +25 -9
- package/docs/README.md +34 -0
- package/docs/telemetry.md +167 -0
- package/package.json +12 -2
- package/dist/utils/telemetry.js +0 -88
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { ANALYTICS_EVENT_NAMES, CPU_ARCHITECTURES, DURATION_BUCKETS, HTTP_METHODS, INSTRUMENTATION_SCOPES, LOG_EVENT_CODES, MAX_ACCOUNT_POOL_SIZE, MAX_ATTEMPT, MAX_CONCURRENCY, MAX_DURATION_MS, ERROR_KINDS, MAX_STACK_FRAMES, MAX_STACK_FRAME_PATH_LENGTH, MAX_TIMESTAMP_MS, MAX_TOKEN_COUNT, MAX_VERSION_LENGTH, MODEL_FAMILIES, OPERATIONS, OS_FAMILIES, OUTCOMES, PROVIDERS, REQUEST_SOURCES, ROUTES, RUNTIME_MODES, SETUP_METHODS, SETUP_REASONS, SETUP_STAGES, SEVERITIES, SPAN_KINDS, SPAN_STATUS_CODES, STREAM_OUTCOMES, SYSTEM_ERROR_CODES, } from "./contracts.js";
|
|
4
|
+
// Only frames under this package's own dist/ or node_modules/ survive, so the
|
|
5
|
+
// user's home directory and workspace layout never reach a stack trace.
|
|
6
|
+
const PROJECT_ROOT = fileURLToPath(new URL("../../", import.meta.url))
|
|
7
|
+
.replace(/\\/g, "/")
|
|
8
|
+
.replace(/\/+$/, "");
|
|
9
|
+
const CASE_INSENSITIVE_ROOT = process.platform === "win32";
|
|
10
|
+
const COMPARISON_ROOT = CASE_INSENSITIVE_ROOT ? PROJECT_ROOT.toLowerCase() : PROJECT_ROOT;
|
|
11
|
+
/** OTel attribute name for every field of the closed span schema. */
|
|
12
|
+
export const SPAN_ATTRIBUTE_KEYS = {
|
|
13
|
+
httpMethod: "http.request.method",
|
|
14
|
+
httpStatusCode: "http.response.status_code",
|
|
15
|
+
provider: "cc_router.provider",
|
|
16
|
+
route: "cc_router.route",
|
|
17
|
+
modelFamily: "cc_router.model_family",
|
|
18
|
+
requestSource: "cc_router.request_source",
|
|
19
|
+
runtimeMode: "cc_router.runtime_mode",
|
|
20
|
+
streaming: "cc_router.streaming",
|
|
21
|
+
streamOutcome: "cc_router.stream_outcome",
|
|
22
|
+
outcome: "cc_router.outcome",
|
|
23
|
+
attempt: "cc_router.attempt",
|
|
24
|
+
accountPoolSize: "cc_router.account_pool_size",
|
|
25
|
+
concurrency: "cc_router.concurrency",
|
|
26
|
+
inputTokens: "cc_router.input_tokens",
|
|
27
|
+
outputTokens: "cc_router.output_tokens",
|
|
28
|
+
operationDurationMs: "cc_router.operation_duration_ms",
|
|
29
|
+
};
|
|
30
|
+
/** OTel attribute name for every field of both closed log schemas. */
|
|
31
|
+
export const LOG_ATTRIBUTE_KEYS = {
|
|
32
|
+
operation: "cc_router.operation",
|
|
33
|
+
provider: "cc_router.provider",
|
|
34
|
+
method: "cc_router.method",
|
|
35
|
+
stage: "cc_router.stage",
|
|
36
|
+
reason: "cc_router.reason",
|
|
37
|
+
outcome: "cc_router.outcome",
|
|
38
|
+
httpStatusCode: "http.response.status_code",
|
|
39
|
+
durationBucket: "cc_router.duration_bucket",
|
|
40
|
+
attempt: "cc_router.attempt",
|
|
41
|
+
accountPoolSize: "cc_router.account_pool_size",
|
|
42
|
+
concurrency: "cc_router.concurrency",
|
|
43
|
+
operationDurationMs: "cc_router.operation_duration_ms",
|
|
44
|
+
serviceVersion: "service.version",
|
|
45
|
+
osFamily: "os.type",
|
|
46
|
+
runtimeMode: "cc_router.runtime_mode",
|
|
47
|
+
diagnosticId: "cc_router.diagnostic_id",
|
|
48
|
+
};
|
|
49
|
+
/** OTel attribute name for every field of the closed resource schema. */
|
|
50
|
+
export const RESOURCE_ATTRIBUTE_KEYS = {
|
|
51
|
+
serviceName: "service.name",
|
|
52
|
+
serviceVersion: "service.version",
|
|
53
|
+
serviceInstanceId: "service.instance.id",
|
|
54
|
+
nodeVersion: "process.runtime.version",
|
|
55
|
+
osFamily: "os.type",
|
|
56
|
+
cpuArchitecture: "host.arch",
|
|
57
|
+
runtimeMode: "cc_router.runtime_mode",
|
|
58
|
+
};
|
|
59
|
+
function isRecord(value) {
|
|
60
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
function own(input, key) {
|
|
63
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
64
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
65
|
+
}
|
|
66
|
+
function member(values, value) {
|
|
67
|
+
return typeof value === "string" && values.includes(value)
|
|
68
|
+
? value
|
|
69
|
+
: undefined;
|
|
70
|
+
}
|
|
71
|
+
function otherEnum(values, value) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
return member(values, value) ?? member(values, "other");
|
|
75
|
+
}
|
|
76
|
+
function boundedInteger(value, maximum, minimum = 0) {
|
|
77
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum && value <= maximum
|
|
78
|
+
? value
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
81
|
+
function boundedNumber(value, maximum, minimum = 0) {
|
|
82
|
+
return typeof value === "number" && Number.isFinite(value) && value >= minimum && value <= maximum
|
|
83
|
+
? value
|
|
84
|
+
: undefined;
|
|
85
|
+
}
|
|
86
|
+
function version(value) {
|
|
87
|
+
if (typeof value !== "string" || value.length > MAX_VERSION_LENGTH)
|
|
88
|
+
return undefined;
|
|
89
|
+
return /^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/.test(value) ? value : undefined;
|
|
90
|
+
}
|
|
91
|
+
function uuid(value) {
|
|
92
|
+
if (typeof value !== "string")
|
|
93
|
+
return undefined;
|
|
94
|
+
const normalized = value.toLowerCase();
|
|
95
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(normalized)
|
|
96
|
+
? normalized
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
99
|
+
function installationId(identity) {
|
|
100
|
+
return uuid(identity?.installationId);
|
|
101
|
+
}
|
|
102
|
+
/** A per-occurrence diagnostic id must never collapse onto the stable install id. */
|
|
103
|
+
function diagnosticId(identity, trustedInstallationId) {
|
|
104
|
+
if (identity?.diagnosticId === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const value = uuid(identity.diagnosticId);
|
|
107
|
+
return value && value !== trustedInstallationId ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
function hexId(value, length) {
|
|
110
|
+
if (typeof value !== "string")
|
|
111
|
+
return undefined;
|
|
112
|
+
const normalized = value.toLowerCase();
|
|
113
|
+
return new RegExp(`^[0-9a-f]{${length}}$`).test(normalized) && !/^0+$/.test(normalized)
|
|
114
|
+
? normalized
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
function httpStatusCode(value) {
|
|
118
|
+
return boundedInteger(value, 599, 100);
|
|
119
|
+
}
|
|
120
|
+
function setupMethodForProvider(provider, value) {
|
|
121
|
+
const method = member(SETUP_METHODS, value);
|
|
122
|
+
if (provider === "anthropic") {
|
|
123
|
+
return method === "macos_keychain" || method === "claude_credentials_file" || method === "manual_token"
|
|
124
|
+
? method
|
|
125
|
+
: undefined;
|
|
126
|
+
}
|
|
127
|
+
return method === "manual_token" || method === "device_oauth" ? method : undefined;
|
|
128
|
+
}
|
|
129
|
+
function assignIfDefined(target, key, value) {
|
|
130
|
+
if (value !== undefined)
|
|
131
|
+
Object.assign(target, { [key]: value });
|
|
132
|
+
}
|
|
133
|
+
/** Project a reconstructed record onto its OTel attribute names. */
|
|
134
|
+
export function toOtelAttributes(keys, safe) {
|
|
135
|
+
const output = {};
|
|
136
|
+
for (const [field, attribute] of Object.entries(keys)) {
|
|
137
|
+
const value = own(safe, field);
|
|
138
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
139
|
+
output[attribute] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return output;
|
|
143
|
+
}
|
|
144
|
+
/** Read an untrusted OTel attribute bag back into candidate record fields. */
|
|
145
|
+
export function fromOtelAttributes(keys, attributes) {
|
|
146
|
+
const output = {};
|
|
147
|
+
for (const [field, attribute] of Object.entries(keys)) {
|
|
148
|
+
output[field] = own(attributes, attribute);
|
|
149
|
+
}
|
|
150
|
+
return output;
|
|
151
|
+
}
|
|
152
|
+
function errorKind(input) {
|
|
153
|
+
if (typeof AggregateError !== "undefined" && input instanceof AggregateError)
|
|
154
|
+
return "aggregate_error";
|
|
155
|
+
if (input instanceof TypeError)
|
|
156
|
+
return "type_error";
|
|
157
|
+
if (input instanceof RangeError)
|
|
158
|
+
return "range_error";
|
|
159
|
+
if (input instanceof ReferenceError)
|
|
160
|
+
return "reference_error";
|
|
161
|
+
if (input instanceof SyntaxError)
|
|
162
|
+
return "syntax_error";
|
|
163
|
+
if (input instanceof URIError)
|
|
164
|
+
return "uri_error";
|
|
165
|
+
if (input instanceof EvalError)
|
|
166
|
+
return "eval_error";
|
|
167
|
+
return "error";
|
|
168
|
+
}
|
|
169
|
+
// UUID-shaped segments are stripped: a temp/cache directory name can identify
|
|
170
|
+
// an install as reliably as a home directory does.
|
|
171
|
+
function safePathSegments(path) {
|
|
172
|
+
return path.split("/").every((segment) => segment.length > 0
|
|
173
|
+
&& segment !== "."
|
|
174
|
+
&& segment !== ".."
|
|
175
|
+
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)
|
|
176
|
+
&& /^[0-9A-Za-z@._+~-]+$/.test(segment));
|
|
177
|
+
}
|
|
178
|
+
function normalizedFramePath(rawPath) {
|
|
179
|
+
let path = rawPath.trim().replace(/\\/g, "/");
|
|
180
|
+
const openingParenthesis = path.lastIndexOf("(");
|
|
181
|
+
if (openingParenthesis >= 0)
|
|
182
|
+
path = path.slice(openingParenthesis + 1);
|
|
183
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(path) && !path.startsWith("file://"))
|
|
184
|
+
return undefined;
|
|
185
|
+
if (path.startsWith("file://"))
|
|
186
|
+
path = path.slice("file://".length);
|
|
187
|
+
if (/^\/[A-Za-z]:\//.test(path))
|
|
188
|
+
path = path.slice(1);
|
|
189
|
+
const comparisonPath = CASE_INSENSITIVE_ROOT ? path.toLowerCase() : path;
|
|
190
|
+
const projectDistPrefix = `${COMPARISON_ROOT}/dist/`;
|
|
191
|
+
if (comparisonPath.startsWith(projectDistPrefix)) {
|
|
192
|
+
const relative = `dist/${path.slice(projectDistPrefix.length)}`;
|
|
193
|
+
return relative.length <= MAX_STACK_FRAME_PATH_LENGTH && safePathSegments(relative)
|
|
194
|
+
? relative
|
|
195
|
+
: undefined;
|
|
196
|
+
}
|
|
197
|
+
const dependencyIndex = path.lastIndexOf("/node_modules/");
|
|
198
|
+
if (dependencyIndex >= 0) {
|
|
199
|
+
const relative = path.slice(dependencyIndex + 1);
|
|
200
|
+
const segments = relative.split("/");
|
|
201
|
+
const packageSegmentCount = segments[1]?.startsWith("@") ? 2 : 1;
|
|
202
|
+
if (segments.length < packageSegmentCount + 2 || !safePathSegments(relative))
|
|
203
|
+
return undefined;
|
|
204
|
+
return relative.length <= MAX_STACK_FRAME_PATH_LENGTH
|
|
205
|
+
? relative
|
|
206
|
+
: undefined;
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
function stackHeaderName(kind) {
|
|
211
|
+
switch (kind) {
|
|
212
|
+
case "type_error": return "TypeError";
|
|
213
|
+
case "range_error": return "RangeError";
|
|
214
|
+
case "reference_error": return "ReferenceError";
|
|
215
|
+
case "syntax_error": return "SyntaxError";
|
|
216
|
+
case "uri_error": return "URIError";
|
|
217
|
+
case "eval_error": return "EvalError";
|
|
218
|
+
case "aggregate_error": return "AggregateError";
|
|
219
|
+
default: return "Error";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function normalizedFrames(input, kind) {
|
|
223
|
+
const stack = input.stack;
|
|
224
|
+
if (typeof stack !== "string")
|
|
225
|
+
return [];
|
|
226
|
+
// The header carries the raw message. Parse frames only after removing it
|
|
227
|
+
// verbatim, so a multiline message can never inject a frame.
|
|
228
|
+
const rawMessage = own(input, "message");
|
|
229
|
+
if (rawMessage !== undefined && typeof rawMessage !== "string")
|
|
230
|
+
return [];
|
|
231
|
+
const header = `${stackHeaderName(kind)}${rawMessage ? `: ${rawMessage}` : ""}`;
|
|
232
|
+
if (!stack.startsWith(`${header}\n`))
|
|
233
|
+
return [];
|
|
234
|
+
const frames = [];
|
|
235
|
+
for (const line of stack.slice(header.length + 1).split("\n")) {
|
|
236
|
+
if (frames.length >= MAX_STACK_FRAMES)
|
|
237
|
+
break;
|
|
238
|
+
const match = line.match(/(?:\(|\bat\s+)(.+):(\d+):(\d+)\)?\s*$/);
|
|
239
|
+
if (!match)
|
|
240
|
+
continue;
|
|
241
|
+
const path = normalizedFramePath(match[1]);
|
|
242
|
+
const frameLine = boundedInteger(Number(match[2]), Number.MAX_SAFE_INTEGER, 1);
|
|
243
|
+
const column = boundedInteger(Number(match[3]), Number.MAX_SAFE_INTEGER, 1);
|
|
244
|
+
if (!path || frameLine === undefined || column === undefined)
|
|
245
|
+
continue;
|
|
246
|
+
frames.push({ path, line: frameLine, column });
|
|
247
|
+
}
|
|
248
|
+
return frames;
|
|
249
|
+
}
|
|
250
|
+
function sanitizedError(reason, frames) {
|
|
251
|
+
const error = new Error(reason);
|
|
252
|
+
error.stack = [
|
|
253
|
+
`Error: ${reason}`,
|
|
254
|
+
...frames.map(frame => ` at ${frame.path}:${frame.line}:${frame.column}`),
|
|
255
|
+
].join("\n");
|
|
256
|
+
return error;
|
|
257
|
+
}
|
|
258
|
+
function fingerprint(kind, context, systemErrorCode, status, frames) {
|
|
259
|
+
return createHash("sha256").update(JSON.stringify({
|
|
260
|
+
errorKind: kind,
|
|
261
|
+
category: context.category,
|
|
262
|
+
reason: context.reason,
|
|
263
|
+
operation: context.operation,
|
|
264
|
+
provider: context.provider,
|
|
265
|
+
setupStage: context.setupStage,
|
|
266
|
+
runtimeMode: context.runtimeMode,
|
|
267
|
+
systemErrorCode,
|
|
268
|
+
httpStatusCode: status,
|
|
269
|
+
frames,
|
|
270
|
+
})).digest("hex");
|
|
271
|
+
}
|
|
272
|
+
function exceptionContext(input) {
|
|
273
|
+
if (!isRecord(input))
|
|
274
|
+
return undefined;
|
|
275
|
+
const category = input.category === "setup" || input.category === "runtime" ? input.category : undefined;
|
|
276
|
+
const reason = otherEnum(SETUP_REASONS, input.reason);
|
|
277
|
+
if (!category || !reason)
|
|
278
|
+
return undefined;
|
|
279
|
+
const output = { category, reason };
|
|
280
|
+
assignIfDefined(output, "operation", member(OPERATIONS, input.operation));
|
|
281
|
+
assignIfDefined(output, "provider", otherEnum(PROVIDERS, input.provider));
|
|
282
|
+
assignIfDefined(output, "setupStage", member(SETUP_STAGES, input.setupStage));
|
|
283
|
+
assignIfDefined(output, "runtimeMode", member(RUNTIME_MODES, input.runtimeMode));
|
|
284
|
+
return output;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Reconstruct an exception from closed safe values. No original Error object,
|
|
288
|
+
* message, cause, or arbitrary thrown-value property escapes this boundary;
|
|
289
|
+
* a hostile thrown value can only cause the occurrence to be dropped.
|
|
290
|
+
*/
|
|
291
|
+
export function sanitizeException(input, candidateContext, identity) {
|
|
292
|
+
try {
|
|
293
|
+
const trustedInstallationId = installationId(identity);
|
|
294
|
+
if (!trustedInstallationId)
|
|
295
|
+
return undefined;
|
|
296
|
+
const trustedDiagnosticId = diagnosticId(identity, trustedInstallationId);
|
|
297
|
+
const context = exceptionContext(candidateContext);
|
|
298
|
+
if (!trustedDiagnosticId || !context)
|
|
299
|
+
return undefined;
|
|
300
|
+
const isError = input instanceof Error;
|
|
301
|
+
const kind = isError ? errorKind(input) : "unexpected_error";
|
|
302
|
+
const frames = isError ? normalizedFrames(input, kind) : [];
|
|
303
|
+
const code = isError
|
|
304
|
+
? member(SYSTEM_ERROR_CODES, own(input, "code"))
|
|
305
|
+
: undefined;
|
|
306
|
+
const status = isError
|
|
307
|
+
? httpStatusCode(own(input, "statusCode")) ?? httpStatusCode(own(input, "status"))
|
|
308
|
+
: undefined;
|
|
309
|
+
const output = {
|
|
310
|
+
error: sanitizedError(context.reason, frames),
|
|
311
|
+
...context,
|
|
312
|
+
errorKind: kind,
|
|
313
|
+
frames,
|
|
314
|
+
fingerprint: fingerprint(kind, context, code, status, frames),
|
|
315
|
+
diagnosticId: trustedDiagnosticId,
|
|
316
|
+
};
|
|
317
|
+
assignIfDefined(output, "systemErrorCode", code);
|
|
318
|
+
assignIfDefined(output, "httpStatusCode", status);
|
|
319
|
+
return output;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
export function reconstructResource(input, identity) {
|
|
326
|
+
if (!isRecord(input) || input.serviceName !== "cc-router")
|
|
327
|
+
return undefined;
|
|
328
|
+
const serviceVersion = version(input.serviceVersion);
|
|
329
|
+
const nodeVersion = version(input.nodeVersion);
|
|
330
|
+
const serviceInstanceId = installationId(identity);
|
|
331
|
+
const runtimeMode = member(RUNTIME_MODES, input.runtimeMode);
|
|
332
|
+
const osFamily = otherEnum(OS_FAMILIES, input.osFamily);
|
|
333
|
+
const cpuArchitecture = otherEnum(CPU_ARCHITECTURES, input.cpuArchitecture);
|
|
334
|
+
if (!serviceVersion || !nodeVersion || !serviceInstanceId || !runtimeMode || !osFamily || !cpuArchitecture) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
"service.name": "cc-router",
|
|
339
|
+
"service.version": serviceVersion,
|
|
340
|
+
"service.instance.id": serviceInstanceId,
|
|
341
|
+
"process.runtime.version": nodeVersion,
|
|
342
|
+
"os.type": osFamily,
|
|
343
|
+
"host.arch": cpuArchitecture,
|
|
344
|
+
"cc_router.runtime_mode": runtimeMode,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function reconstructSpanAttributes(input) {
|
|
348
|
+
if (!isRecord(input))
|
|
349
|
+
return {};
|
|
350
|
+
const output = {};
|
|
351
|
+
assignIfDefined(output, "httpMethod", member(HTTP_METHODS, input.httpMethod));
|
|
352
|
+
assignIfDefined(output, "httpStatusCode", httpStatusCode(input.httpStatusCode));
|
|
353
|
+
assignIfDefined(output, "provider", otherEnum(PROVIDERS, input.provider));
|
|
354
|
+
assignIfDefined(output, "route", otherEnum(ROUTES, input.route));
|
|
355
|
+
assignIfDefined(output, "modelFamily", otherEnum(MODEL_FAMILIES, input.modelFamily));
|
|
356
|
+
assignIfDefined(output, "requestSource", otherEnum(REQUEST_SOURCES, input.requestSource));
|
|
357
|
+
assignIfDefined(output, "runtimeMode", member(RUNTIME_MODES, input.runtimeMode));
|
|
358
|
+
assignIfDefined(output, "streaming", typeof input.streaming === "boolean" ? input.streaming : undefined);
|
|
359
|
+
assignIfDefined(output, "streamOutcome", otherEnum(STREAM_OUTCOMES, input.streamOutcome));
|
|
360
|
+
assignIfDefined(output, "outcome", otherEnum(OUTCOMES, input.outcome));
|
|
361
|
+
assignIfDefined(output, "attempt", boundedInteger(input.attempt, MAX_ATTEMPT));
|
|
362
|
+
assignIfDefined(output, "accountPoolSize", boundedInteger(input.accountPoolSize, MAX_ACCOUNT_POOL_SIZE));
|
|
363
|
+
assignIfDefined(output, "concurrency", boundedInteger(input.concurrency, MAX_CONCURRENCY));
|
|
364
|
+
assignIfDefined(output, "inputTokens", boundedInteger(input.inputTokens, MAX_TOKEN_COUNT));
|
|
365
|
+
assignIfDefined(output, "outputTokens", boundedInteger(input.outputTokens, MAX_TOKEN_COUNT));
|
|
366
|
+
assignIfDefined(output, "operationDurationMs", boundedNumber(input.operationDurationMs, MAX_DURATION_MS));
|
|
367
|
+
return output;
|
|
368
|
+
}
|
|
369
|
+
export function reconstructSpan(input) {
|
|
370
|
+
if (!isRecord(input))
|
|
371
|
+
return undefined;
|
|
372
|
+
const scope = member(INSTRUMENTATION_SCOPES, input.scope);
|
|
373
|
+
const operation = member(OPERATIONS, input.operation);
|
|
374
|
+
const traceId = hexId(input.traceId, 32);
|
|
375
|
+
const spanId = hexId(input.spanId, 16);
|
|
376
|
+
const kind = member(SPAN_KINDS, input.kind);
|
|
377
|
+
const startTimeMs = boundedNumber(input.startTimeMs, MAX_TIMESTAMP_MS);
|
|
378
|
+
const durationMs = boundedNumber(input.durationMs, MAX_DURATION_MS);
|
|
379
|
+
const statusCode = member(SPAN_STATUS_CODES, input.statusCode);
|
|
380
|
+
if (!scope || !operation || !traceId || !spanId || !kind || startTimeMs === undefined
|
|
381
|
+
|| durationMs === undefined || !statusCode) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const output = {
|
|
385
|
+
scope,
|
|
386
|
+
name: operation,
|
|
387
|
+
traceId,
|
|
388
|
+
spanId,
|
|
389
|
+
kind,
|
|
390
|
+
startTimeMs,
|
|
391
|
+
durationMs,
|
|
392
|
+
statusCode,
|
|
393
|
+
attributes: reconstructSpanAttributes(input.attributes),
|
|
394
|
+
};
|
|
395
|
+
assignIfDefined(output, "parentSpanId", hexId(input.parentSpanId, 16));
|
|
396
|
+
return output;
|
|
397
|
+
}
|
|
398
|
+
function setupAttributes(input, trustedDiagnosticId) {
|
|
399
|
+
if (!isRecord(input))
|
|
400
|
+
return undefined;
|
|
401
|
+
const provider = member(PROVIDERS, input.provider);
|
|
402
|
+
if (provider !== "anthropic" && provider !== "openai")
|
|
403
|
+
return undefined;
|
|
404
|
+
const method = setupMethodForProvider(provider, input.method);
|
|
405
|
+
const stage = member(SETUP_STAGES, input.stage);
|
|
406
|
+
if (!method || !stage)
|
|
407
|
+
return undefined;
|
|
408
|
+
const output = { provider, method, stage, diagnosticId: trustedDiagnosticId };
|
|
409
|
+
assignIfDefined(output, "reason", otherEnum(SETUP_REASONS, input.reason));
|
|
410
|
+
assignIfDefined(output, "outcome", otherEnum(OUTCOMES, input.outcome));
|
|
411
|
+
assignIfDefined(output, "httpStatusCode", httpStatusCode(input.httpStatusCode));
|
|
412
|
+
assignIfDefined(output, "durationBucket", member(DURATION_BUCKETS, input.durationBucket));
|
|
413
|
+
assignIfDefined(output, "serviceVersion", version(input.serviceVersion));
|
|
414
|
+
assignIfDefined(output, "osFamily", otherEnum(OS_FAMILIES, input.osFamily));
|
|
415
|
+
assignIfDefined(output, "runtimeMode", member(RUNTIME_MODES, input.runtimeMode));
|
|
416
|
+
return output;
|
|
417
|
+
}
|
|
418
|
+
function runtimeFailureAttributes(input, trustedDiagnosticId) {
|
|
419
|
+
if (!isRecord(input))
|
|
420
|
+
return undefined;
|
|
421
|
+
const operation = member(OPERATIONS, input.operation);
|
|
422
|
+
const reason = otherEnum(SETUP_REASONS, input.reason);
|
|
423
|
+
if (!operation || !reason)
|
|
424
|
+
return undefined;
|
|
425
|
+
const output = { operation, reason };
|
|
426
|
+
assignIfDefined(output, "provider", otherEnum(PROVIDERS, input.provider));
|
|
427
|
+
assignIfDefined(output, "outcome", otherEnum(OUTCOMES, input.outcome));
|
|
428
|
+
assignIfDefined(output, "httpStatusCode", httpStatusCode(input.httpStatusCode));
|
|
429
|
+
assignIfDefined(output, "attempt", boundedInteger(input.attempt, MAX_ATTEMPT));
|
|
430
|
+
assignIfDefined(output, "accountPoolSize", boundedInteger(input.accountPoolSize, MAX_ACCOUNT_POOL_SIZE));
|
|
431
|
+
assignIfDefined(output, "concurrency", boundedInteger(input.concurrency, MAX_CONCURRENCY));
|
|
432
|
+
assignIfDefined(output, "operationDurationMs", boundedNumber(input.operationDurationMs, MAX_DURATION_MS));
|
|
433
|
+
assignIfDefined(output, "serviceVersion", version(input.serviceVersion));
|
|
434
|
+
assignIfDefined(output, "osFamily", otherEnum(OS_FAMILIES, input.osFamily));
|
|
435
|
+
assignIfDefined(output, "runtimeMode", member(RUNTIME_MODES, input.runtimeMode));
|
|
436
|
+
assignIfDefined(output, "diagnosticId", trustedDiagnosticId);
|
|
437
|
+
return output;
|
|
438
|
+
}
|
|
439
|
+
export function reconstructLog(input, identity) {
|
|
440
|
+
if (!isRecord(input))
|
|
441
|
+
return undefined;
|
|
442
|
+
const scope = member(INSTRUMENTATION_SCOPES, input.scope);
|
|
443
|
+
const body = member(LOG_EVENT_CODES, input.body);
|
|
444
|
+
const severity = member(SEVERITIES, input.severity);
|
|
445
|
+
const timestampMs = boundedNumber(input.timestampMs, MAX_TIMESTAMP_MS);
|
|
446
|
+
const trustedInstallationId = installationId(identity);
|
|
447
|
+
if (!scope || !body || !severity || timestampMs === undefined || !trustedInstallationId)
|
|
448
|
+
return undefined;
|
|
449
|
+
const trustedDiagnosticId = diagnosticId(identity, trustedInstallationId);
|
|
450
|
+
if (identity.diagnosticId !== undefined && !trustedDiagnosticId)
|
|
451
|
+
return undefined;
|
|
452
|
+
if (body === "account.setup.diagnostic" && !trustedDiagnosticId)
|
|
453
|
+
return undefined;
|
|
454
|
+
const attributes = body === "account.setup.diagnostic"
|
|
455
|
+
? setupAttributes(input.attributes, trustedDiagnosticId)
|
|
456
|
+
: runtimeFailureAttributes(input.attributes, trustedDiagnosticId);
|
|
457
|
+
if (!attributes)
|
|
458
|
+
return undefined;
|
|
459
|
+
const context = {};
|
|
460
|
+
assignIfDefined(context, "traceId", hexId(input.traceId, 32));
|
|
461
|
+
assignIfDefined(context, "spanId", hexId(input.spanId, 16));
|
|
462
|
+
return body === "account.setup.diagnostic"
|
|
463
|
+
? { scope, body, severity, timestampMs, ...context, attributes: attributes }
|
|
464
|
+
: { scope, body, severity, timestampMs, ...context, attributes: attributes };
|
|
465
|
+
}
|
|
466
|
+
function runtimeEventProperties(input) {
|
|
467
|
+
if (!isRecord(input))
|
|
468
|
+
return undefined;
|
|
469
|
+
const output = {};
|
|
470
|
+
assignIfDefined(output, "serviceVersion", version(input.serviceVersion));
|
|
471
|
+
assignIfDefined(output, "osFamily", otherEnum(OS_FAMILIES, input.osFamily));
|
|
472
|
+
assignIfDefined(output, "runtimeMode", member(RUNTIME_MODES, input.runtimeMode));
|
|
473
|
+
assignIfDefined(output, "accountPoolSize", boundedInteger(input.accountPoolSize, MAX_ACCOUNT_POOL_SIZE));
|
|
474
|
+
return output;
|
|
475
|
+
}
|
|
476
|
+
function setupEventProperties(input, trustedDiagnosticId) {
|
|
477
|
+
const attributes = setupAttributes(input, trustedDiagnosticId);
|
|
478
|
+
if (!attributes)
|
|
479
|
+
return undefined;
|
|
480
|
+
const output = {
|
|
481
|
+
provider: attributes.provider,
|
|
482
|
+
method: attributes.method,
|
|
483
|
+
stage: attributes.stage,
|
|
484
|
+
diagnosticId: trustedDiagnosticId,
|
|
485
|
+
};
|
|
486
|
+
assignIfDefined(output, "reason", attributes.reason);
|
|
487
|
+
assignIfDefined(output, "durationBucket", attributes.durationBucket);
|
|
488
|
+
assignIfDefined(output, "serviceVersion", attributes.serviceVersion);
|
|
489
|
+
assignIfDefined(output, "osFamily", attributes.osFamily);
|
|
490
|
+
assignIfDefined(output, "runtimeMode", attributes.runtimeMode);
|
|
491
|
+
return output;
|
|
492
|
+
}
|
|
493
|
+
export function reconstructAnalyticsEvent(input, identity) {
|
|
494
|
+
if (!isRecord(input))
|
|
495
|
+
return undefined;
|
|
496
|
+
const event = member(ANALYTICS_EVENT_NAMES, input.event);
|
|
497
|
+
const trustedInstallationId = installationId(identity);
|
|
498
|
+
if (!event || !trustedInstallationId)
|
|
499
|
+
return undefined;
|
|
500
|
+
const isSetupEvent = event.startsWith("account_setup.");
|
|
501
|
+
const trustedDiagnosticId = diagnosticId(identity, trustedInstallationId);
|
|
502
|
+
if (identity.diagnosticId !== undefined && !trustedDiagnosticId)
|
|
503
|
+
return undefined;
|
|
504
|
+
if (isSetupEvent && !trustedDiagnosticId)
|
|
505
|
+
return undefined;
|
|
506
|
+
const properties = isSetupEvent
|
|
507
|
+
? setupEventProperties(input.properties, trustedDiagnosticId)
|
|
508
|
+
: runtimeEventProperties(input.properties);
|
|
509
|
+
if (!properties)
|
|
510
|
+
return undefined;
|
|
511
|
+
const output = { event, properties, installationId: trustedInstallationId };
|
|
512
|
+
assignIfDefined(output, "diagnosticId", trustedDiagnosticId);
|
|
513
|
+
return output;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Rebuild a sanitized exception that was persisted for crash-safe delivery.
|
|
517
|
+
* The record is re-validated against the same closed schema as a live
|
|
518
|
+
* sanitization; anything outside it (including an edited file) is dropped.
|
|
519
|
+
*/
|
|
520
|
+
export function rebuildSanitizedException(input) {
|
|
521
|
+
if (!isRecord(input))
|
|
522
|
+
return undefined;
|
|
523
|
+
const category = input.category === "setup" || input.category === "runtime" ? input.category : undefined;
|
|
524
|
+
const reason = member(SETUP_REASONS, input.reason);
|
|
525
|
+
const errorKind = member(ERROR_KINDS, input.errorKind);
|
|
526
|
+
const fingerprint = typeof input.fingerprint === "string" && /^[0-9a-f]{64}$/.test(input.fingerprint)
|
|
527
|
+
? input.fingerprint
|
|
528
|
+
: undefined;
|
|
529
|
+
const diagnosticId = uuid(input.diagnosticId);
|
|
530
|
+
if (!category || !reason || !errorKind || !fingerprint || !diagnosticId || !Array.isArray(input.frames)) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
const frames = [];
|
|
534
|
+
for (const frame of input.frames.slice(0, MAX_STACK_FRAMES)) {
|
|
535
|
+
if (!isRecord(frame) || typeof frame.path !== "string")
|
|
536
|
+
return undefined;
|
|
537
|
+
const path = frame.path;
|
|
538
|
+
if (!/^(dist|node_modules)\//.test(path) || path.length > MAX_STACK_FRAME_PATH_LENGTH || !safePathSegments(path)) {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
const line = frame.line === undefined ? undefined : boundedInteger(frame.line, Number.MAX_SAFE_INTEGER, 1);
|
|
542
|
+
const column = frame.column === undefined ? undefined : boundedInteger(frame.column, Number.MAX_SAFE_INTEGER, 1);
|
|
543
|
+
if ((frame.line !== undefined && line === undefined) || (frame.column !== undefined && column === undefined)) {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
const safeFrame = { path: path };
|
|
547
|
+
assignIfDefined(safeFrame, "line", line);
|
|
548
|
+
assignIfDefined(safeFrame, "column", column);
|
|
549
|
+
frames.push(safeFrame);
|
|
550
|
+
}
|
|
551
|
+
const contract = {
|
|
552
|
+
error: sanitizedError(reason, frames),
|
|
553
|
+
category,
|
|
554
|
+
reason,
|
|
555
|
+
errorKind,
|
|
556
|
+
frames,
|
|
557
|
+
fingerprint,
|
|
558
|
+
diagnosticId,
|
|
559
|
+
};
|
|
560
|
+
assignIfDefined(contract, "systemErrorCode", member(SYSTEM_ERROR_CODES, input.systemErrorCode));
|
|
561
|
+
assignIfDefined(contract, "httpStatusCode", httpStatusCode(input.httpStatusCode));
|
|
562
|
+
assignIfDefined(contract, "operation", member(OPERATIONS, input.operation));
|
|
563
|
+
assignIfDefined(contract, "provider", member(PROVIDERS, input.provider));
|
|
564
|
+
assignIfDefined(contract, "setupStage", member(SETUP_STAGES, input.setupStage));
|
|
565
|
+
assignIfDefined(contract, "runtimeMode", member(RUNTIME_MODES, input.runtimeMode));
|
|
566
|
+
return contract;
|
|
567
|
+
}
|