@timo972/cc-router 0.12.2-rc.0 → 0.12.2-rc.1
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/telemetry/posthog-client.js +16 -9
- package/dist/telemetry/privacy.js +40 -20
- package/docs/telemetry.md +7 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PostHog } from "posthog-node";
|
|
2
2
|
import { ERROR_KINDS, MAX_STACK_FRAMES, MAX_STACK_FRAME_PATH_LENGTH, OPERATIONS, POSTHOG_FLUSH_AT, POSTHOG_FLUSH_INTERVAL_MS, POSTHOG_HOST, POSTHOG_MAX_QUEUE_SIZE, POSTHOG_PROJECT_TOKEN, POSTHOG_REQUEST_TIMEOUT_MS, PROVIDERS, RUNTIME_MODES, SETUP_REASONS, SETUP_STAGES, SYSTEM_ERROR_CODES, } from "./contracts.js";
|
|
3
|
-
import { reconstructAnalyticsEvent } from "./privacy.js";
|
|
3
|
+
import { exceptionName, frameFunction, reconstructAnalyticsEvent } from "./privacy.js";
|
|
4
4
|
/** Stamped on every capture and re-checked in before_send against live consent. */
|
|
5
5
|
const CONSENT_GENERATION_PROPERTY = "cc_router.consent_generation";
|
|
6
6
|
const QUEUE_KEYS = ["queue", "ai_queue", "logs_queue"];
|
|
@@ -71,17 +71,24 @@ function reconstructExceptionFrames(input) {
|
|
|
71
71
|
const column = positiveInteger(own(candidate, "colno"));
|
|
72
72
|
if (!filename || !line || !column)
|
|
73
73
|
return undefined;
|
|
74
|
-
frames.push({
|
|
74
|
+
frames.push({
|
|
75
|
+
platform: "node:javascript",
|
|
76
|
+
function: frameFunction(own(candidate, "function")),
|
|
77
|
+
filename,
|
|
78
|
+
lineno: line,
|
|
79
|
+
colno: column,
|
|
80
|
+
});
|
|
75
81
|
}
|
|
76
82
|
return frames;
|
|
77
83
|
}
|
|
78
|
-
function reconstructExceptionList(input
|
|
84
|
+
function reconstructExceptionList(input) {
|
|
79
85
|
if (!Array.isArray(input) || input.length !== 1 || !isRecord(input[0]))
|
|
80
86
|
return undefined;
|
|
81
87
|
const exception = input[0];
|
|
82
88
|
const mechanism = own(exception, "mechanism");
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
const name = exceptionName(own(exception, "type"));
|
|
90
|
+
if (own(exception, "type") !== name
|
|
91
|
+
|| own(exception, "value") !== ""
|
|
85
92
|
|| !isRecord(mechanism)
|
|
86
93
|
|| own(mechanism, "type") !== "generic"
|
|
87
94
|
|| own(mechanism, "handled") !== true
|
|
@@ -90,7 +97,7 @@ function reconstructExceptionList(input, reason) {
|
|
|
90
97
|
}
|
|
91
98
|
const stacktrace = own(exception, "stacktrace");
|
|
92
99
|
if (stacktrace === undefined) {
|
|
93
|
-
return [{ type:
|
|
100
|
+
return [{ type: name, value: "", mechanism: { type: "generic", handled: true, synthetic: false } }];
|
|
94
101
|
}
|
|
95
102
|
if (!isRecord(stacktrace) || own(stacktrace, "type") !== "raw")
|
|
96
103
|
return undefined;
|
|
@@ -98,8 +105,8 @@ function reconstructExceptionList(input, reason) {
|
|
|
98
105
|
if (!frames)
|
|
99
106
|
return undefined;
|
|
100
107
|
return [{
|
|
101
|
-
type:
|
|
102
|
-
value:
|
|
108
|
+
type: name,
|
|
109
|
+
value: "",
|
|
103
110
|
mechanism: { type: "generic", handled: true, synthetic: false },
|
|
104
111
|
stacktrace: { type: "raw", frames },
|
|
105
112
|
}];
|
|
@@ -124,7 +131,7 @@ function reconstructExceptionEvent(event, installationId) {
|
|
|
124
131
|
|| own(properties, "$exception_level") !== "error") {
|
|
125
132
|
return null;
|
|
126
133
|
}
|
|
127
|
-
const exceptionList = reconstructExceptionList(own(properties, "$exception_list")
|
|
134
|
+
const exceptionList = reconstructExceptionList(own(properties, "$exception_list"));
|
|
128
135
|
if (!exceptionList)
|
|
129
136
|
return null;
|
|
130
137
|
const systemErrorCode = optionalMember(SYSTEM_ERROR_CODES, own(properties, "systemErrorCode"));
|
|
@@ -219,45 +219,58 @@ function stackHeaderName(kind) {
|
|
|
219
219
|
default: return "Error";
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
|
|
222
|
+
/** Keep code symbols, not arbitrary messages or paths supplied as names. */
|
|
223
|
+
export function exceptionName(value) {
|
|
224
|
+
return typeof value === "string" && /^[A-Za-z_$][\w.$-]{0,127}$/.test(value) ? value : "Error";
|
|
225
|
+
}
|
|
226
|
+
export function frameFunction(value) {
|
|
227
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256
|
|
228
|
+
&& /^[\w$.[\]<> ():-]+$/.test(value) ? value : "<anonymous>";
|
|
229
|
+
}
|
|
230
|
+
function normalizedFrames(input) {
|
|
223
231
|
const stack = input.stack;
|
|
224
232
|
if (typeof stack !== "string")
|
|
225
233
|
return [];
|
|
226
|
-
//
|
|
227
|
-
// verbatim, so a multiline message can never inject a frame.
|
|
234
|
+
// Remove the complete raw header so multiline messages cannot inject frames.
|
|
228
235
|
const rawMessage = own(input, "message");
|
|
229
236
|
if (rawMessage !== undefined && typeof rawMessage !== "string")
|
|
230
237
|
return [];
|
|
231
|
-
const
|
|
238
|
+
const rawName = input.name;
|
|
239
|
+
if (typeof rawName !== "string" || /[\r\n]/.test(rawName))
|
|
240
|
+
return [];
|
|
241
|
+
const header = rawName ? `${rawName}${rawMessage ? `: ${rawMessage}` : ""}` : (rawMessage ?? "");
|
|
232
242
|
if (!stack.startsWith(`${header}\n`))
|
|
233
243
|
return [];
|
|
234
244
|
const frames = [];
|
|
235
245
|
for (const line of stack.slice(header.length + 1).split("\n")) {
|
|
236
246
|
if (frames.length >= MAX_STACK_FRAMES)
|
|
237
247
|
break;
|
|
238
|
-
const
|
|
239
|
-
|
|
248
|
+
const named = line.match(/^\s*at (.+) \((.+):(\d+):(\d+)\)\s*$/);
|
|
249
|
+
const anonymous = named ? null : line.match(/^\s*at (.+):(\d+):(\d+)\s*$/);
|
|
250
|
+
if (!named && !anonymous)
|
|
240
251
|
continue;
|
|
241
|
-
const path = normalizedFramePath(
|
|
242
|
-
const frameLine = boundedInteger(Number(
|
|
243
|
-
const column = boundedInteger(Number(
|
|
252
|
+
const path = normalizedFramePath(named ? named[2] : anonymous[1]);
|
|
253
|
+
const frameLine = boundedInteger(Number(named ? named[3] : anonymous[2]), Number.MAX_SAFE_INTEGER, 1);
|
|
254
|
+
const column = boundedInteger(Number(named ? named[4] : anonymous[3]), Number.MAX_SAFE_INTEGER, 1);
|
|
244
255
|
if (!path || frameLine === undefined || column === undefined)
|
|
245
256
|
continue;
|
|
246
|
-
frames.push({ path, line: frameLine, column });
|
|
257
|
+
frames.push({ path, line: frameLine, column, function: frameFunction(named?.[1]) });
|
|
247
258
|
}
|
|
248
259
|
return frames;
|
|
249
260
|
}
|
|
250
|
-
function sanitizedError(
|
|
251
|
-
const error = new Error(
|
|
261
|
+
function sanitizedError(name, frames) {
|
|
262
|
+
const error = new Error();
|
|
263
|
+
Object.defineProperty(error, "name", { value: name, configurable: true, writable: true });
|
|
252
264
|
error.stack = [
|
|
253
|
-
|
|
254
|
-
...frames.map(frame => ` at ${frame.path}:${frame.line}:${frame.column}`),
|
|
265
|
+
name,
|
|
266
|
+
...frames.map(frame => ` at ${frameFunction(frame.function)} (${frame.path}:${frame.line}:${frame.column})`),
|
|
255
267
|
].join("\n");
|
|
256
268
|
return error;
|
|
257
269
|
}
|
|
258
|
-
function fingerprint(kind, context, systemErrorCode, status, frames) {
|
|
270
|
+
function fingerprint(kind, name, context, systemErrorCode, status, frames) {
|
|
259
271
|
return createHash("sha256").update(JSON.stringify({
|
|
260
272
|
errorKind: kind,
|
|
273
|
+
errorName: name,
|
|
261
274
|
category: context.category,
|
|
262
275
|
reason: context.reason,
|
|
263
276
|
operation: context.operation,
|
|
@@ -299,7 +312,8 @@ export function sanitizeException(input, candidateContext, identity) {
|
|
|
299
312
|
return undefined;
|
|
300
313
|
const isError = input instanceof Error;
|
|
301
314
|
const kind = isError ? errorKind(input) : "unexpected_error";
|
|
302
|
-
const
|
|
315
|
+
const name = isError ? exceptionName(input.name) : "Error";
|
|
316
|
+
const frames = isError ? normalizedFrames(input) : [];
|
|
303
317
|
const code = isError
|
|
304
318
|
? member(SYSTEM_ERROR_CODES, own(input, "code"))
|
|
305
319
|
: undefined;
|
|
@@ -307,11 +321,12 @@ export function sanitizeException(input, candidateContext, identity) {
|
|
|
307
321
|
? httpStatusCode(own(input, "statusCode")) ?? httpStatusCode(own(input, "status"))
|
|
308
322
|
: undefined;
|
|
309
323
|
const output = {
|
|
310
|
-
error: sanitizedError(
|
|
324
|
+
error: sanitizedError(name, frames),
|
|
325
|
+
errorName: name,
|
|
311
326
|
...context,
|
|
312
327
|
errorKind: kind,
|
|
313
328
|
frames,
|
|
314
|
-
fingerprint: fingerprint(kind, context, code, status, frames),
|
|
329
|
+
fingerprint: fingerprint(kind, name, context, code, status, frames),
|
|
315
330
|
diagnosticId: trustedDiagnosticId,
|
|
316
331
|
};
|
|
317
332
|
assignIfDefined(output, "systemErrorCode", code);
|
|
@@ -543,13 +558,18 @@ export function rebuildSanitizedException(input) {
|
|
|
543
558
|
if ((frame.line !== undefined && line === undefined) || (frame.column !== undefined && column === undefined)) {
|
|
544
559
|
return undefined;
|
|
545
560
|
}
|
|
546
|
-
const safeFrame = {
|
|
561
|
+
const safeFrame = {
|
|
562
|
+
path: path,
|
|
563
|
+
function: frameFunction(frame.function),
|
|
564
|
+
};
|
|
547
565
|
assignIfDefined(safeFrame, "line", line);
|
|
548
566
|
assignIfDefined(safeFrame, "column", column);
|
|
549
567
|
frames.push(safeFrame);
|
|
550
568
|
}
|
|
569
|
+
const name = exceptionName(input.errorName ?? stackHeaderName(errorKind));
|
|
551
570
|
const contract = {
|
|
552
|
-
error: sanitizedError(
|
|
571
|
+
error: sanitizedError(name, frames),
|
|
572
|
+
errorName: name,
|
|
553
573
|
category,
|
|
554
574
|
reason,
|
|
555
575
|
errorKind,
|
package/docs/telemetry.md
CHANGED
|
@@ -118,15 +118,18 @@ Providers `anthropic` (`macos_keychain`, `claude_credentials_file`,
|
|
|
118
118
|
### Sanitized exceptions
|
|
119
119
|
|
|
120
120
|
An unexpected failure is rebuilt as a *new* `Error` containing only: category
|
|
121
|
-
(`setup`/`runtime`), one safe reason, error
|
|
121
|
+
(`setup`/`runtime`), one safe reason, the original error name (including custom
|
|
122
|
+
error names), error kind (`error`, `type_error`,
|
|
122
123
|
`range_error`, `reference_error`, `syntax_error`, `uri_error`, `eval_error`,
|
|
123
124
|
`aggregate_error`, `unexpected_error`), optional system code (`EAI_AGAIN`,
|
|
124
125
|
`ECONNREFUSED`, `ECONNRESET`, `ENETUNREACH`, `ENOTFOUND`, `EPIPE`,
|
|
125
126
|
`ETIMEDOUT`), optional HTTP status, operation, provider, setup stage, runtime
|
|
126
127
|
mode, stack frames normalized to `dist/...` or `node_modules/<package>/...`
|
|
127
|
-
(max 20 frames, 256 chars
|
|
128
|
-
fresh random diagnostic ID.
|
|
129
|
-
|
|
128
|
+
with function names (max 20 frames, 256 chars per path and function), a
|
|
129
|
+
fingerprint over those safe fields, and a fresh random diagnostic ID. Anonymous frames use `<anonymous>`. Error names are
|
|
130
|
+
bounded code identifiers (max 128 chars); malformed names fall back to `Error`.
|
|
131
|
+
The exception message is empty; the classified reason remains separate metadata.
|
|
132
|
+
The original message, cause chain, custom properties, and unrecognized frames are dropped. The diagnostic ID is printed
|
|
130
133
|
next to the detailed local error so an issue report can reference it.
|
|
131
134
|
|
|
132
135
|
A fatal (uncaught) exception cannot be sent by the crashing process. Its
|
package/package.json
CHANGED