@autter/runtime-node 1.4.0 → 1.6.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/dist/index.js CHANGED
@@ -1,3 +1,76 @@
1
+ import {
2
+ CODE_PATTERN,
3
+ DEFAULT_REQUEST_ID_HEADER,
4
+ RuntimeError,
5
+ addBreadcrumb,
6
+ breadcrumbEventAttributes,
7
+ captureExceptionAsLog,
8
+ compileIgnore,
9
+ configureRuntimeLogger,
10
+ consoleSink,
11
+ createRequestState,
12
+ createRuntimeLogger,
13
+ currentOperation,
14
+ defineRuntimeErrors,
15
+ emitSummary,
16
+ errorAttributes,
17
+ errorCodeOf,
18
+ errorInternal,
19
+ fileSink,
20
+ fingerprintEventAttributes,
21
+ fingerprintValues,
22
+ flushRuntimeLogs,
23
+ isError,
24
+ isRuntimeErrorLike,
25
+ local,
26
+ makeRedactor,
27
+ normalizeRoutePath,
28
+ noteOperationError,
29
+ notifyException,
30
+ otlpSink,
31
+ parseUserAgent,
32
+ recordOperationAiUsage,
33
+ redactAttributes,
34
+ redactText,
35
+ reportExplicitFailure,
36
+ requestOutcome,
37
+ resolveRequestId,
38
+ responseStatusOf,
39
+ runInBackground,
40
+ runtimeContext,
41
+ runtimeLogStats,
42
+ runtimeLogger,
43
+ sanitizeBreadcrumbs,
44
+ shutdownRuntimeLogger,
45
+ splitFingerprint,
46
+ toClientError,
47
+ withRuntimeOperation
48
+ } from "./chunk-FBKYOHU2.js";
49
+ import {
50
+ runDoctor
51
+ } from "./chunk-Y4T7SQMO.js";
52
+ import {
53
+ COMPAT_MANIFEST,
54
+ CountingExporter,
55
+ INGESTER_VERSION_HEADER,
56
+ SDK_IDENTITY,
57
+ configureCompatCheck,
58
+ debugLog,
59
+ ensureCompatConfigured,
60
+ evaluateCompat,
61
+ featuresForBrowserEvents,
62
+ fetchIngesterCompat,
63
+ installAutterAutoFlush,
64
+ isDebugEnabled,
65
+ noteCompatFeature,
66
+ noteRelayRejection,
67
+ observeIngesterResponse,
68
+ registerFlushTarget,
69
+ setDebugMode,
70
+ telemetryStats,
71
+ unregisterFlushTargets
72
+ } from "./chunk-OY2HTUWY.js";
73
+
1
74
  // src/relay.ts
2
75
  var IpWindow = class {
3
76
  constructor(limitPerMinute) {
@@ -17,8 +90,8 @@ var IpWindow = class {
17
90
  return entry.count <= this.limitPerMinute;
18
91
  }
19
92
  };
20
- function firstForwardedFor(value2) {
21
- const raw = Array.isArray(value2) ? value2[0] : value2;
93
+ function firstForwardedFor(value) {
94
+ const raw = Array.isArray(value) ? value[0] : value;
22
95
  return raw ? (raw.split(",")[0] ?? "").trim() : "";
23
96
  }
24
97
  var DEFAULT_ENDPOINT = "https://otlp.autter.dev";
@@ -36,7 +109,142 @@ var EVENT_TYPES = /* @__PURE__ */ new Set([
36
109
  "timing"
37
110
  ]);
38
111
  var SEVERITIES = /* @__PURE__ */ new Set(["fatal", "error", "warning", "info"]);
39
- function sanitizeBrowserPayload(raw) {
112
+ var TRACE_ID = /^(?!0{32})[0-9a-f]{32}$/;
113
+ var CONTEXT_MAX_DEPTH = 6;
114
+ var CONTEXT_MAX_NODES = 256;
115
+ var CONTEXT_MAX_STRING = 4e3;
116
+ var CONTEXT_MAX_ARRAY = 100;
117
+ var CONTEXT_MAX_KEYS = 100;
118
+ function byteLength(text) {
119
+ return new TextEncoder().encode(text).length;
120
+ }
121
+ function boundContext(value) {
122
+ let nodes = 0;
123
+ const seen = /* @__PURE__ */ new WeakSet();
124
+ const walk = (v, depth) => {
125
+ if (v === null) return null;
126
+ const t = typeof v;
127
+ if (t === "string") return v.slice(0, CONTEXT_MAX_STRING);
128
+ if (t === "number" || t === "boolean") return v;
129
+ if (t !== "object") return void 0;
130
+ if (depth >= CONTEXT_MAX_DEPTH || nodes >= CONTEXT_MAX_NODES) return void 0;
131
+ const obj = v;
132
+ if (seen.has(obj)) return void 0;
133
+ seen.add(obj);
134
+ let isArr = false;
135
+ try {
136
+ isArr = Array.isArray(v);
137
+ } catch {
138
+ return void 0;
139
+ }
140
+ if (isArr) {
141
+ const arr = v;
142
+ const out2 = [];
143
+ let len = 0;
144
+ try {
145
+ len = arr.length;
146
+ } catch {
147
+ return out2;
148
+ }
149
+ for (let i = 0; i < len && i < CONTEXT_MAX_ARRAY; i++) {
150
+ if (nodes >= CONTEXT_MAX_NODES) break;
151
+ nodes++;
152
+ let el;
153
+ try {
154
+ el = walk(arr[i], depth + 1);
155
+ } catch {
156
+ el = void 0;
157
+ }
158
+ if (el !== void 0) out2.push(el);
159
+ }
160
+ return out2;
161
+ }
162
+ let keys;
163
+ try {
164
+ keys = Object.keys(obj);
165
+ } catch {
166
+ return void 0;
167
+ }
168
+ const out = {};
169
+ for (let i = 0; i < keys.length && i < CONTEXT_MAX_KEYS; i++) {
170
+ if (nodes >= CONTEXT_MAX_NODES) break;
171
+ const key = keys[i];
172
+ if (key === void 0) continue;
173
+ nodes++;
174
+ let child;
175
+ try {
176
+ child = walk(obj[key], depth + 1);
177
+ } catch {
178
+ child = void 0;
179
+ }
180
+ if (child !== void 0) out[key] = child;
181
+ }
182
+ return out;
183
+ };
184
+ let result;
185
+ try {
186
+ result = walk(value, 0);
187
+ } catch {
188
+ result = void 0;
189
+ }
190
+ return result && typeof result === "object" ? result : {};
191
+ }
192
+ async function readBodyBounded(request, maxBody) {
193
+ const body = request.body;
194
+ if (!body) {
195
+ const text = await request.text();
196
+ return byteLength(text) > maxBody ? { tooLarge: true } : { tooLarge: false, text };
197
+ }
198
+ const reader = body.getReader();
199
+ const chunks = [];
200
+ let total = 0;
201
+ for (; ; ) {
202
+ const { done, value } = await reader.read();
203
+ if (done) break;
204
+ if (!value) continue;
205
+ total += value.byteLength;
206
+ if (total > maxBody) {
207
+ void Promise.resolve().then(() => reader.cancel()).catch(() => {
208
+ });
209
+ return { tooLarge: true };
210
+ }
211
+ chunks.push(value);
212
+ }
213
+ const buf = new Uint8Array(total);
214
+ let offset = 0;
215
+ for (const chunk of chunks) {
216
+ buf.set(chunk, offset);
217
+ offset += chunk.byteLength;
218
+ }
219
+ return { tooLarge: false, text: new TextDecoder().decode(buf) };
220
+ }
221
+ function relayContext(raw, redactor) {
222
+ const bounded = boundContext(raw);
223
+ return Array.isArray(bounded) ? {} : { context: redactor(bounded) };
224
+ }
225
+ function relayBreadcrumbs(raw, redactor) {
226
+ const breadcrumbs = sanitizeBreadcrumbs(raw, redactor);
227
+ return breadcrumbs ? { breadcrumbs } : {};
228
+ }
229
+ var DEBUG_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
230
+ function relayDebugIds(raw, redactor) {
231
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
232
+ const debugIds = {};
233
+ let count = 0;
234
+ for (const [url, id] of Object.entries(raw)) {
235
+ if (count >= 50) break;
236
+ if (typeof id !== "string" || !DEBUG_ID_RE.test(id) || url.length > 1e3) continue;
237
+ debugIds[redactor.text(url.split(/[?#]/)[0])] = id.toLowerCase();
238
+ count++;
239
+ }
240
+ return count ? { debugIds } : {};
241
+ }
242
+ function relayFingerprint(raw, redactor) {
243
+ const values = fingerprintValues(raw, redactor.text);
244
+ return values ? { fingerprint: Array.isArray(raw) ? values : values[0] } : {};
245
+ }
246
+ var defaultRelayRedactor = makeRedactor(true);
247
+ function sanitizeBrowserPayload(raw, redactor = defaultRelayRedactor) {
40
248
  if (typeof raw !== "object" || raw === null) return null;
41
249
  const p = raw;
42
250
  if (p.version !== 1) return null;
@@ -53,16 +261,20 @@ function sanitizeBrowserPayload(raw) {
53
261
  type: e.type,
54
262
  timestamp: e.timestamp,
55
263
  ...typeof e.severity === "string" && SEVERITIES.has(e.severity) ? { severity: e.severity } : {},
56
- message: typeof e.message === "string" ? e.message.slice(0, 4e3) : "",
57
- ...typeof e.name === "string" ? { name: e.name.slice(0, 200) } : {},
264
+ message: typeof e.message === "string" ? redactor.text(e.message.slice(0, 4512)).slice(0, 4e3) : "",
265
+ ...typeof e.name === "string" ? { name: redactor.text(e.name.slice(0, 712)).slice(0, 200) } : {},
58
266
  ...typeof e.durationMs === "number" && Number.isFinite(e.durationMs) ? { durationMs: Math.max(0, Math.min(12e4, e.durationMs)) } : {},
59
- ...typeof e.stack === "string" ? { stack: e.stack.slice(0, 32e3) } : {},
267
+ ...typeof e.stack === "string" ? { stack: redactor.text(e.stack.slice(0, 32512)).slice(0, 32e3) } : {},
60
268
  ...typeof e.errorType === "string" ? { errorType: e.errorType.slice(0, 200) } : {},
61
269
  ...typeof e.filename === "string" ? { filename: e.filename.split("?")[0].slice(0, 1e3) } : {},
62
270
  ...typeof e.line === "number" ? { line: e.line } : {},
63
271
  ...typeof e.column === "number" ? { column: e.column } : {},
64
272
  ...typeof e.route === "string" ? { route: e.route.split("?")[0].slice(0, 1e3) } : {},
65
- ...typeof e.context === "object" && e.context !== null ? { context: e.context } : {}
273
+ ...typeof e.context === "object" && e.context !== null ? relayContext(e.context, redactor) : {},
274
+ ...relayBreadcrumbs(e.breadcrumbs, redactor),
275
+ ...relayDebugIds(e.debugIds, redactor),
276
+ ...relayFingerprint(e.fingerprint, redactor),
277
+ ...typeof e.traceId === "string" && TRACE_ID.test(e.traceId) ? { traceId: e.traceId } : {}
66
278
  });
67
279
  }
68
280
  return {
@@ -71,11 +283,16 @@ function sanitizeBrowserPayload(raw) {
71
283
  service: p.service.slice(0, 200),
72
284
  environment: p.environment.slice(0, 100),
73
285
  ...typeof p.release === "string" ? { release: p.release.slice(0, 200) } : {},
286
+ // @autter/runtime-browser version, for the ingester's compat records.
287
+ ...typeof p.sdk === "string" && /^\d{1,6}\.\d{1,6}\.\d{1,6}[0-9A-Za-z.+-]{0,20}$/.test(p.sdk) ? { sdk: p.sdk } : {},
74
288
  events
75
289
  };
76
290
  }
77
291
  function forward(payload, opts) {
78
- const url = `${(opts.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "")}/v1/browser`;
292
+ const endpoint = (opts.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
293
+ const url = `${endpoint}/v1/browser`;
294
+ const compat = opts.compatCheck !== false;
295
+ if (compat) ensureCompatConfigured(endpoint);
79
296
  void fetch(url, {
80
297
  method: "POST",
81
298
  headers: {
@@ -84,6 +301,17 @@ function forward(payload, opts) {
84
301
  },
85
302
  body: JSON.stringify(payload),
86
303
  signal: AbortSignal.timeout(1e4)
304
+ }).then((response) => {
305
+ if (!compat) return;
306
+ const types = new Set(
307
+ (payload.events ?? []).map((e) => String(e.type))
308
+ );
309
+ observeIngesterResponse({ status: response.status, headers: response.headers });
310
+ for (const feature of featuresForBrowserEvents(types)) noteCompatFeature(feature);
311
+ if ((payload.events ?? []).some((e) => e.traceId)) {
312
+ noteCompatFeature("browser_trace_propagation");
313
+ }
314
+ noteRelayRejection(types, response.status, response.headers);
87
315
  }).catch((err) => {
88
316
  (opts.onError ?? ((e) => console.warn("autter relay forward failed:", e)))(
89
317
  err
@@ -92,6 +320,7 @@ function forward(payload, opts) {
92
320
  }
93
321
  function createBrowserRelayFetchHandler(opts) {
94
322
  const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY;
323
+ const redactor = opts.redact === void 0 || opts.redact === true ? defaultRelayRedactor : makeRedactor(opts.redact);
95
324
  const limiter = opts.perIpRateLimit === false ? null : new IpWindow(opts.perIpRateLimit ?? 120);
96
325
  return async (request) => {
97
326
  if (request.method !== "POST") {
@@ -105,21 +334,28 @@ function createBrowserRelayFetchHandler(opts) {
105
334
  });
106
335
  }
107
336
  }
108
- const text = await request.text();
109
- if (text.length > maxBody) {
337
+ let bounded;
338
+ try {
339
+ bounded = await readBodyBounded(request, maxBody);
340
+ } catch {
341
+ return new Response(JSON.stringify({ error: "invalid json" }), {
342
+ status: 400
343
+ });
344
+ }
345
+ if (bounded.tooLarge) {
110
346
  return new Response(JSON.stringify({ error: "payload too large" }), {
111
347
  status: 413
112
348
  });
113
349
  }
114
350
  let raw;
115
351
  try {
116
- raw = JSON.parse(text);
352
+ raw = JSON.parse(bounded.text);
117
353
  } catch {
118
354
  return new Response(JSON.stringify({ error: "invalid json" }), {
119
355
  status: 400
120
356
  });
121
357
  }
122
- const payload = sanitizeBrowserPayload(raw);
358
+ const payload = sanitizeBrowserPayload(raw, redactor);
123
359
  if (!payload) {
124
360
  return new Response(JSON.stringify({ error: "invalid payload" }), {
125
361
  status: 400
@@ -131,6 +367,7 @@ function createBrowserRelayFetchHandler(opts) {
131
367
  }
132
368
  function createBrowserRelayHandler(opts) {
133
369
  const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY;
370
+ const redactor = opts.redact === void 0 || opts.redact === true ? defaultRelayRedactor : makeRedactor(opts.redact);
134
371
  const limiter = opts.perIpRateLimit === false ? null : new IpWindow(opts.perIpRateLimit ?? 120);
135
372
  function respond(res, status, body) {
136
373
  res.statusCode = status;
@@ -142,7 +379,7 @@ function createBrowserRelayHandler(opts) {
142
379
  }
143
380
  }
144
381
  function handleParsed(raw, res) {
145
- const payload = sanitizeBrowserPayload(raw);
382
+ const payload = sanitizeBrowserPayload(raw, redactor);
146
383
  if (!payload) {
147
384
  respond(res, 400, { error: "invalid payload" });
148
385
  return;
@@ -204,22 +441,22 @@ function createBrowserRelayHandler(opts) {
204
441
 
205
442
  // src/server.ts
206
443
  import { ServerResponse } from "http";
207
- import { randomUUID as randomUUID2 } from "crypto";
444
+ import { randomUUID } from "crypto";
208
445
  import {
209
- context,
446
+ context as context2,
210
447
  trace as trace2,
211
448
  SpanKind,
212
449
  SpanStatusCode,
213
450
  TraceFlags
214
451
  } from "@opentelemetry/api";
215
- import { getRPCMetadata, RPCType } from "@opentelemetry/core";
452
+ import { getRPCMetadata as getRPCMetadata2, RPCType as RPCType2 } from "@opentelemetry/core";
216
453
  import {
217
454
  AggregationTemporalityPreference,
218
455
  OTLPMetricExporter
219
456
  } from "@opentelemetry/exporter-metrics-otlp-http";
220
457
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
221
458
  import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
222
- import { Resource } from "@opentelemetry/resources";
459
+ import { Resource as Resource2 } from "@opentelemetry/resources";
223
460
  import { ExplicitBucketHistogramAggregation, PeriodicExportingMetricReader, View } from "@opentelemetry/sdk-metrics";
224
461
  import { NodeSDK } from "@opentelemetry/sdk-node";
225
462
  import {
@@ -235,50 +472,83 @@ import {
235
472
  ATTR_SERVICE_VERSION
236
473
  } from "@opentelemetry/semantic-conventions";
237
474
 
238
- // src/lifecycle.ts
475
+ // src/redact-exporter.ts
239
476
  import { ExportResultCode } from "@opentelemetry/core";
240
- var debugEnabled = process.env.AUTTER_DEBUG === "1" || process.env.AUTTER_DEBUG === "true";
241
- function setDebugMode(enabled) {
242
- debugEnabled = enabled;
243
- }
244
- function isDebugEnabled() {
245
- return debugEnabled;
246
- }
247
- function debugLog(...args) {
248
- if (debugEnabled) {
249
- console.error("[autter]", ...args);
250
- }
477
+ import { Resource } from "@opentelemetry/resources";
478
+ import {
479
+ AggregationTemporality
480
+ } from "@opentelemetry/sdk-metrics";
481
+ var scrubbedResources = /* @__PURE__ */ new WeakMap();
482
+ function redactResource(resource, redactor) {
483
+ const cached = scrubbedResources.get(resource);
484
+ if (cached) return cached;
485
+ const scrubbed = new Resource(redactor.exported(resource.attributes));
486
+ scrubbedResources.set(resource, scrubbed);
487
+ return scrubbed;
488
+ }
489
+ function redactSpan(span, redactor) {
490
+ const status = span.status.message ? { ...span.status, message: redactor.text(span.status.message) } : span.status;
491
+ return {
492
+ name: redactor.text(span.name),
493
+ kind: span.kind,
494
+ spanContext: () => span.spanContext(),
495
+ parentSpanId: span.parentSpanId,
496
+ startTime: span.startTime,
497
+ endTime: span.endTime,
498
+ status,
499
+ attributes: redactor.exported(span.attributes),
500
+ links: span.links.map(
501
+ (link) => link.attributes ? { ...link, attributes: redactor.exported(link.attributes) } : link
502
+ ),
503
+ events: span.events.map((event) => ({
504
+ ...event,
505
+ name: redactor.text(event.name),
506
+ ...event.attributes ? { attributes: redactor.exported(event.attributes) } : {}
507
+ })),
508
+ duration: span.duration,
509
+ ended: span.ended,
510
+ resource: redactResource(span.resource, redactor),
511
+ instrumentationLibrary: span.instrumentationLibrary,
512
+ droppedAttributesCount: span.droppedAttributesCount,
513
+ droppedEventsCount: span.droppedEventsCount,
514
+ droppedLinksCount: span.droppedLinksCount
515
+ };
251
516
  }
252
- var TelemetryStats = class {
253
- /** Captures queued via our APIs and not yet seen leaving an exporter. */
254
- pendingCaptures = 0;
255
- markCaptured(count = 1) {
256
- this.pendingCaptures += count;
257
- }
258
- /** An exporter batch left the process successfully. */
259
- onExported(spanCount) {
260
- this.pendingCaptures = Math.max(0, this.pendingCaptures - spanCount);
261
- if (debugEnabled && spanCount > 0) {
262
- debugLog(`exported ${spanCount} span${spanCount === 1 ? "" : "s"}`);
263
- }
264
- }
265
- markAllFlushed() {
266
- this.pendingCaptures = 0;
267
- }
268
- };
269
- var telemetryStats = new TelemetryStats();
270
- var CountingExporter = class {
271
- constructor(inner) {
517
+ var RedactingSpanExporter = class {
518
+ constructor(inner, redactor) {
272
519
  this.inner = inner;
520
+ this.redactor = redactor;
273
521
  }
274
522
  inner;
523
+ redactor;
275
524
  export(spans, resultCallback) {
276
- this.inner.export(spans, (result) => {
277
- if (result.code === ExportResultCode.SUCCESS) {
278
- telemetryStats.onExported(spans.length);
525
+ const redactor = this.redactor();
526
+ let scrubbed = spans;
527
+ let failed = 0;
528
+ if (redactor.enabled) {
529
+ scrubbed = [];
530
+ for (const span of spans) {
531
+ try {
532
+ scrubbed.push(redactSpan(span, redactor));
533
+ } catch {
534
+ failed++;
535
+ }
279
536
  }
280
- resultCallback(result);
281
- });
537
+ }
538
+ if (!failed) {
539
+ this.inner.export(scrubbed, resultCallback);
540
+ return;
541
+ }
542
+ const error = new Error(`autter-runtime: ${failed} span(s) could not be redacted and were not exported`);
543
+ const fail = () => resultCallback({ code: ExportResultCode.FAILED, error });
544
+ if (!scrubbed.length) {
545
+ fail();
546
+ return;
547
+ }
548
+ this.inner.export(
549
+ scrubbed,
550
+ (result) => result.code === ExportResultCode.SUCCESS ? fail() : resultCallback(result)
551
+ );
282
552
  }
283
553
  shutdown() {
284
554
  return this.inner.shutdown();
@@ -287,394 +557,38 @@ var CountingExporter = class {
287
557
  return this.inner.forceFlush?.() ?? Promise.resolve();
288
558
  }
289
559
  };
290
- var flushTargets = /* @__PURE__ */ new Map();
291
- function registerFlushTarget(key, target) {
292
- flushTargets.set(key, target);
293
- }
294
- function unregisterFlushTargets() {
295
- flushTargets.clear();
296
- }
297
- function activeFlushTargets() {
298
- return [...flushTargets.values()];
299
- }
300
- var DEFAULT_SIGNALS = ["SIGINT", "SIGTERM"];
301
- var SIGNAL_EXIT_CODES = {
302
- SIGINT: 130,
303
- SIGTERM: 143
304
- };
305
- function installAutterAutoFlush(options = {}) {
306
- const logger = options.logger ?? console;
307
- const log = options.log !== false;
308
- const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
309
- const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
310
- const signals = options.signals ?? DEFAULT_SIGNALS;
311
- const flushOnUncaughtException = options.flushOnUncaughtException !== false;
312
- let disposed = false;
313
- let inFlight2 = null;
314
- const doFlush = async () => {
315
- const targets = options.targets ?? activeFlushTargets();
316
- if (targets.length === 0) return true;
317
- let timer2;
318
- const timedOut = new Promise((resolve) => {
319
- timer2 = setTimeout(() => resolve(false), timeoutMs);
320
- });
321
- const drained = Promise.allSettled(
322
- targets.map(
323
- (target) => Promise.resolve().then(() => target.forceFlush())
324
- )
325
- ).then(
326
- (results) => results.every((result) => result.status === "fulfilled")
327
- );
328
- let ok;
329
- try {
330
- ok = await Promise.race([drained, timedOut]);
331
- } finally {
332
- clearTimeout(timer2);
333
- }
334
- if (ok) {
335
- telemetryStats.markAllFlushed();
336
- if (log) {
337
- logger.log("[autter] telemetry flushed");
338
- }
339
- } else {
340
- logger.error(
341
- `[autter] flush did not finish within ${timeoutMs}ms \u2014 some telemetry may be lost`
342
- );
343
- }
344
- return ok;
345
- };
346
- const flush = (reason = "exit") => {
347
- if (disposed) return Promise.resolve(false);
348
- if (!inFlight2) {
349
- inFlight2 = doFlush().finally(() => {
350
- inFlight2 = null;
351
- });
352
- }
353
- if (isDebugEnabled()) debugLog(`auto-flush requested on ${reason}`);
354
- return inFlight2;
355
- };
356
- const signalCounts = /* @__PURE__ */ new Map();
357
- let handlingUncaughtException = false;
358
- const onUncaughtException = (error) => {
359
- if (process.listenerCount("uncaughtException") > 1) return;
360
- if (handlingUncaughtException) return;
361
- handlingUncaughtException = true;
362
- logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
363
- const keepAlive = setTimeout(() => {
364
- }, timeoutMs + 50);
365
- void flush("uncaughtException").finally(() => {
366
- clearTimeout(keepAlive);
367
- process.exit(1);
368
- });
369
- };
370
- const onSignal = (signal) => {
371
- const count = (signalCounts.get(signal) ?? 0) + 1;
372
- signalCounts.set(signal, count);
373
- const exitCode = SIGNAL_EXIT_CODES[signal] ?? 1;
374
- if (count > 1) {
375
- process.exit(exitCode);
376
- }
377
- const otherHandlers = process.listenerCount(signal) - 1;
378
- if (otherHandlers > 0) {
379
- void flush(signal);
380
- return;
381
- }
382
- void flush(signal).finally(() => {
383
- process.exit(exitCode);
384
- });
385
- };
386
- const MAX_BEFORE_EXIT_FLUSHES = 10;
387
- let beforeExitFlushes = 0;
388
- const onBeforeExit = () => {
389
- if (disposed) return;
390
- const targets = options.targets ?? activeFlushTargets();
391
- const worthIt = beforeExitFlushes === 0 ? targets.length > 0 : telemetryStats.pendingCaptures > 0;
392
- if (!worthIt || beforeExitFlushes >= MAX_BEFORE_EXIT_FLUSHES) return;
393
- beforeExitFlushes++;
394
- const keepAlive = setTimeout(() => {
395
- }, timeoutMs);
396
- void flush("beforeExit").finally(() => clearTimeout(keepAlive));
397
- };
398
- const onExit = () => {
399
- if (!warnOnUnflushedExit || disposed) return;
400
- const pending = telemetryStats.pendingCaptures;
401
- if (pending > 0) {
402
- logger.error(
403
- `[autter] process exiting with ~${pending} captured item(s) never confirmed exported \u2014 await autter.shutdown() on graceful shutdown`
404
- );
405
- }
406
- };
407
- const signalHandlers = /* @__PURE__ */ new Map();
408
- process.on("beforeExit", onBeforeExit);
409
- if (flushOnUncaughtException) {
410
- process.on("uncaughtException", onUncaughtException);
411
- }
412
- for (const signal of signals) {
413
- const handler = () => onSignal(signal);
414
- signalHandlers.set(signal, handler);
415
- process.on(signal, handler);
416
- }
417
- process.on("exit", onExit);
418
- return {
419
- flush,
420
- dispose() {
421
- disposed = true;
422
- process.removeListener("beforeExit", onBeforeExit);
423
- process.removeListener("uncaughtException", onUncaughtException);
424
- process.removeListener("exit", onExit);
425
- for (const [signal, handler] of signalHandlers) {
426
- process.removeListener(signal, handler);
427
- }
428
- signalHandlers.clear();
429
- }
430
- };
431
- }
432
-
433
- // src/redact.ts
434
- var SENSITIVE_KEY_PATTERNS = [
435
- /e-?mail/,
436
- /pass(word|wd|phrase)|^pass$/,
437
- /token/,
438
- /secret/,
439
- /credential/,
440
- /(api|access|secret|private|consumer|client|signing|encryption)-?[_.]?key/,
441
- /^-?x?-?authorization$|^auth(-|_|$)|bearer/,
442
- /cookie/,
443
- /phone|msisdn/,
444
- /\bssn\b|social[-_ ]?security/,
445
- /cvv|cvc|card([-_. ]?(number|num|no))?$/,
446
- /credit[-_.]?card/,
447
- /connection[-_.]?string/,
448
- /recovery[-_.]?code|\botp\b|magic[-_.]?link/
449
- ];
450
- var PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g;
451
- var EMAIL_VALUE_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
452
- var JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
453
- var OPENAI_KEY_RE = /\bsk-[A-Za-z0-9]{20,}\b/g;
454
- var GITHUB_TOKEN_RE = /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g;
455
- var AWS_KEY_RE = /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g;
456
- var SLACK_TOKEN_RE = /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g;
457
- var BEARER_RE = /\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/gi;
458
- var URL_CREDENTIALS_RE = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
459
- function toCaseInsensitive(pattern) {
460
- return typeof pattern === "string" ? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(pattern.source, pattern.flags.includes("i") ? pattern.flags : `${pattern.flags}i`);
461
- }
462
- function compile(options) {
463
- const keyPatterns = [...SENSITIVE_KEY_PATTERNS];
464
- const valuePatterns = [
465
- JWT_RE,
466
- OPENAI_KEY_RE,
467
- GITHUB_TOKEN_RE,
468
- AWS_KEY_RE,
469
- SLACK_TOKEN_RE,
470
- BEARER_RE
471
- ];
472
- if (options?.additionalKeyPatterns) {
473
- for (const p of options.additionalKeyPatterns) {
474
- keyPatterns.push(toCaseInsensitive(p));
475
- }
560
+ var RedactingMetricExporter = class {
561
+ constructor(inner, redactor) {
562
+ this.inner = inner;
563
+ this.redactor = redactor;
476
564
  }
477
- if (options?.additionalValuePatterns) {
478
- for (const p of options.additionalValuePatterns) {
479
- if (!valuePatterns.some((existing) => existing.source === (typeof p === "string" ? p : p.source))) {
480
- valuePatterns.push(typeof p === "string" ? new RegExp(p, "gi") : p);
481
- }
565
+ inner;
566
+ redactor;
567
+ export(metrics2, resultCallback) {
568
+ const redactor = this.redactor();
569
+ if (!redactor.enabled) {
570
+ this.inner.export(metrics2, resultCallback);
571
+ return;
482
572
  }
483
- }
484
- return {
485
- keyPatterns,
486
- valuePatterns,
487
- urlCredentials: URL_CREDENTIALS_RE,
488
- privateKeyBlock: PRIVATE_KEY_BLOCK_RE,
489
- mask: options?.mask ?? "[redacted]",
490
- scrubEmailValues: options?.scrubEmailValues !== false
491
- };
492
- }
493
- function redactString(value2, r) {
494
- let out = value2.replace(r.privateKeyBlock, r.mask);
495
- for (const re of r.valuePatterns) {
496
- out = out.replace(re, r.mask);
497
- }
498
- out = out.replace(r.urlCredentials, "$1" + r.mask + "@");
499
- if (r.scrubEmailValues) {
500
- out = out.replace(EMAIL_VALUE_RE, r.mask);
501
- }
502
- return out;
503
- }
504
- var MAX_REDACTION_DEPTH = 64;
505
- var MAX_REDACTION_WORK = 1e4;
506
- var MAX_COLLECTION_ENTRIES = 1e3;
507
- function canTraverse(depth, state) {
508
- if (depth > MAX_REDACTION_DEPTH || state.remainingWork <= 0) return false;
509
- state.remainingWork -= 1;
510
- return true;
511
- }
512
- function redactValue(value2, r, state, depth) {
513
- if (typeof value2 === "string") return redactString(value2, r);
514
- let isArray = false;
515
- try {
516
- isArray = Array.isArray(value2);
517
- } catch {
518
- return r.mask;
519
- }
520
- if (isArray) {
521
- return redactArray(value2, r, state, depth);
522
- }
523
- if (typeof value2 === "object" && value2 !== null) {
524
- return redactObject(value2, r, state, depth);
525
- }
526
- return value2;
527
- }
528
- function redactArray(value2, r, state, depth) {
529
- const existing = state.ancestors.get(value2);
530
- if (existing) return r.mask;
531
- if (!canTraverse(depth, state)) return r.mask;
532
- const out = [];
533
- state.ancestors.set(value2, out);
534
- let length;
535
- try {
536
- length = value2.length;
537
- } catch {
538
- state.ancestors.delete(value2);
539
- return r.mask;
540
- }
541
- const limit = Math.min(length, MAX_COLLECTION_ENTRIES);
542
- for (let i = 0; i < limit; i += 1) {
543
- let item;
573
+ let resource;
544
574
  try {
545
- item = value2[i];
546
- } catch {
547
- out.push(r.mask);
548
- continue;
575
+ resource = redactResource(metrics2.resource, redactor);
576
+ } catch (error) {
577
+ resultCallback({ code: ExportResultCode.FAILED, error });
578
+ return;
549
579
  }
550
- out.push(redactValue(item, r, state, depth + 1));
580
+ this.inner.export({ ...metrics2, resource }, resultCallback);
551
581
  }
552
- if (length > limit) {
553
- out.push(r.mask);
582
+ forceFlush() {
583
+ return this.inner.forceFlush();
554
584
  }
555
- state.ancestors.delete(value2);
556
- return out;
557
- }
558
- function redactObject(value2, r, state, depth) {
559
- const existing = state.ancestors.get(value2);
560
- if (existing) return r.mask;
561
- if (!canTraverse(depth, state)) return r.mask;
562
- const out = {};
563
- state.ancestors.set(value2, out);
564
- let count = 0;
565
- let truncated = false;
566
- try {
567
- for (const key in value2) {
568
- if (!Object.prototype.propertyIsEnumerable.call(
569
- value2,
570
- key
571
- )) {
572
- continue;
573
- }
574
- if (count >= MAX_COLLECTION_ENTRIES) {
575
- truncated = true;
576
- break;
577
- }
578
- let nestedValue;
579
- try {
580
- nestedValue = value2[key];
581
- } catch {
582
- out[key] = r.mask;
583
- count += 1;
584
- continue;
585
- }
586
- count += 1;
587
- out[key] = isSensitiveKey(key, nestedValue, r) ? r.mask : redactValue(
588
- nestedValue,
589
- r,
590
- state,
591
- depth + 1
592
- );
593
- }
594
- } catch {
595
- truncated = true;
596
- }
597
- if (truncated) {
598
- out.__redaction_truncated__ = r.mask;
599
- }
600
- state.ancestors.delete(value2);
601
- return out;
602
- }
603
- var USAGE_TOKEN_KEYS = /* @__PURE__ */ new Set([
604
- "input_tokens",
605
- "output_tokens",
606
- "prompt_tokens",
607
- "completion_tokens",
608
- "total_tokens",
609
- "token_count",
610
- "gen_ai.usage.input_tokens",
611
- "gen_ai.usage.output_tokens",
612
- "gen_ai.usage.prompt_tokens",
613
- "gen_ai.usage.completion_tokens",
614
- "gen_ai.usage.total_tokens",
615
- "gen_ai.usage.token_count"
616
- ]);
617
- function isSensitiveKey(key, value2, r) {
618
- const lowered = key.toLowerCase();
619
- if (USAGE_TOKEN_KEYS.has(lowered)) {
620
- return !(typeof value2 === "number" && Number.isFinite(value2) && value2 >= 0);
621
- }
622
- return r.keyPatterns.some((re) => re.test(lowered));
623
- }
624
- function redactAttributes(attributes, options) {
625
- const r = compile(options);
626
- return redactWith(attributes, r);
627
- }
628
- function redactWith(attributes, r) {
629
- const out = {};
630
- if (!attributes) return out;
631
- const state = {
632
- ancestors: /* @__PURE__ */ new WeakMap(),
633
- remainingWork: MAX_REDACTION_WORK
634
- };
635
- let count = 0;
636
- let truncated = false;
637
- try {
638
- for (const key in attributes) {
639
- if (!Object.prototype.propertyIsEnumerable.call(
640
- attributes,
641
- key
642
- )) {
643
- continue;
644
- }
645
- if (count >= MAX_COLLECTION_ENTRIES || state.remainingWork <= 0) {
646
- truncated = true;
647
- break;
648
- }
649
- let value2;
650
- try {
651
- value2 = attributes[key];
652
- } catch {
653
- out[key] = r.mask;
654
- count += 1;
655
- state.remainingWork -= 1;
656
- continue;
657
- }
658
- count += 1;
659
- state.remainingWork -= 1;
660
- if (value2 === void 0) continue;
661
- out[key] = isSensitiveKey(key, value2, r) ? r.mask : redactValue(value2, r, state, 0);
662
- }
663
- } catch {
664
- truncated = true;
585
+ shutdown() {
586
+ return this.inner.shutdown();
665
587
  }
666
- if (truncated) {
667
- out.__redaction_truncated__ = r.mask;
588
+ selectAggregationTemporality(instrumentType) {
589
+ return this.inner.selectAggregationTemporality ? this.inner.selectAggregationTemporality(instrumentType) : AggregationTemporality.CUMULATIVE;
668
590
  }
669
- return out;
670
- }
671
- function makeRedactor(options) {
672
- if (options === false) {
673
- return (attributes) => ({ ...attributes ?? {} });
674
- }
675
- const r = compile(options === true ? void 0 : options);
676
- return (attributes) => redactWith(attributes, r);
677
- }
591
+ };
678
592
 
679
593
  // src/memory.ts
680
594
  import { PerformanceObserver } from "perf_hooks";
@@ -721,388 +635,544 @@ function startMemoryMetrics() {
721
635
  };
722
636
  }
723
637
 
724
- // src/logger.ts
725
- import { AsyncLocalStorage } from "async_hooks";
726
- import { randomUUID } from "crypto";
727
- import { trace } from "@opentelemetry/api";
728
- var local = new AsyncLocalStorage();
729
- var config = null;
730
- var queue = [];
731
- var queueBytes = 0;
732
- var inFlightBytes = 0;
733
- var timer;
734
- var flushing = null;
735
- var dropped = 0;
736
- var inFlight = 0;
737
- var stopping = false;
738
- var failureStreak = 0;
739
- var severity = { debug: 5, info: 9, warning: 13, error: 17 };
740
- function safe(attributes = {}) {
741
- const redacted = config?.redact(attributes) ?? redactAttributes(attributes);
742
- let budget = 512;
743
- let characters = 16384;
744
- let truncated = false;
745
- const visit = (input, depth) => {
746
- if (--budget < 0 || depth > 6) {
747
- truncated = true;
748
- return "[truncated]";
749
- }
750
- if (typeof input === "string") {
751
- const text = input.replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/gi, "$1");
752
- const kept = text.slice(0, Math.max(0, characters));
753
- truncated ||= kept.length < text.length;
754
- characters -= kept.length;
755
- return kept;
756
- }
757
- if (typeof input === "number")
758
- return Number.isFinite(input) ? input : void 0;
759
- if (typeof input === "boolean" || input === null) return input;
760
- if (Array.isArray(input)) {
761
- truncated ||= input.length > 64;
762
- return input.slice(0, 64).map((item) => visit(item, depth + 1));
638
+ // src/requests.ts
639
+ import { AsyncResource } from "async_hooks";
640
+ import { context, trace } from "@opentelemetry/api";
641
+ import { getRPCMetadata, RPCType } from "@opentelemetry/core";
642
+ var REQUEST_STATE = /* @__PURE__ */ Symbol.for("autter.runtime.request");
643
+ var BOUND = /* @__PURE__ */ Symbol.for("autter.runtime.bound");
644
+ var CAPTURED = /* @__PURE__ */ new WeakSet();
645
+ var capturer = () => {
646
+ };
647
+ function setRequestExceptionCapturer(fn) {
648
+ capturer = fn;
649
+ }
650
+ var annotator = () => {
651
+ };
652
+ function setRequestSpanAnnotator(fn) {
653
+ annotator = fn;
654
+ }
655
+ function markCaptured(error) {
656
+ if (error && typeof error === "object") CAPTURED.add(error);
657
+ }
658
+ function expressRouteOf(req) {
659
+ const props = req;
660
+ const path = props?.route?.path;
661
+ if (typeof path !== "string" || path === "") return null;
662
+ const base = typeof props?.baseUrl === "string" ? props.baseUrl : "";
663
+ const route = base + path;
664
+ return route.startsWith("/") ? route : null;
665
+ }
666
+ function defaultShouldCapture(error) {
667
+ return error instanceof RuntimeError || errorCodeOf(error) !== void 0 || responseStatusOf(error) >= 500;
668
+ }
669
+ function onRequestSpan(fn) {
670
+ const active2 = context.active();
671
+ const metadata = getRPCMetadata(active2);
672
+ const span = metadata?.type === RPCType.HTTP ? metadata.span : void 0;
673
+ if (!span || !span.isRecording() || trace.getSpan(active2) === span) {
674
+ fn();
675
+ return;
676
+ }
677
+ context.with(trace.setSpan(active2, span), fn);
678
+ }
679
+ function captureAtBoundary(error, state, rule = void 0, attributes) {
680
+ let wanted;
681
+ try {
682
+ wanted = typeof rule === "function" ? rule(error) : rule === void 0 ? defaultShouldCapture(error) : rule;
683
+ } catch {
684
+ wanted = false;
685
+ }
686
+ if (!wanted) return;
687
+ if (error && typeof error === "object" && CAPTURED.has(error)) return;
688
+ markCaptured(error);
689
+ try {
690
+ const run = () => onRequestSpan(() => capturer(error, attributes));
691
+ if (state && !state.sealed) local.run(state, run);
692
+ else run();
693
+ } catch {
694
+ }
695
+ }
696
+ function requestStateOf(req) {
697
+ if (!req || typeof req !== "object") return void 0;
698
+ return req[REQUEST_STATE];
699
+ }
700
+ function captureRequestError(error, attributes) {
701
+ if (error && typeof error === "object" && CAPTURED.has(error)) {
702
+ if (!attributes) return;
703
+ try {
704
+ onRequestSpan(() => annotator(attributes));
705
+ } catch {
763
706
  }
764
- if (input && typeof input === "object") {
765
- const entries = Object.entries(input);
766
- if (depth === 0) {
767
- const priority = (key) => /^autter\.(?:operation|event)\./.test(key) ? 2 : /^exception\./.test(key) ? 1 : 0;
768
- entries.sort(([a], [b]) => priority(b) - priority(a));
769
- }
770
- truncated ||= entries.length > 100;
771
- return Object.fromEntries(
772
- entries.slice(0, 100).filter(([key]) => !/^(?:__proto__|constructor|prototype)$/.test(key)).map(([key, item]) => [key.slice(0, 200), visit(item, depth + 1)])
773
- );
707
+ return;
708
+ }
709
+ captureAtBoundary(error, currentOperation(), true, attributes);
710
+ }
711
+ function bindEmitter(emitter, state) {
712
+ const target = emitter;
713
+ if (!target || typeof target !== "object" || target[BOUND]) return;
714
+ target[BOUND] = true;
715
+ const wrappers = /* @__PURE__ */ new WeakMap();
716
+ const wrap = (listener) => {
717
+ if (typeof listener !== "function") return listener;
718
+ let wrapped = wrappers.get(listener);
719
+ if (!wrapped) {
720
+ wrapped = function(...args) {
721
+ return local.run(
722
+ state,
723
+ () => listener.apply(this, args)
724
+ );
725
+ };
726
+ wrappers.set(listener, wrapped);
774
727
  }
775
- return void 0;
728
+ return wrapped;
776
729
  };
777
- const result = visit(redacted, 0);
778
- if (truncated) result["autter.context.truncated"] = true;
779
- return result;
780
- }
781
- function operationAttributes(state) {
782
- return state ? {
783
- "autter.operation.id": state.id,
784
- "autter.operation.name": state.name,
785
- ...state.parentId ? { "autter.operation.parent_id": state.parentId } : {},
786
- ...state.attributes
787
- } : {};
788
- }
789
- function userAttributes(attributes = {}) {
790
- return Object.fromEntries(
791
- Object.entries(safe(attributes)).filter(
792
- ([key]) => !/^autter\.(?:operation|event)\./.test(key)
793
- )
794
- );
730
+ for (const method of [
731
+ "on",
732
+ "addListener",
733
+ "prependListener",
734
+ "once",
735
+ "prependOnceListener"
736
+ ]) {
737
+ const original = target[method];
738
+ if (typeof original !== "function") continue;
739
+ target[method] = function(event, listener) {
740
+ return original.call(this, event, wrap(listener));
741
+ };
742
+ }
743
+ for (const method of ["removeListener", "off"]) {
744
+ const original = target[method];
745
+ if (typeof original !== "function") continue;
746
+ target[method] = function(event, listener) {
747
+ const wrapped = typeof listener === "function" ? wrappers.get(listener) ?? listener : listener;
748
+ return original.call(this, event, wrapped);
749
+ };
750
+ }
795
751
  }
796
- function mergeContext(current, next = {}) {
797
- const merge = (left, right, depth) => {
798
- const result = { ...left };
799
- for (const [key, value2] of Object.entries(right)) {
800
- const prior = result[key];
801
- result[key] = depth < 6 && value2 && typeof value2 === "object" && !Array.isArray(value2) && prior && typeof prior === "object" && !Array.isArray(prior) ? merge(prior, value2, depth + 1) : value2;
752
+ function exposeHeaderValue(current, header) {
753
+ const text = Array.isArray(current) ? current.join(", ") : String(current ?? "");
754
+ const names = text.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
755
+ if (names.includes("*") || names.includes(header)) return null;
756
+ return names.length ? `${text}, ${header}` : header;
757
+ }
758
+ function exposeOnCors(res, header) {
759
+ const original = res.writeHead;
760
+ res.writeHead = function(...args) {
761
+ try {
762
+ const inline = args.slice(1).find(
763
+ (arg) => !!arg && typeof arg === "object" && !Array.isArray(arg)
764
+ );
765
+ const inlineKey = (name) => inline ? Object.keys(inline).find((key) => key.toLowerCase() === name) : void 0;
766
+ const cors = res.getHeader("access-control-allow-origin") !== void 0 || inlineKey("access-control-allow-origin") !== void 0;
767
+ if (cors) {
768
+ const key = inlineKey("access-control-expose-headers");
769
+ const value = exposeHeaderValue(
770
+ key && inline ? inline[key] : res.getHeader("access-control-expose-headers"),
771
+ header
772
+ );
773
+ if (value && key && inline) inline[key] = value;
774
+ else if (value) res.setHeader("access-control-expose-headers", value);
775
+ }
776
+ } catch {
802
777
  }
803
- return result;
778
+ return original.apply(this, args);
804
779
  };
805
- return userAttributes(merge(current, userAttributes(next), 0));
806
- }
807
- function traceAttributes(state) {
808
- return Object.fromEntries(
809
- Object.entries(operationAttributes(state)).filter(([, item]) => item !== null && item !== void 0).map(([key, item]) => [
810
- key,
811
- typeof item === "object" ? JSON.stringify(item) : item
812
- ])
813
- );
814
780
  }
815
- function scheduleFlush() {
816
- if (!timer && config && !stopping && queue.length) {
817
- timer = setTimeout(
818
- () => {
819
- timer = void 0;
820
- void flushRuntimeLogs().catch(() => {
821
- });
822
- },
823
- Math.min(3e4, 2e3 * 2 ** Math.min(failureStreak, 4))
781
+ function pathOf(req) {
782
+ const raw = req.originalUrl ?? req.url ?? "/";
783
+ return raw.split(/[?#]/)[0] || "/";
784
+ }
785
+ function finishRequest(state, input) {
786
+ if (state.sealed) return;
787
+ state.name = (input.name ?? `${input.method} ${input.route}`).slice(0, 200);
788
+ state.outcome = requestOutcome({
789
+ ...state.explicitOutcome ? { explicit: state.outcome } : {},
790
+ ...state.error !== void 0 ? { error: state.error } : {},
791
+ ...state.errorThrown ? { thrown: true } : {},
792
+ status: input.status,
793
+ aborted: input.aborted
794
+ });
795
+ const span = trace.getActiveSpan();
796
+ if (span?.isRecording()) span.setAttribute("autter.operation.outcome", state.outcome);
797
+ if (input.span) {
798
+ const remote = trace.wrapSpanContext(input.span);
799
+ context.with(
800
+ trace.setSpan(context.active(), remote),
801
+ () => reportExplicitFailure(state)
824
802
  );
825
- timer.unref();
826
- }
827
- }
828
- function value(input) {
829
- if (typeof input === "number") return { doubleValue: input };
830
- if (typeof input === "boolean") return { boolValue: input };
831
- if (Array.isArray(input))
832
- return { arrayValue: { values: input.slice(0, 64).map(value) } };
833
- if (input && typeof input === "object")
834
- return {
835
- kvlistValue: {
836
- values: Object.entries(input).slice(0, 128).map(([key, item]) => ({ key, value: value(item) }))
837
- }
838
- };
839
- return { stringValue: String(input ?? "").slice(0, 32e3) };
803
+ } else reportExplicitFailure(state);
804
+ emitSummary(state, {
805
+ http: {
806
+ "http.request.method": input.method,
807
+ "http.route": input.route,
808
+ "http.response.status_code": input.status,
809
+ "autter.request.aborted": input.aborted
810
+ },
811
+ ...input.span ? { span: input.span } : {}
812
+ });
840
813
  }
841
- function emit(level, message, attributes, extra = {}) {
842
- const state = local.getStore();
843
- if (state?.sealed) return;
844
- if (extra["autter.event.type"] !== "operation" && severity[level] < severity[config?.options?.minLevel ?? "debug"])
845
- return;
846
- const span = trace.getActiveSpan()?.spanContext();
847
- const user = mergeContext(state?.attributes ?? {}, attributes);
848
- const exceptions = Object.fromEntries(
849
- Object.entries(user).filter(([key]) => key.startsWith("exception."))
814
+ function beginNodeRequest(req, res, options, route) {
815
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
816
+ const requestId = resolveRequestId(
817
+ options.trustRequestId === false ? void 0 : req.headers[header]
850
818
  );
851
- const attrs = safe({
852
- "autter.event.id": randomUUID(),
853
- ...extra,
854
- ...exceptions,
855
- ...operationAttributes(state),
856
- ...user
819
+ const method = (req.method ?? "GET").toUpperCase();
820
+ const path = pathOf(req);
821
+ const state = createRequestState({
822
+ name: `${method} ${normalizeRoutePath(path)}`,
823
+ requestId,
824
+ req,
825
+ res
857
826
  });
858
- const record = {
859
- timeUnixNano: String(BigInt(Date.now()) * 1000000n),
860
- severityNumber: severity[level],
861
- severityText: level.toUpperCase(),
862
- body: value(String(safe({ message }).message ?? "").slice(0, 4e3)),
863
- attributes: Object.entries(attrs).map(([key, item]) => ({
864
- key,
865
- value: value(item)
866
- })),
867
- ...span && /^[a-f0-9]{32}$/.test(span.traceId) && !/^0+$/.test(span.traceId) ? { traceId: span.traceId, spanId: span.spanId } : {}
827
+ req[REQUEST_STATE] = state;
828
+ try {
829
+ if (!res.headersSent) res.setHeader(header, requestId);
830
+ exposeOnCors(res, header);
831
+ } catch {
832
+ }
833
+ const span = trace.getActiveSpan();
834
+ if (span?.isRecording())
835
+ span.setAttributes({
836
+ "autter.request.id": requestId,
837
+ "autter.operation.kind": "request"
838
+ });
839
+ const spanContext = span?.spanContext();
840
+ bindEmitter(req, state);
841
+ bindEmitter(res, state);
842
+ let finished = false;
843
+ const done = () => {
844
+ finishRequest(state, {
845
+ method,
846
+ route: route?.() ?? expressRouteOf(req) ?? normalizeRoutePath(path),
847
+ status: res.statusCode,
848
+ aborted: !finished,
849
+ ...spanContext ? { span: spanContext } : {}
850
+ });
851
+ };
852
+ res.once("finish", () => {
853
+ finished = true;
854
+ done();
855
+ });
856
+ res.once("close", done);
857
+ return state;
858
+ }
859
+ function autterRequests(options = {}) {
860
+ const ignored = compileIgnore(options.ignore);
861
+ return function autterRequestsMiddleware(req, res, next) {
862
+ if (req[REQUEST_STATE] || ignored(pathOf(req))) {
863
+ next();
864
+ return;
865
+ }
866
+ const state = beginNodeRequest(req, res, options);
867
+ local.run(state, () => next());
868
868
  };
869
- if (config?.options?.console !== false)
870
- console.log(
871
- JSON.stringify({ ...attrs, level, message: record.body.stringValue })
869
+ }
870
+ function autterErrorResponse(options = {}) {
871
+ return function autterErrorResponseMiddleware(error, req, res, next) {
872
+ const state = req[REQUEST_STATE];
873
+ if (state) noteOperationError(error, true, state);
874
+ captureAtBoundary(error, state, options.capture);
875
+ if (res.headersSent) {
876
+ next(error);
877
+ return;
878
+ }
879
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
880
+ const inbound = req.headers?.[header];
881
+ const requestId = state?.requestId ?? (typeof inbound === "string" ? inbound.slice(0, 128) : void 0);
882
+ res.statusCode = responseStatusOf(error);
883
+ res.setHeader("content-type", "application/json; charset=utf-8");
884
+ res.end(JSON.stringify(toClientError(error, requestId)));
885
+ };
886
+ }
887
+ var RESOURCE = /* @__PURE__ */ Symbol.for("autter.runtime.resource");
888
+ function autterFastify(fastify, options, done) {
889
+ const opts = options ?? {};
890
+ const header = (opts.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
891
+ const ignored = compileIgnore(opts.ignore);
892
+ const stateOf = (request) => request[REQUEST_STATE];
893
+ fastify.addHook("onRequest", ((request, reply, next) => {
894
+ if (ignored(request.url) || request.raw[REQUEST_STATE]) {
895
+ next();
896
+ return;
897
+ }
898
+ const requestId = resolveRequestId(
899
+ opts.trustRequestId === false ? void 0 : request.headers[header]
872
900
  );
873
- if (!config) return;
874
- if (stopping) {
875
- dropped++;
876
- return;
901
+ const method = String(request.method ?? "GET").toUpperCase();
902
+ const state = createRequestState({
903
+ name: `${method} ${normalizeRoutePath(request.url)}`,
904
+ requestId,
905
+ req: request.raw,
906
+ res: reply.raw
907
+ });
908
+ request[REQUEST_STATE] = state;
909
+ request.raw[REQUEST_STATE] = state;
910
+ reply.header(header, requestId);
911
+ const span = trace.getActiveSpan();
912
+ if (span?.isRecording())
913
+ span.setAttributes({
914
+ "autter.request.id": requestId,
915
+ "autter.operation.kind": "request"
916
+ });
917
+ const spanContext = span?.spanContext();
918
+ bindEmitter(request.raw, state);
919
+ bindEmitter(reply.raw, state);
920
+ let finished = false;
921
+ const finish = (aborted) => finishRequest(state, {
922
+ method,
923
+ route: request.routeOptions?.url ?? request.routerPath ?? normalizeRoutePath(request.url),
924
+ status: reply.raw.statusCode || reply.statusCode,
925
+ aborted,
926
+ ...spanContext ? { span: spanContext } : {}
927
+ });
928
+ reply.raw.once("finish", () => {
929
+ finished = true;
930
+ finish(false);
931
+ });
932
+ reply.raw.once("close", () => finish(!finished));
933
+ local.run(state, () => {
934
+ request[RESOURCE] = new AsyncResource("autter-request");
935
+ next();
936
+ });
937
+ }));
938
+ fastify.addHook("preHandler", ((request, _reply, next) => {
939
+ const resource = request[RESOURCE];
940
+ if (resource) resource.runInAsyncScope(() => next());
941
+ else next();
942
+ }));
943
+ fastify.addHook("onSend", ((request, reply, payload, next) => {
944
+ try {
945
+ if (stateOf(request) && reply.getHeader("access-control-allow-origin") !== void 0) {
946
+ const value = exposeHeaderValue(reply.getHeader("access-control-expose-headers"), header);
947
+ if (value) reply.header("access-control-expose-headers", value);
948
+ }
949
+ } catch {
950
+ }
951
+ next(null, payload);
952
+ }));
953
+ fastify.addHook("onError", ((request, _reply, error, next) => {
954
+ const state = stateOf(request);
955
+ if (state) noteOperationError(error, true, state);
956
+ captureAtBoundary(error, state);
957
+ next();
958
+ }));
959
+ done();
960
+ }
961
+ autterFastify[/* @__PURE__ */ Symbol.for("skip-override")] = true;
962
+ autterFastify[/* @__PURE__ */ Symbol.for("fastify.display-name")] = "autter-runtime";
963
+ function withHeaders(response, header, requestId) {
964
+ const apply = (target) => {
965
+ target.headers.set(header, requestId);
966
+ if (target.headers.get("access-control-allow-origin") !== null) {
967
+ const value = exposeHeaderValue(
968
+ target.headers.get("access-control-expose-headers") ?? "",
969
+ header
970
+ );
971
+ if (value) target.headers.set("access-control-expose-headers", value);
972
+ }
973
+ return target;
974
+ };
975
+ try {
976
+ return apply(response);
977
+ } catch {
978
+ try {
979
+ return apply(new Response(response.body, response));
980
+ } catch {
981
+ return response;
982
+ }
877
983
  }
878
- const bytes = Buffer.byteLength(JSON.stringify(record));
879
- if (bytes > 256 * 1024 || queue.length + inFlight >= 1e3 || queueBytes + inFlightBytes + bytes > 4 * 1024 * 1024) {
880
- dropped++;
881
- console.warn(
882
- "[autter-runtime] log buffer or record limit reached; record dropped"
984
+ }
985
+ function withRuntimeRequest(handler, options = {}) {
986
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
987
+ const ignored = compileIgnore(options.ignore);
988
+ return async (request, ...rest) => {
989
+ let pathname = "/";
990
+ try {
991
+ pathname = new URL(request.url, "http://localhost").pathname;
992
+ } catch {
993
+ }
994
+ if (ignored(pathname)) return handler(request, ...rest);
995
+ const requestId = resolveRequestId(
996
+ options.trustRequestId === false ? void 0 : request.headers.get(header)
883
997
  );
884
- return;
885
- }
886
- queue.push({ record, bytes });
887
- queueBytes += bytes;
888
- scheduleFlush();
889
- }
890
- function configureRuntimeLogger(next) {
891
- config = next;
892
- stopping = false;
893
- failureStreak = 0;
894
- }
895
- function runtimeLogStats() {
896
- return { buffered: queue.length + inFlight, dropped };
897
- }
898
- async function flushRuntimeLogs() {
899
- if (flushing) return flushing;
900
- if (!config || !queue.length) return;
901
- if (timer) {
902
- clearTimeout(timer);
903
- timer = void 0;
904
- }
905
- const activeConfig = config;
906
- const deadline = Date.now() + 1e4;
907
- flushing = (async () => {
908
- const count = queue.length;
909
- for (let remaining = count; remaining > 0; ) {
910
- const batch = [];
911
- inFlightBytes = 0;
912
- while (batch.length < Math.min(50, remaining) && queue.length) {
913
- const next = queue[0];
914
- if (batch.length && inFlightBytes + next.bytes > 512 * 1024) break;
915
- batch.push(queue.shift());
916
- inFlightBytes += next.bytes;
917
- queueBytes -= next.bytes;
918
- }
919
- remaining -= batch.length;
920
- inFlight = batch.length;
921
- const body = JSON.stringify({
922
- resourceLogs: [
923
- {
924
- resource: {
925
- attributes: Object.entries({
926
- "service.name": activeConfig.service,
927
- "deployment.environment.name": activeConfig.environment,
928
- ...activeConfig.release ? { "service.version": activeConfig.release } : {}
929
- }).map(([key, item]) => ({ key, value: value(item) }))
930
- },
931
- scopeLogs: [
932
- {
933
- scope: { name: "autter-runtime" },
934
- logRecords: batch.map((entry) => entry.record)
935
- }
936
- ]
937
- }
938
- ]
998
+ const method = String(request.method ?? "GET").toUpperCase();
999
+ const route = options.route ?? normalizeRoutePath(pathname);
1000
+ const state = createRequestState({
1001
+ name: `${method} ${options.name ?? route}`,
1002
+ requestId,
1003
+ req: request
1004
+ });
1005
+ const span = trace.getActiveSpan();
1006
+ if (span?.isRecording())
1007
+ span.setAttributes({
1008
+ "autter.request.id": requestId,
1009
+ "autter.operation.kind": "request"
939
1010
  });
940
- let failure;
941
- for (let attempt = 0; attempt < 3; attempt++) {
1011
+ const spanContext = span?.spanContext();
1012
+ let status = 500;
1013
+ try {
1014
+ const response = await local.run(state, async () => {
942
1015
  try {
943
- if (Date.now() >= deadline)
944
- throw new Error("Runtime log flush exceeded 10 seconds");
945
- const response = await fetch(`${activeConfig.endpoint}/v1/logs`, {
946
- method: "POST",
947
- headers: {
948
- "content-type": "application/json",
949
- authorization: `Bearer ${activeConfig.apiKey}`
950
- },
951
- body,
952
- signal: AbortSignal.timeout(
953
- Math.max(1, Math.min(3e3, deadline - Date.now()))
954
- )
955
- });
956
- if (!response.ok)
957
- throw new Error(`Runtime log export failed (${response.status})`);
958
- failure = void 0;
959
- break;
1016
+ return await handler(request, ...rest);
960
1017
  } catch (error) {
961
- failure = error;
1018
+ noteOperationError(error, true, state);
1019
+ captureAtBoundary(error, state, true);
1020
+ throw error;
1021
+ }
1022
+ });
1023
+ status = response.status;
1024
+ return withHeaders(response, header, requestId);
1025
+ } finally {
1026
+ const aborted = request.signal?.aborted === true;
1027
+ finishRequest(state, {
1028
+ method,
1029
+ route,
1030
+ name: `${method} ${options.name ?? route}`,
1031
+ status,
1032
+ aborted,
1033
+ ...spanContext ? { span: spanContext } : {}
1034
+ });
1035
+ if (options.waitUntil) {
1036
+ try {
1037
+ options.waitUntil(flushRuntimeLogs().catch(() => {
1038
+ }));
1039
+ } catch {
962
1040
  }
963
1041
  }
964
- inFlight = 0;
965
- if (failure) {
966
- failureStreak++;
967
- queue = [...batch, ...queue];
968
- queueBytes += inFlightBytes;
969
- inFlightBytes = 0;
970
- throw failure;
971
- }
972
- inFlightBytes = 0;
973
- failureStreak = 0;
974
1042
  }
975
- })().finally(() => {
976
- flushing = null;
977
- scheduleFlush();
978
- });
979
- return flushing;
1043
+ };
1044
+ }
1045
+ function startHookRequest(res) {
1046
+ const req = res.req;
1047
+ if (!req || req[REQUEST_STATE]) return;
1048
+ const state = beginNodeRequest(req, res, {});
1049
+ local.enterWith(state);
980
1050
  }
981
- async function shutdownRuntimeLogger() {
982
- stopping = true;
1051
+
1052
+ // src/unhandled-rejections.ts
1053
+ var MODES = /* @__PURE__ */ new Set([
1054
+ "throw",
1055
+ "strict",
1056
+ "warn",
1057
+ "warn-with-error-code",
1058
+ "none"
1059
+ ]);
1060
+ var LISTENER_MARK = /* @__PURE__ */ Symbol.for("autter.runtime.unhandledRejection.listener");
1061
+ var HANDLED_KEY = /* @__PURE__ */ Symbol.for("autter.runtime.unhandledRejection.handled");
1062
+ function handledPromises() {
1063
+ const registry = globalThis;
1064
+ let set = registry[HANDLED_KEY];
1065
+ if (!set) {
1066
+ set = /* @__PURE__ */ new WeakSet();
1067
+ registry[HANDLED_KEY] = set;
1068
+ }
1069
+ return set;
1070
+ }
1071
+ var recorded = /* @__PURE__ */ new WeakSet();
1072
+ function markRecorded(reason) {
1073
+ if (!reason || typeof reason !== "object") return;
1074
+ recorded.add(reason);
1075
+ setImmediate(() => recorded.delete(reason)).unref?.();
1076
+ }
1077
+ function recordedAsRejection(error) {
1078
+ return !!error && typeof error === "object" && recorded.has(error);
1079
+ }
1080
+ function splitOptions(value) {
1081
+ return value ? value.split(/\s+/).filter(Boolean) : [];
1082
+ }
1083
+ function unhandledRejectionsMode(execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS) {
1084
+ const args = [...splitOptions(nodeOptions), ...execArgv];
1085
+ let mode = "throw";
1086
+ for (let i = 0; i < args.length; i++) {
1087
+ const arg = args[i] ?? "";
1088
+ if (!arg.startsWith("--")) continue;
1089
+ const eq = arg.indexOf("=");
1090
+ const name = (eq < 0 ? arg : arg.slice(0, eq)).replace(/_/g, "-");
1091
+ if (name !== "--unhandled-rejections") continue;
1092
+ const value = eq < 0 ? args[++i] : arg.slice(eq + 1);
1093
+ if (value && MODES.has(value))
1094
+ mode = value;
1095
+ }
1096
+ return mode;
1097
+ }
1098
+ function isErrorLike(value) {
983
1099
  try {
984
- await flushRuntimeLogs();
985
- } finally {
986
- if (queue.length) {
987
- dropped += queue.length;
988
- console.warn(
989
- `[autter-runtime] ${queue.length} log records could not be delivered before shutdown`
990
- );
1100
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "stack");
1101
+ } catch {
1102
+ return false;
1103
+ }
1104
+ }
1105
+ function describeReason(reason) {
1106
+ if (typeof reason !== "object" && typeof reason !== "function" || reason === null) {
1107
+ try {
1108
+ return String(reason);
1109
+ } catch {
1110
+ return typeof reason;
991
1111
  }
992
- queue = [];
993
- queueBytes = 0;
994
- config = null;
995
- if (timer) clearTimeout(timer);
996
- timer = void 0;
1112
+ }
1113
+ try {
1114
+ const proto = Object.getPrototypeOf(reason);
1115
+ const ctor = proto?.constructor;
1116
+ const name = typeof ctor === "function" ? ctor.name : "";
1117
+ return name ? `#<${name}>` : "[object Object]";
1118
+ } catch {
1119
+ return "[object Object]";
997
1120
  }
998
1121
  }
999
- function createRuntimeLogger(attributes = {}) {
1000
- const attrs = userAttributes(attributes);
1001
- return {
1002
- debug: (message, extra) => emit("debug", message, mergeContext(attrs, extra)),
1003
- info: (message, extra) => emit("info", message, mergeContext(attrs, extra)),
1004
- warn: (message, extra) => emit("warning", message, mergeContext(attrs, extra)),
1005
- error: (error, extra) => emit("error", error instanceof Error ? error.message : String(error), {
1006
- ...attrs,
1007
- ...extra,
1008
- ...error instanceof Error ? {
1009
- "exception.type": error.name,
1010
- "exception.stacktrace": error.stack ?? ""
1011
- } : {}
1012
- })
1013
- };
1122
+ function toUncaught(reason, from) {
1123
+ if (isErrorLike(reason)) return reason;
1124
+ const error = new Error(
1125
+ `This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "${describeReason(reason)}".`
1126
+ );
1127
+ error.name = "UnhandledPromiseRejection";
1128
+ error.code = "ERR_UNHANDLED_REJECTION";
1129
+ Error.captureStackTrace?.(error, from);
1130
+ return error;
1014
1131
  }
1015
- var runtimeLogger = createRuntimeLogger();
1016
- function withRuntimeOperation(name, fn, attributes = {}) {
1017
- const parent = local.getStore();
1018
- const state = {
1019
- id: randomUUID(),
1020
- name: String(safe({ name }).name).slice(0, 200),
1021
- parentId: parent?.sealed ? void 0 : parent?.id,
1022
- attributes: mergeContext(
1023
- parent?.sealed ? {} : parent?.attributes ?? {},
1024
- attributes
1025
- ),
1026
- startedAt: Date.now(),
1027
- outcome: "succeeded",
1028
- steps: []
1132
+ var warningId = 0;
1133
+ function emitNodeWarning(reason) {
1134
+ try {
1135
+ const text = isErrorLike(reason) ? String(reason.stack) : describeReason(reason);
1136
+ process.emitWarning(text, "UnhandledPromiseRejectionWarning");
1137
+ process.emitWarning(
1138
+ `Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag \`--unhandled-rejections=strict\` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: ${++warningId})`,
1139
+ "UnhandledPromiseRejectionWarning"
1140
+ );
1141
+ } catch {
1142
+ }
1143
+ }
1144
+ function installUnhandledRejectionCapture(capture, mode = unhandledRejectionsMode()) {
1145
+ if (mode === "strict") return () => {
1029
1146
  };
1030
- const operation = {
1031
- id: state.id,
1032
- setContext: (attrs) => {
1033
- if (!state.sealed) {
1034
- state.attributes = mergeContext(state.attributes, attrs);
1035
- trace.getActiveSpan()?.setAttributes(traceAttributes(state));
1147
+ const listener = (reason, promise) => {
1148
+ try {
1149
+ if (promise && typeof promise === "object") {
1150
+ const handled = handledPromises();
1151
+ if (handled.has(promise)) return;
1152
+ handled.add(promise);
1036
1153
  }
1037
- },
1038
- outcome: (status, message) => {
1039
- if (state.sealed) return;
1040
- state.outcome = status;
1041
- state.message = message;
1042
- if (status === "failed")
1043
- state.reportingStack = new Error("Operation outcome reported here").stack ?? "";
1044
- },
1045
- step: async (stepName, stepFn) => {
1046
- const started = Date.now();
1047
- let status = "succeeded";
1048
- try {
1049
- return await stepFn();
1050
- } catch (error) {
1051
- status = "failed";
1052
- throw error;
1053
- } finally {
1054
- if (!state.sealed && state.steps.length < 64)
1055
- state.steps.push({
1056
- name: String(safe({ name: stepName }).name).slice(0, 200),
1057
- status,
1058
- durationMs: Date.now() - started
1059
- });
1154
+ const alone = process.listeners("unhandledRejection").every((fn) => fn[LISTENER_MARK] === true);
1155
+ if (alone && mode === "throw") {
1156
+ const error = toUncaught(reason, listener);
1157
+ process.nextTick(() => {
1158
+ throw error;
1159
+ });
1160
+ return;
1060
1161
  }
1162
+ if (alone && mode === "warn-with-error-code") {
1163
+ emitNodeWarning(reason);
1164
+ process.exitCode = 1;
1165
+ }
1166
+ markRecorded(reason);
1167
+ capture(reason);
1168
+ } catch {
1061
1169
  }
1062
1170
  };
1063
- const run = () => local.run(state, async () => {
1064
- let thrown;
1065
- try {
1066
- return await fn(operation);
1067
- } catch (error) {
1068
- state.outcome = "failed";
1069
- thrown = error;
1070
- throw error;
1071
- } finally {
1072
- trace.getActiveSpan()?.setAttributes({
1073
- ...traceAttributes(state),
1074
- "autter.operation.outcome": state.outcome
1075
- });
1076
- if (state.outcome === "failed" && !thrown)
1077
- config?.reportOutcome(
1078
- state.name,
1079
- state.message ?? "Operation failed",
1080
- {
1081
- ...traceAttributes(state),
1082
- "autter.outcome.stack": state.reportingStack ?? ""
1083
- }
1084
- );
1085
- emit(
1086
- state.outcome === "failed" ? "error" : "info",
1087
- state.message ?? `${state.name}: ${state.outcome}`,
1088
- {
1089
- ...operationAttributes(state),
1090
- ...thrown instanceof Error ? {
1091
- "exception.type": thrown.name,
1092
- "exception.stacktrace": thrown.stack ?? ""
1093
- } : {}
1094
- },
1095
- {
1096
- "autter.event.type": "operation",
1097
- "autter.operation.outcome": state.outcome,
1098
- "autter.operation.duration_ms": Date.now() - state.startedAt,
1099
- "autter.operation.steps": state.steps
1100
- }
1101
- );
1102
- state.sealed = true;
1103
- }
1104
- });
1105
- return config ? config.run(state.name, run, traceAttributes(state)) : run();
1171
+ listener[LISTENER_MARK] = true;
1172
+ process.on("unhandledRejection", listener);
1173
+ return () => {
1174
+ process.off("unhandledRejection", listener);
1175
+ };
1106
1176
  }
1107
1177
 
1108
1178
  // src/server.ts
@@ -1326,36 +1396,43 @@ async function runLlmSpan(tracer, info, fn) {
1326
1396
  kind: SpanKind.CLIENT,
1327
1397
  attributes: llmBaseAttributes(info)
1328
1398
  });
1329
- const setResponseModel = (model) => span.setAttribute("gen_ai.response.model", model);
1399
+ const usage = {};
1400
+ let responseModel;
1401
+ const setResponseModel = (model) => {
1402
+ responseModel = model;
1403
+ span.setAttribute("gen_ai.response.model", model);
1404
+ };
1330
1405
  const handle = {
1331
- setUsage: (usage) => span.setAttributes(usageAttributes(usage)),
1332
- setCost: (usd) => span.setAttribute("autter.llm.cost_usd", usd),
1406
+ setUsage: (next) => {
1407
+ Object.assign(usage, next);
1408
+ span.setAttributes(usageAttributes(next));
1409
+ },
1410
+ setCost: (usd) => {
1411
+ usage.costUsd = usd;
1412
+ span.setAttribute("autter.llm.cost_usd", usd);
1413
+ },
1333
1414
  setResponseModel,
1334
1415
  setModel: setResponseModel,
1335
- setAttributes: (attributes) => span.setAttributes(attributes)
1416
+ setAttributes: (attributes) => span.setAttributes(activeRedactor(attributes))
1336
1417
  };
1337
1418
  try {
1338
- const result = await context.with(
1339
- trace2.setSpan(context.active(), span),
1419
+ const result = await context2.with(
1420
+ trace2.setSpan(context2.active(), span),
1340
1421
  () => fn(handle)
1341
1422
  );
1342
1423
  span.setStatus({ code: SpanStatusCode.OK });
1343
1424
  return result;
1344
1425
  } catch (err) {
1345
- if (err instanceof Error) {
1346
- span.recordException(err);
1347
- span.setAttribute("error.type", err.name);
1348
- }
1349
- span.setStatus({
1350
- code: SpanStatusCode.ERROR,
1351
- message: err instanceof Error ? err.message : String(err)
1352
- });
1426
+ recordError(span, err);
1427
+ if (isError(err)) span.setAttribute("error.type", err.name);
1353
1428
  throw err;
1354
1429
  } finally {
1355
1430
  span.end();
1431
+ recordOperationAiUsage({ ...usage, model: responseModel ?? info.model });
1356
1432
  }
1357
1433
  }
1358
1434
  function recordLlmCall(tracer, call) {
1435
+ recordOperationAiUsage({ ...call, model: call.model });
1359
1436
  const durationMs = Math.max(0, call.durationMs ?? 0);
1360
1437
  const endTime = /* @__PURE__ */ new Date();
1361
1438
  const startTime = new Date(endTime.getTime() - durationMs);
@@ -1368,51 +1445,54 @@ function recordLlmCall(tracer, call) {
1368
1445
  }
1369
1446
  );
1370
1447
  if (call.error !== void 0 && call.error !== null) {
1371
- if (call.error instanceof Error) {
1372
- span.recordException(call.error);
1448
+ recordError(span, call.error);
1449
+ if (isError(call.error)) {
1373
1450
  span.setAttribute("error.type", call.error.name);
1374
1451
  }
1375
- span.setStatus({
1376
- code: SpanStatusCode.ERROR,
1377
- message: call.error instanceof Error ? call.error.message : String(call.error)
1378
- });
1379
1452
  } else {
1380
1453
  span.setStatus({ code: SpanStatusCode.OK });
1381
1454
  }
1382
1455
  span.end(endTime);
1383
1456
  }
1384
- async function runWithSpan(tracer, name, fn, attributes) {
1385
- const span = tracer.startSpan(name, { attributes });
1457
+ async function runWithSpan(tracer, name, fn, attributes, links) {
1458
+ const span = tracer.startSpan(name, {
1459
+ attributes,
1460
+ ...links?.length ? { links } : {}
1461
+ });
1386
1462
  try {
1387
- const result = await context.with(
1388
- trace2.setSpan(context.active(), span),
1463
+ const result = await context2.with(
1464
+ trace2.setSpan(context2.active(), span),
1389
1465
  fn
1390
1466
  );
1391
1467
  span.setStatus({ code: SpanStatusCode.OK });
1392
1468
  return result;
1393
1469
  } catch (err) {
1394
- if (err instanceof Error) span.recordException(err);
1395
- span.setStatus({
1396
- code: SpanStatusCode.ERROR,
1397
- message: err instanceof Error ? err.message : String(err)
1398
- });
1470
+ recordError(span, err);
1399
1471
  throw err;
1400
1472
  } finally {
1401
1473
  span.end();
1402
1474
  }
1403
1475
  }
1404
- function expressRouteOf(req) {
1405
- const props = req;
1406
- const path = props?.route?.path;
1407
- if (typeof path !== "string" || path === "") return null;
1408
- const base = typeof props?.baseUrl === "string" ? props.baseUrl : "";
1409
- const route = base + path;
1410
- return route.startsWith("/") ? route : null;
1476
+ function recordError(span, err) {
1477
+ const isErr = isError(err);
1478
+ const message = activeRedactor.text(isErr ? err.message : String(err));
1479
+ if (isErr) {
1480
+ span.addEvent("exception", {
1481
+ "exception.type": err.name,
1482
+ "exception.message": message,
1483
+ ...err.stack ? { "exception.stacktrace": activeRedactor.text(err.stack) } : {},
1484
+ ...breadcrumbEventAttributes(activeRedactor.text, err)
1485
+ });
1486
+ }
1487
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
1488
+ }
1489
+ function redactWithActive(attributes) {
1490
+ return activeRedactor(attributes);
1411
1491
  }
1412
1492
  function captureExpressRoute(_span, response) {
1413
1493
  if (!(response instanceof ServerResponse)) return;
1414
- const rpcMetadata = getRPCMetadata(context.active());
1415
- if (rpcMetadata?.type !== RPCType.HTTP) return;
1494
+ const rpcMetadata = getRPCMetadata2(context2.active());
1495
+ if (rpcMetadata?.type !== RPCType2.HTTP) return;
1416
1496
  const setRoute = () => {
1417
1497
  if (rpcMetadata.route) return;
1418
1498
  const route = expressRouteOf(response.req);
@@ -1420,6 +1500,21 @@ function captureExpressRoute(_span, response) {
1420
1500
  };
1421
1501
  response.prependListener("finish", setRoute);
1422
1502
  response.prependListener("close", setRoute);
1503
+ if (hookRequests) startHookRequest(response);
1504
+ }
1505
+ var hookRequests = false;
1506
+ function structuredErrorAttributes(error) {
1507
+ const attrs = { ...errorAttributes(error, { causes: false }) };
1508
+ const internal = errorInternal(error);
1509
+ if (internal) {
1510
+ try {
1511
+ attrs["autter.error.internal"] = JSON.stringify(
1512
+ activeRedactor(internal)
1513
+ ).slice(0, 4e3);
1514
+ } catch {
1515
+ }
1516
+ }
1517
+ return attrs;
1423
1518
  }
1424
1519
  var active = null;
1425
1520
  var activeAlwaysOnProvider = null;
@@ -1431,22 +1526,41 @@ function initAutterServer(options) {
1431
1526
  ""
1432
1527
  );
1433
1528
  const headers = { authorization: `Bearer ${options.apiKey}` };
1529
+ const traceExporter = () => new CountingExporter(
1530
+ new RedactingSpanExporter(
1531
+ new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers }),
1532
+ () => activeRedactor
1533
+ )
1534
+ );
1434
1535
  const environment = options.environment ?? process.env.NODE_ENV ?? "production";
1435
1536
  if (isDebugEnabled() || options.debug === true) setDebugMode(true);
1436
1537
  debugLog(`initialising service=${options.service} endpoint=${endpoint}`);
1437
1538
  activeRedactor = makeRedactor(options.redactAttributes ?? true);
1438
- const resource = new Resource({
1539
+ const distro = options.distro ?? SDK_IDENTITY;
1540
+ configureCompatCheck({ endpoint, enabled: options.compatCheck !== false, sdk: distro });
1541
+ const resource = new Resource2({
1439
1542
  [ATTR_SERVICE_NAME]: options.service,
1440
- "service.instance.id": (options.instanceId || process.env.AUTTER_RUNTIME_INSTANCE_ID || randomUUID2()).slice(0, 128),
1543
+ "service.instance.id": (options.instanceId || process.env.AUTTER_RUNTIME_INSTANCE_ID || randomUUID()).slice(0, 128),
1441
1544
  ...options.release ? { [ATTR_SERVICE_VERSION]: options.release } : {},
1442
1545
  "deployment.environment": environment,
1546
+ // OTel semconv for SDK distributions: the ingester records which
1547
+ // Autter SDK version each service runs (version compatibility checks).
1548
+ "telemetry.distro.name": distro.name,
1549
+ "telemetry.distro.version": distro.version,
1443
1550
  // Tells the ingester request metrics arrive on the metrics pipe, so
1444
1551
  // it must not also fold our server spans into usage rollups (that
1445
1552
  // would double-count every sampled request).
1446
1553
  "autter.metrics_wired": true
1447
1554
  });
1555
+ const rootSampler = new TraceIdRatioBasedSampler(options.traceSampleRate ?? 0.01);
1448
1556
  const headSampler = new ParentBasedSampler({
1449
- root: new TraceIdRatioBasedSampler(options.traceSampleRate ?? 0.01)
1557
+ root: rootSampler,
1558
+ // Browsers (runtime-browser's traceparent) send unsampled parents: they
1559
+ // make no sampling decision. Apply this service's own rate to such
1560
+ // requests instead of never head-sampling them. The ratio sampler is
1561
+ // deterministic by trace id, so services sharing a rate still agree
1562
+ // with an upstream service that chose not to sample.
1563
+ remoteParentNotSampled: rootSampler
1450
1564
  });
1451
1565
  const retainOnError = options.retainTracesOnError !== false;
1452
1566
  const slowThresholdMs = Number.isFinite(options.retainTracesAboveMs) ? Math.max(0, options.retainTracesAboveMs) : 0;
@@ -1454,30 +1568,24 @@ function initAutterServer(options) {
1454
1568
  let sampler = retainTraces ? new RecordUnsampledSampler(headSampler) : headSampler;
1455
1569
  if (options.llmTracing !== false) sampler = new LlmAwareSampler(sampler);
1456
1570
  const errorTraceBuffer = retainTraces ? new ErrorTraceRetentionProcessor(
1457
- new BatchSpanProcessor(
1458
- new CountingExporter(
1459
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1460
- ),
1461
- { scheduledDelayMillis: 2e3 }
1462
- ),
1571
+ new BatchSpanProcessor(traceExporter(), { scheduledDelayMillis: 2e3 }),
1463
1572
  retainOnError,
1464
1573
  slowThresholdMs
1465
1574
  ) : null;
1466
- const mainSpanProcessor = new BatchSpanProcessor(
1467
- new CountingExporter(
1468
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1469
- )
1470
- );
1575
+ const mainSpanProcessor = new BatchSpanProcessor(traceExporter());
1471
1576
  const metricReader = new PeriodicExportingMetricReader({
1472
- exporter: new OTLPMetricExporter({
1473
- url: `${endpoint}/v1/metrics`,
1474
- headers,
1475
- // Deltas, not lifetime totals: the ingester SUMs data points
1476
- // into runtime_metrics_1m, and the default (cumulative)
1477
- // temporality would re-count every past request on each
1478
- // 60 s export.
1479
- temporalityPreference: AggregationTemporalityPreference.DELTA
1480
- }),
1577
+ exporter: new RedactingMetricExporter(
1578
+ new OTLPMetricExporter({
1579
+ url: `${endpoint}/v1/metrics`,
1580
+ headers,
1581
+ // Deltas, not lifetime totals: the ingester SUMs data points
1582
+ // into runtime_metrics_1m, and the default (cumulative)
1583
+ // temporality would re-count every past request on each
1584
+ // 60 s export.
1585
+ temporalityPreference: AggregationTemporalityPreference.DELTA
1586
+ }),
1587
+ () => activeRedactor
1588
+ ),
1481
1589
  exportIntervalMillis: options.metricIntervalMs ?? 6e4
1482
1590
  });
1483
1591
  const sdk = new NodeSDK({
@@ -1503,16 +1611,14 @@ function initAutterServer(options) {
1503
1611
  });
1504
1612
  sdk.start();
1505
1613
  const stopMemoryMetrics = options.memoryMetrics === false ? null : startMemoryMetrics();
1614
+ noteCompatFeature("endpoint_latency");
1615
+ if (stopMemoryMetrics) noteCompatFeature("memory_metrics");
1616
+ if (options.llmTracing !== false) noteCompatFeature("llm_calls");
1506
1617
  const alwaysOnProvider = new BasicTracerProvider({
1507
1618
  resource,
1508
1619
  sampler: new AlwaysOnSampler(),
1509
1620
  spanProcessors: [
1510
- new BatchSpanProcessor(
1511
- new CountingExporter(
1512
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1513
- ),
1514
- { scheduledDelayMillis: 2e3 }
1515
- )
1621
+ new BatchSpanProcessor(traceExporter(), { scheduledDelayMillis: 2e3 })
1516
1622
  ]
1517
1623
  });
1518
1624
  const errorTracer = alwaysOnProvider.getTracer("autter-errors");
@@ -1521,53 +1627,73 @@ function initAutterServer(options) {
1521
1627
  function captureException2(error, attributes) {
1522
1628
  errorTraceBuffer?.retainActiveTrace();
1523
1629
  telemetryStats.markCaptured();
1524
- const isError = error instanceof Error;
1525
- const message = isError ? error.message : String(error);
1526
- const redacted = activeRedactor(attributes);
1630
+ const isErr = isError(error);
1631
+ const message = activeRedactor.text(isErr ? error.message : String(error));
1632
+ const stackText = isErr && error.stack ? activeRedactor.text(error.stack) : "";
1633
+ const split = splitFingerprint(attributes);
1634
+ const fingerprint = fingerprintEventAttributes(split.fingerprint, activeRedactor.text);
1635
+ if (fingerprint["autter.fingerprint"]) noteCompatFeature("fingerprint_override");
1636
+ const redacted = activeRedactor(split.attributes);
1527
1637
  const activeSpan = trace2.getActiveSpan();
1528
1638
  const reuseActive = activeSpan?.isRecording() === true;
1529
- const span = reuseActive ? activeSpan : errorTracer.startSpan(isError ? error.name : "Error");
1530
- span.setAttributes({ "autter.severity": "error", ...redacted });
1531
- if (isError && error.stack) {
1532
- if (redacted["autter.handled"] === true) {
1533
- span.addEvent("exception", {
1534
- "exception.type": error.name,
1535
- "exception.message": message,
1536
- "exception.stacktrace": error.stack,
1537
- "autter.handled": true,
1538
- "autter.sampled": redacted["autter.sampled"] === true
1539
- });
1540
- } else {
1541
- span.recordException(error);
1542
- }
1639
+ const span = reuseActive ? activeSpan : errorTracer.startSpan(isErr ? error.name : "Error");
1640
+ const structured = structuredErrorAttributes(error);
1641
+ const causes = errorAttributes(error);
1642
+ const declared = {
1643
+ ...Object.fromEntries(Object.entries(causes).filter(([key]) => key !== "autter.error.internal")),
1644
+ ...fingerprint
1645
+ };
1646
+ const requestId = currentOperation()?.requestId;
1647
+ span.setAttributes({
1648
+ "autter.severity": "error",
1649
+ ...redacted,
1650
+ ...structured,
1651
+ ...requestId ? { "autter.request.id": requestId } : {}
1652
+ });
1653
+ markCaptured(error);
1654
+ noteOperationError(error);
1655
+ notifyException(error, { ...redacted, ...fingerprint });
1656
+ if (isErr && stackText) {
1657
+ span.addEvent("exception", {
1658
+ "exception.type": error.name,
1659
+ "exception.message": message,
1660
+ "exception.stacktrace": stackText,
1661
+ ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1662
+ ...declared,
1663
+ ...breadcrumbEventAttributes(activeRedactor.text, error)
1664
+ });
1543
1665
  } else {
1544
1666
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
1545
1667
  span.addEvent("exception", {
1546
- "exception.type": isError ? error.name : "Error",
1668
+ "exception.type": isErr ? error.name : "Error",
1547
1669
  "exception.message": message,
1548
- ...stack ? { "exception.stacktrace": stack } : {},
1549
- ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {}
1670
+ ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1671
+ ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1672
+ ...declared,
1673
+ ...breadcrumbEventAttributes(activeRedactor.text, error)
1550
1674
  });
1551
1675
  }
1552
1676
  span.setStatus({ code: SpanStatusCode.ERROR, message });
1553
1677
  if (!reuseActive) span.end();
1554
1678
  }
1555
- function captureMessage2(message, severity2 = "warning", attributes) {
1556
- if (severity2 === "error" || severity2 === "fatal") {
1679
+ function captureMessage2(message, severity = "warning", attributes) {
1680
+ if (severity === "error" || severity === "fatal") {
1557
1681
  errorTraceBuffer?.retainActiveTrace();
1558
1682
  }
1559
1683
  telemetryStats.markCaptured();
1560
1684
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureMessage")).join("\n");
1685
+ const safeMessage = activeRedactor.text(message);
1561
1686
  const span = errorTracer.startSpan("Message", {
1562
- attributes: { "autter.severity": severity2, ...activeRedactor(attributes) }
1687
+ attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
1563
1688
  });
1564
1689
  span.addEvent("exception", {
1565
1690
  "exception.type": "Message",
1566
- "exception.message": message,
1567
- ...stack ? { "exception.stacktrace": stack } : {},
1568
- "autter.severity": severity2
1691
+ "exception.message": safeMessage,
1692
+ ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1693
+ "autter.severity": severity,
1694
+ ...breadcrumbEventAttributes(activeRedactor.text)
1569
1695
  });
1570
- span.setStatus({ code: SpanStatusCode.ERROR, message });
1696
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1571
1697
  span.end();
1572
1698
  }
1573
1699
  function reportOutcome2(name, message, attributes) {
@@ -1593,18 +1719,29 @@ function initAutterServer(options) {
1593
1719
  apiKey: options.apiKey,
1594
1720
  service: options.service,
1595
1721
  environment,
1722
+ distro,
1596
1723
  release: options.release,
1597
1724
  options: options.logging,
1598
1725
  redact: activeRedactor,
1599
- run: (name, fn, attributes) => runWithSpan(processTracer, name, fn, activeRedactor(attributes)),
1726
+ run: (name, fn, attributes, links) => runWithSpan(processTracer, name, fn, activeRedactor(attributes), links),
1600
1727
  reportOutcome: reportOutcome2
1601
1728
  });
1729
+ hookRequests = options.logging?.requests === true;
1730
+ let onCrash = null;
1731
+ let uninstallRejections = null;
1602
1732
  if (options.captureGlobalErrors !== false) {
1603
- process.on("uncaughtExceptionMonitor", (err) => {
1733
+ onCrash = (err) => {
1734
+ if (recordedAsRejection(err)) return;
1604
1735
  captureException2(err, { "autter.unhandled": true });
1605
1736
  if (options.autoFlush === false) void Promise.resolve(flushTarget.forceFlush()).catch(() => {
1606
1737
  });
1607
- });
1738
+ };
1739
+ process.on("uncaughtExceptionMonitor", onCrash);
1740
+ if (options.captureUnhandledRejections !== false) {
1741
+ uninstallRejections = installUnhandledRejectionCapture(
1742
+ (reason) => captureException2(reason, { "autter.unhandled_rejection": true })
1743
+ );
1744
+ }
1608
1745
  }
1609
1746
  const flushTarget = {
1610
1747
  forceFlush: async () => {
@@ -1637,6 +1774,9 @@ function initAutterServer(options) {
1637
1774
  trackLlmCall: (call) => recordLlmCall(llmTracer, call),
1638
1775
  shutdown: async () => {
1639
1776
  const logShutdown = await Promise.allSettled([shutdownRuntimeLogger()]);
1777
+ hookRequests = false;
1778
+ if (onCrash) process.off("uncaughtExceptionMonitor", onCrash);
1779
+ uninstallRejections?.();
1640
1780
  stopMemoryMetrics?.();
1641
1781
  active = null;
1642
1782
  activeAlwaysOnProvider = null;
@@ -1657,17 +1797,41 @@ function captureException(error, attributes) {
1657
1797
  active.captureException(error, attributes);
1658
1798
  return;
1659
1799
  }
1660
- const span = trace2.getTracer("autter-errors").startSpan(error instanceof Error ? error.name : "Error", { attributes });
1661
- if (error instanceof Error) span.recordException(error);
1662
- span.setStatus({
1663
- code: SpanStatusCode.ERROR,
1664
- message: error instanceof Error ? error.message : String(error)
1800
+ if (loggerOnlyCapture) {
1801
+ loggerOnlyCapture(error, attributes);
1802
+ return;
1803
+ }
1804
+ markCaptured(error);
1805
+ noteOperationError(error);
1806
+ const split = splitFingerprint(attributes);
1807
+ const fingerprint = fingerprintEventAttributes(split.fingerprint, activeRedactor.text);
1808
+ notifyException(error, { ...split.attributes, ...fingerprint });
1809
+ const span = trace2.getTracer("autter-errors").startSpan(isError(error) ? error.name : "Error", {
1810
+ attributes: { ...activeRedactor(split.attributes), ...fingerprint }
1665
1811
  });
1812
+ recordError(span, error);
1666
1813
  span.end();
1667
1814
  }
1815
+ var loggerOnlyCapture = null;
1816
+ function setLoggerOnlyCapture(fn) {
1817
+ loggerOnlyCapture = fn;
1818
+ }
1819
+ function serverActive() {
1820
+ return active !== null;
1821
+ }
1822
+ setRequestExceptionCapturer((error, attributes) => captureException(error, attributes));
1823
+ setRequestSpanAnnotator((attributes) => {
1824
+ const span = trace2.getActiveSpan();
1825
+ if (span?.isRecording()) span.setAttributes(activeRedactor(splitFingerprint(attributes).attributes));
1826
+ });
1668
1827
  function withProcessSpan(name, fn, attributes) {
1669
1828
  if (active) return active.withProcessSpan(name, fn, attributes);
1670
- return runWithSpan(trace2.getTracer("autter-processes"), name, fn, attributes);
1829
+ return runWithSpan(
1830
+ trace2.getTracer("autter-processes"),
1831
+ name,
1832
+ fn,
1833
+ activeRedactor(attributes)
1834
+ );
1671
1835
  }
1672
1836
  function withLlmCall(info, fn) {
1673
1837
  if (active) return active.withLlmCall(info, fn);
@@ -1680,20 +1844,22 @@ function trackLlmCall(call) {
1680
1844
  }
1681
1845
  recordLlmCall(trace2.getTracer("autter-llm"), call);
1682
1846
  }
1683
- function captureMessage(message, severity2 = "warning", attributes) {
1847
+ function captureMessage(message, severity = "warning", attributes) {
1684
1848
  if (active) {
1685
- active.captureMessage(message, severity2, attributes);
1849
+ active.captureMessage(message, severity, attributes);
1686
1850
  return;
1687
1851
  }
1852
+ const safeMessage = activeRedactor.text(message);
1688
1853
  const span = trace2.getTracer("autter-errors").startSpan("Message", {
1689
- attributes: { "autter.severity": severity2, ...attributes }
1854
+ attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
1690
1855
  });
1691
1856
  span.addEvent("exception", {
1692
1857
  "exception.type": "Message",
1693
- "exception.message": message,
1694
- "autter.severity": severity2
1858
+ "exception.message": safeMessage,
1859
+ "autter.severity": severity,
1860
+ ...breadcrumbEventAttributes(activeRedactor.text)
1695
1861
  });
1696
- span.setStatus({ code: SpanStatusCode.ERROR, message });
1862
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1697
1863
  span.end();
1698
1864
  }
1699
1865
  function reportOutcome(name, message, attributes) {
@@ -1715,7 +1881,7 @@ function makeSafeCapture(options) {
1715
1881
  const clean = (attributes) => attributes ? { ...redactAttributes(attributes, options) } : void 0;
1716
1882
  return {
1717
1883
  captureException: (error, attributes) => captureException(error, clean(attributes)),
1718
- captureMessage: (message, severity2, attributes) => captureMessage(message, severity2 ?? "warning", clean(attributes))
1884
+ captureMessage: (message, severity, attributes) => captureMessage(message, severity ?? "warning", clean(attributes))
1719
1885
  };
1720
1886
  }
1721
1887
  function alwaysOnTracer(name) {
@@ -1787,15 +1953,26 @@ function extractUsage(result) {
1787
1953
  const r = result;
1788
1954
  const usage = r?.usage ?? r?.message?.usage;
1789
1955
  const meta = r?.usageMetadata;
1956
+ const details = usage?.prompt_tokens_details;
1957
+ const cached = usage?.cache_read_input_tokens ?? (typeof details?.cached_tokens === "number" ? details.cached_tokens : void 0) ?? meta?.cachedContentTokenCount;
1790
1958
  const input = usage?.prompt_tokens ?? usage?.input_tokens ?? meta?.promptTokenCount ?? void 0;
1791
1959
  const output = usage?.completion_tokens ?? usage?.output_tokens ?? meta?.candidatesTokenCount ?? void 0;
1792
1960
  const responseModel = r?.model ?? r?.modelVersion;
1793
1961
  return {
1794
1962
  inputTokens: typeof input === "number" ? input : void 0,
1795
1963
  outputTokens: typeof output === "number" ? output : void 0,
1964
+ cacheReadTokens: typeof cached === "number" ? cached : void 0,
1796
1965
  responseModel: typeof responseModel === "string" ? responseModel : void 0
1797
1966
  };
1798
1967
  }
1968
+ function rollUp(model, usage) {
1969
+ recordOperationAiUsage({
1970
+ model: usage.responseModel ?? model,
1971
+ ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
1972
+ ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
1973
+ ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {}
1974
+ });
1975
+ }
1799
1976
  function applyUsage(span, usage) {
1800
1977
  if (usage.inputTokens !== void 0) {
1801
1978
  span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
@@ -1808,14 +1985,10 @@ function applyUsage(span, usage) {
1808
1985
  }
1809
1986
  }
1810
1987
  function endWithError(span, err) {
1811
- if (err instanceof Error) span.recordException(err);
1812
- span.setStatus({
1813
- code: SpanStatusCode2.ERROR,
1814
- message: err instanceof Error ? err.message : String(err)
1815
- });
1988
+ recordError(span, err);
1816
1989
  span.end();
1817
1990
  }
1818
- function wrapStream(stream, span) {
1991
+ function wrapStream(stream, span, model) {
1819
1992
  let ended = false;
1820
1993
  const seen = {
1821
1994
  inputTokens: void 0,
@@ -1826,6 +1999,7 @@ function wrapStream(stream, span) {
1826
1999
  if (ended) return;
1827
2000
  ended = true;
1828
2001
  applyUsage(span, seen);
2002
+ rollUp(model, seen);
1829
2003
  span.setAttribute("autter.llm.streamed", true);
1830
2004
  if (err !== void 0) {
1831
2005
  endWithError(span, err);
@@ -1847,6 +2021,7 @@ function wrapStream(stream, span) {
1847
2021
  const usage = extractUsage(step.value);
1848
2022
  if (usage.inputTokens !== void 0) seen.inputTokens = usage.inputTokens;
1849
2023
  if (usage.outputTokens !== void 0) seen.outputTokens = usage.outputTokens;
2024
+ if (usage.cacheReadTokens !== void 0) seen.cacheReadTokens = usage.cacheReadTokens;
1850
2025
  if (usage.responseModel) seen.responseModel = usage.responseModel;
1851
2026
  } else {
1852
2027
  finish();
@@ -1857,9 +2032,9 @@ function wrapStream(stream, span) {
1857
2032
  throw err;
1858
2033
  }
1859
2034
  },
1860
- async return(value3) {
2035
+ async return(value2) {
1861
2036
  finish();
1862
- return inner.return ? inner.return(value3) : { done: true, value: void 0 };
2037
+ return inner.return ? inner.return(value2) : { done: true, value: void 0 };
1863
2038
  },
1864
2039
  async throw(err) {
1865
2040
  finish(err);
@@ -1869,8 +2044,8 @@ function wrapStream(stream, span) {
1869
2044
  };
1870
2045
  };
1871
2046
  }
1872
- const value2 = Reflect.get(target, prop, receiver);
1873
- return typeof value2 === "function" ? value2.bind(target) : value2;
2047
+ const value = Reflect.get(target, prop, receiver);
2048
+ return typeof value === "function" ? value.bind(target) : value;
1874
2049
  }
1875
2050
  });
1876
2051
  }
@@ -1892,7 +2067,7 @@ function instrumentLlmClient(client, options) {
1892
2067
  "gen_ai.system": provider,
1893
2068
  "gen_ai.request.model": request.model,
1894
2069
  ...userId ? { "autter.user_id": userId } : {},
1895
- ...options?.attributes
2070
+ ...redactWithActive(options?.attributes)
1896
2071
  }
1897
2072
  });
1898
2073
  let result;
@@ -1904,9 +2079,11 @@ function instrumentLlmClient(client, options) {
1904
2079
  }
1905
2080
  if (!(result instanceof Promise)) {
1906
2081
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
1907
- return wrapStream(result, span);
2082
+ return wrapStream(result, span, request.model);
1908
2083
  }
1909
- applyUsage(span, extractUsage(result));
2084
+ const usage = extractUsage(result);
2085
+ applyUsage(span, usage);
2086
+ rollUp(request.model, usage);
1910
2087
  span.setStatus({ code: SpanStatusCode2.OK });
1911
2088
  span.end();
1912
2089
  return result;
@@ -1914,9 +2091,11 @@ function instrumentLlmClient(client, options) {
1914
2091
  return result.then(
1915
2092
  (resolved) => {
1916
2093
  if (resolved && typeof resolved === "object" && Symbol.asyncIterator in resolved) {
1917
- return wrapStream(resolved, span);
2094
+ return wrapStream(resolved, span, request.model);
1918
2095
  }
1919
- applyUsage(span, extractUsage(resolved));
2096
+ const usage = extractUsage(resolved);
2097
+ applyUsage(span, usage);
2098
+ rollUp(request.model, usage);
1920
2099
  span.setStatus({ code: SpanStatusCode2.OK });
1921
2100
  span.end();
1922
2101
  return resolved;
@@ -1932,30 +2111,30 @@ function instrumentLlmClient(client, options) {
1932
2111
  if (cached) return cached;
1933
2112
  const proxy = new Proxy(target, {
1934
2113
  get(t, prop, receiver) {
1935
- const value2 = Reflect.get(t, prop, receiver);
1936
- if (typeof prop === "symbol") return value2;
2114
+ const value = Reflect.get(t, prop, receiver);
2115
+ if (typeof prop === "symbol") return value;
1937
2116
  const propPath = path ? `${path}.${prop}` : prop;
1938
- if (typeof value2 === "function") {
2117
+ if (typeof value === "function") {
1939
2118
  if (CALL_METHODS.has(prop)) {
1940
2119
  return (...args) => {
1941
2120
  const first = args[0];
1942
2121
  if (first && typeof first === "object" && typeof first.model === "string") {
1943
2122
  return traceCall(
1944
- value2,
2123
+ value,
1945
2124
  t,
1946
2125
  propPath,
1947
2126
  args
1948
2127
  );
1949
2128
  }
1950
- return value2.apply(t, args);
2129
+ return value.apply(t, args);
1951
2130
  };
1952
2131
  }
1953
- return value2.bind(t);
2132
+ return value.bind(t);
1954
2133
  }
1955
- if (value2 !== null && typeof value2 === "object" && !(value2 instanceof Promise)) {
1956
- return deepProxy(value2, propPath);
2134
+ if (value !== null && typeof value === "object" && !(value instanceof Promise)) {
2135
+ return deepProxy(value, propPath);
1957
2136
  }
1958
- return value2;
2137
+ return value;
1959
2138
  }
1960
2139
  });
1961
2140
  proxied.set(target, proxy);
@@ -2007,26 +2186,300 @@ ${frames.join("\n")}`;
2007
2186
  });
2008
2187
  } };
2009
2188
  }
2189
+
2190
+ // src/framework-errors.ts
2191
+ function recordBoundaryError(error, req, rule) {
2192
+ try {
2193
+ const state = requestStateOf(req);
2194
+ if (state) noteOperationError(error, false, state);
2195
+ captureAtBoundary(error, state, rule);
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ function autterErrorHandler(options = {}) {
2200
+ return function autterErrorHandlerMiddleware(error, req, _res, next) {
2201
+ recordBoundaryError(error, req, options.capture);
2202
+ next(error);
2203
+ };
2204
+ }
2205
+ function autterFastifyErrors(fastify, options, done) {
2206
+ const rule = options?.capture;
2207
+ fastify.addHook("onError", ((request, _reply, error, next) => {
2208
+ recordBoundaryError(error, request?.raw, rule);
2209
+ next();
2210
+ }));
2211
+ done?.();
2212
+ }
2213
+ autterFastifyErrors[/* @__PURE__ */ Symbol.for("skip-override")] = true;
2214
+ autterFastifyErrors[/* @__PURE__ */ Symbol.for("fastify.display-name")] = "autter-runtime-errors";
2215
+ function autterKoa(options = {}) {
2216
+ return async function autterKoaMiddleware(ctx, next) {
2217
+ try {
2218
+ await next();
2219
+ } catch (error) {
2220
+ recordBoundaryError(error, ctx?.req, options.capture);
2221
+ throw error;
2222
+ }
2223
+ };
2224
+ }
2225
+
2226
+ // src/logging-only.ts
2227
+ import {
2228
+ SpanStatusCode as SpanStatusCode3,
2229
+ trace as trace3
2230
+ } from "@opentelemetry/api";
2231
+ var activeLogging = null;
2232
+ function initAutterLogging(options) {
2233
+ if (activeLogging) return activeLogging;
2234
+ if (serverActive()) {
2235
+ console.warn(
2236
+ "[autter-runtime] initAutterLogging() ignored: initAutterServer() is already active and includes logging"
2237
+ );
2238
+ return {
2239
+ captureException,
2240
+ flush: flushRuntimeLogs,
2241
+ shutdown: async () => {
2242
+ }
2243
+ };
2244
+ }
2245
+ if (isDebugEnabled() || options.debug === true) setDebugMode(true);
2246
+ const endpoint = (options.endpoint ?? "https://otlp.autter.dev").replace(/\/$/, "");
2247
+ const environment = options.environment ?? process.env.NODE_ENV ?? "production";
2248
+ const redact = makeRedactor(options.redactAttributes ?? true);
2249
+ const mode = options.exceptions ?? "auto";
2250
+ debugLog(`initialising logger-only service=${options.service} endpoint=${endpoint}`);
2251
+ function captureException2(error, attributes) {
2252
+ markCaptured(error);
2253
+ const split = splitFingerprint(attributes);
2254
+ const fingerprint = fingerprintEventAttributes(split.fingerprint, redact.text);
2255
+ const redacted = redact(split.attributes);
2256
+ const span = trace3.getActiveSpan();
2257
+ if (mode === "auto" && span?.isRecording()) {
2258
+ noteOperationError(error);
2259
+ notifyException(error, { ...redacted, ...fingerprint });
2260
+ span.setAttributes({ "autter.severity": "error", ...redacted, ...structuredErrorAttributes(error) });
2261
+ const declared = { ...errorAttributes(error), ...fingerprint };
2262
+ if (isError(error)) {
2263
+ span.addEvent("exception", {
2264
+ "exception.type": error.name,
2265
+ "exception.message": error.message,
2266
+ ...error.stack ? { "exception.stacktrace": error.stack } : {},
2267
+ ...declared
2268
+ });
2269
+ } else {
2270
+ span.addEvent("exception", {
2271
+ "exception.type": "Error",
2272
+ "exception.message": String(error),
2273
+ ...declared
2274
+ });
2275
+ }
2276
+ span.setStatus({
2277
+ code: SpanStatusCode3.ERROR,
2278
+ message: isError(error) ? error.message : String(error)
2279
+ });
2280
+ return;
2281
+ }
2282
+ notifyException(error, { ...redacted, ...fingerprint });
2283
+ captureExceptionAsLog(error, { ...redacted, ...fingerprint });
2284
+ }
2285
+ const captured = /* @__PURE__ */ new WeakSet();
2286
+ configureRuntimeLogger({
2287
+ endpoint,
2288
+ apiKey: options.apiKey ?? "",
2289
+ service: options.service,
2290
+ environment,
2291
+ ...options.release ? { release: options.release } : {},
2292
+ ...options.logging ? { options: options.logging } : {},
2293
+ redact,
2294
+ reportOutcome: (name, message, attributes) => {
2295
+ runtimeLogger.error(message, {
2296
+ ...attributes,
2297
+ "autter.outcome.name": name.slice(0, 200),
2298
+ "autter.outcome.message": message.slice(0, 1e3)
2299
+ });
2300
+ },
2301
+ captureThrown: (error) => {
2302
+ if (error && typeof error === "object") {
2303
+ if (captured.has(error)) return;
2304
+ captured.add(error);
2305
+ }
2306
+ captureException2(error);
2307
+ }
2308
+ });
2309
+ setLoggerOnlyCapture(captureException2);
2310
+ const onCrash = (error) => {
2311
+ if (recordedAsRejection(error)) return;
2312
+ captureException2(error, { "autter.unhandled": true });
2313
+ };
2314
+ let uninstallRejections = null;
2315
+ if (options.captureGlobalErrors !== false) {
2316
+ process.on("uncaughtExceptionMonitor", onCrash);
2317
+ if (options.captureUnhandledRejections !== false)
2318
+ uninstallRejections = installUnhandledRejectionCapture(
2319
+ (reason) => captureException2(reason, { "autter.unhandled_rejection": true })
2320
+ );
2321
+ }
2322
+ registerFlushTarget("logging", { forceFlush: () => flushRuntimeLogs() });
2323
+ let autoFlushHandle = options.autoFlush === false ? null : installAutterAutoFlush();
2324
+ const handle = {
2325
+ captureException: captureException2,
2326
+ flush: flushRuntimeLogs,
2327
+ shutdown: async () => {
2328
+ process.off("uncaughtExceptionMonitor", onCrash);
2329
+ uninstallRejections?.();
2330
+ uninstallRejections = null;
2331
+ setLoggerOnlyCapture(null);
2332
+ autoFlushHandle?.dispose();
2333
+ autoFlushHandle = null;
2334
+ unregisterFlushTargets();
2335
+ activeLogging = null;
2336
+ await shutdownRuntimeLogger();
2337
+ }
2338
+ };
2339
+ activeLogging = handle;
2340
+ return handle;
2341
+ }
2342
+
2343
+ // src/enrichers.ts
2344
+ function headerOf(req, name) {
2345
+ const headers = req?.headers;
2346
+ if (!headers) return void 0;
2347
+ if (typeof headers.get === "function") {
2348
+ return headers.get(name) ?? void 0;
2349
+ }
2350
+ const value = headers[name.toLowerCase()];
2351
+ if (Array.isArray(value)) return value[0] === void 0 ? void 0 : String(value[0]);
2352
+ return value === void 0 || value === null ? void 0 : String(value);
2353
+ }
2354
+ function enrichUserAgent() {
2355
+ return (event, { req }) => {
2356
+ const ua = headerOf(req, "user-agent");
2357
+ if (!ua) return;
2358
+ const info = parseUserAgent(ua);
2359
+ if (info.browser) event.attributes["user_agent.browser"] = info.browser;
2360
+ if (info.os) event.attributes["user_agent.os"] = info.os;
2361
+ event.attributes["user_agent.device"] = info.device;
2362
+ event.attributes["user_agent.bot"] = info.bot;
2363
+ };
2364
+ }
2365
+ function enrichRequestSize() {
2366
+ return (event, { req, res }) => {
2367
+ const request = Number(headerOf(req, "content-length"));
2368
+ if (Number.isFinite(request) && request >= 0)
2369
+ event.attributes["http.request.body.size"] = request;
2370
+ const raw = res?.getHeader?.(
2371
+ "content-length"
2372
+ );
2373
+ const response = Number(raw);
2374
+ if (raw !== void 0 && Number.isFinite(response) && response >= 0)
2375
+ event.attributes["http.response.body.size"] = response;
2376
+ };
2377
+ }
2378
+ var COUNTRY_HEADERS = [
2379
+ "cf-ipcountry",
2380
+ "x-vercel-ip-country",
2381
+ "cloudfront-viewer-country",
2382
+ "x-country-code",
2383
+ "fastly-geo-country-code"
2384
+ ];
2385
+ function enrichEdgeGeo() {
2386
+ return (event, { req }) => {
2387
+ for (const name of COUNTRY_HEADERS) {
2388
+ const value = headerOf(req, name)?.trim().toUpperCase();
2389
+ if (value && /^[A-Z]{2}$/.test(value) && value !== "XX" && value !== "T1") {
2390
+ event.attributes["geo.country.iso_code"] = value;
2391
+ return;
2392
+ }
2393
+ }
2394
+ };
2395
+ }
2396
+ var REGION_VARS = [
2397
+ "AUTTER_REGION",
2398
+ "AWS_REGION",
2399
+ "VERCEL_REGION",
2400
+ "FLY_REGION",
2401
+ "RAILWAY_REPLICA_REGION",
2402
+ "GOOGLE_CLOUD_REGION",
2403
+ "CLOUD_RUN_REGION",
2404
+ "RENDER_REGION",
2405
+ "AZURE_REGION"
2406
+ ];
2407
+ var COMMIT_VARS = [
2408
+ "AUTTER_COMMIT",
2409
+ "GIT_SHA",
2410
+ "GIT_COMMIT",
2411
+ "COMMIT_SHA",
2412
+ "SOURCE_COMMIT",
2413
+ "VERCEL_GIT_COMMIT_SHA",
2414
+ "GITHUB_SHA",
2415
+ "RENDER_GIT_COMMIT",
2416
+ "RAILWAY_GIT_COMMIT_SHA",
2417
+ "CF_PAGES_COMMIT_SHA",
2418
+ "HEROKU_SLUG_COMMIT",
2419
+ "SOURCE_VERSION"
2420
+ ];
2421
+ function enrichDeployment(env = process.env) {
2422
+ const pick = (names) => names.map((name) => env[name]?.trim()).find((value) => value);
2423
+ const region = pick(REGION_VARS)?.slice(0, 64);
2424
+ const commit = pick(COMMIT_VARS)?.slice(0, 64);
2425
+ return (event) => {
2426
+ if (region) event.attributes["deployment.region"] = region;
2427
+ if (commit) event.attributes["deployment.commit"] = commit;
2428
+ };
2429
+ }
2010
2430
  export {
2431
+ CODE_PATTERN,
2432
+ COMPAT_MANIFEST,
2433
+ INGESTER_VERSION_HEADER,
2434
+ RuntimeError,
2435
+ SDK_IDENTITY,
2436
+ addBreadcrumb,
2437
+ autterErrorHandler,
2438
+ autterErrorResponse,
2439
+ autterFastify,
2440
+ autterFastifyErrors,
2441
+ autterKoa,
2442
+ autterRequests,
2011
2443
  captureException,
2012
2444
  captureMessage,
2445
+ captureRequestError,
2446
+ consoleSink,
2013
2447
  createBrowserRelayFetchHandler,
2014
2448
  createBrowserRelayHandler,
2015
2449
  createRuntimeLogger,
2450
+ defineRuntimeErrors,
2016
2451
  emitLlmSelftestTrace,
2452
+ enrichDeployment,
2453
+ enrichEdgeGeo,
2454
+ enrichRequestSize,
2455
+ enrichUserAgent,
2456
+ errorAttributes,
2457
+ evaluateCompat,
2458
+ fetchIngesterCompat,
2459
+ fileSink,
2017
2460
  flushRuntimeLogs,
2461
+ initAutterLogging,
2018
2462
  initAutterServer,
2019
2463
  installAutterAutoFlush,
2020
2464
  instrumentLlmClient,
2465
+ isRuntimeErrorLike,
2021
2466
  makeSafeCapture,
2467
+ otlpSink,
2022
2468
  redactAttributes,
2469
+ redactText,
2023
2470
  reportOutcome,
2471
+ runDoctor,
2472
+ runInBackground,
2473
+ runtimeContext,
2024
2474
  runtimeLogStats,
2025
2475
  runtimeLogger,
2026
2476
  sanitizeBrowserPayload,
2027
2477
  startCaughtExceptionSampler,
2478
+ toClientError,
2028
2479
  trackLlmCall,
2480
+ unhandledRejectionsMode,
2029
2481
  withLlmCall,
2030
2482
  withProcessSpan,
2031
- withRuntimeOperation
2483
+ withRuntimeOperation,
2484
+ withRuntimeRequest
2032
2485
  };