@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.
@@ -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({ platform: "node:javascript", filename, lineno: line, colno: column });
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, reason) {
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
- if (own(exception, "type") !== "Error"
84
- || own(exception, "value") !== reason
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: "Error", value: reason, mechanism: { type: "generic", handled: true, synthetic: false } }];
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: "Error",
102
- value: reason,
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"), reason);
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
- function normalizedFrames(input, kind) {
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
- // The header carries the raw message. Parse frames only after removing it
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 header = `${stackHeaderName(kind)}${rawMessage ? `: ${rawMessage}` : ""}`;
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 match = line.match(/(?:\(|\bat\s+)(.+):(\d+):(\d+)\)?\s*$/);
239
- if (!match)
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(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);
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(reason, frames) {
251
- const error = new Error(reason);
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
- `Error: ${reason}`,
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 frames = isError ? normalizedFrames(input, kind) : [];
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(context.reason, frames),
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 = { path: path };
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(reason, frames),
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 kind (`error`, `type_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 each), a fingerprint over those safe fields, and a
128
- fresh random diagnostic ID. The original message, cause chain, custom
129
- properties, and unrecognized frames are dropped. The diagnostic ID is printed
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timo972/cc-router",
3
- "version": "0.12.2-rc.0",
3
+ "version": "0.12.2-rc.1",
4
4
  "description": "Cache-aware session router for Claude Max OAuth tokens — use multiple Claude Max accounts with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {