@autter/runtime-node 1.4.0 → 1.5.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,69 @@
1
+ import {
2
+ CODE_PATTERN,
3
+ DEFAULT_REQUEST_ID_HEADER,
4
+ RuntimeError,
5
+ captureExceptionAsLog,
6
+ compileIgnore,
7
+ configureRuntimeLogger,
8
+ consoleSink,
9
+ createRequestState,
10
+ createRuntimeLogger,
11
+ currentOperation,
12
+ defineRuntimeErrors,
13
+ emitSummary,
14
+ errorAttributes,
15
+ errorCodeOf,
16
+ errorInternal,
17
+ fileSink,
18
+ flushRuntimeLogs,
19
+ isRuntimeErrorLike,
20
+ local,
21
+ makeRedactor,
22
+ normalizeRoutePath,
23
+ noteOperationError,
24
+ notifyException,
25
+ otlpSink,
26
+ parseUserAgent,
27
+ recordOperationAiUsage,
28
+ redactAttributes,
29
+ redactText,
30
+ reportExplicitFailure,
31
+ requestOutcome,
32
+ resolveRequestId,
33
+ responseStatusOf,
34
+ runInBackground,
35
+ runtimeContext,
36
+ runtimeLogStats,
37
+ runtimeLogger,
38
+ shutdownRuntimeLogger,
39
+ toClientError,
40
+ withRuntimeOperation
41
+ } from "./chunk-3TJHUCZ6.js";
42
+ import {
43
+ runDoctor
44
+ } from "./chunk-ZS4WQJQR.js";
45
+ import {
46
+ COMPAT_MANIFEST,
47
+ CountingExporter,
48
+ INGESTER_VERSION_HEADER,
49
+ SDK_IDENTITY,
50
+ configureCompatCheck,
51
+ debugLog,
52
+ ensureCompatConfigured,
53
+ evaluateCompat,
54
+ featuresForBrowserEvents,
55
+ fetchIngesterCompat,
56
+ installAutterAutoFlush,
57
+ isDebugEnabled,
58
+ noteCompatFeature,
59
+ noteRelayRejection,
60
+ observeIngesterResponse,
61
+ registerFlushTarget,
62
+ setDebugMode,
63
+ telemetryStats,
64
+ unregisterFlushTargets
65
+ } from "./chunk-L7HWKMCZ.js";
66
+
1
67
  // src/relay.ts
2
68
  var IpWindow = class {
3
69
  constructor(limitPerMinute) {
@@ -17,8 +83,8 @@ var IpWindow = class {
17
83
  return entry.count <= this.limitPerMinute;
18
84
  }
19
85
  };
20
- function firstForwardedFor(value2) {
21
- const raw = Array.isArray(value2) ? value2[0] : value2;
86
+ function firstForwardedFor(value) {
87
+ const raw = Array.isArray(value) ? value[0] : value;
22
88
  return raw ? (raw.split(",")[0] ?? "").trim() : "";
23
89
  }
24
90
  var DEFAULT_ENDPOINT = "https://otlp.autter.dev";
@@ -36,7 +102,120 @@ var EVENT_TYPES = /* @__PURE__ */ new Set([
36
102
  "timing"
37
103
  ]);
38
104
  var SEVERITIES = /* @__PURE__ */ new Set(["fatal", "error", "warning", "info"]);
39
- function sanitizeBrowserPayload(raw) {
105
+ var CONTEXT_MAX_DEPTH = 6;
106
+ var CONTEXT_MAX_NODES = 256;
107
+ var CONTEXT_MAX_STRING = 4e3;
108
+ var CONTEXT_MAX_ARRAY = 100;
109
+ var CONTEXT_MAX_KEYS = 100;
110
+ function byteLength(text) {
111
+ return new TextEncoder().encode(text).length;
112
+ }
113
+ function boundContext(value) {
114
+ let nodes = 0;
115
+ const seen = /* @__PURE__ */ new WeakSet();
116
+ const walk = (v, depth) => {
117
+ if (v === null) return null;
118
+ const t = typeof v;
119
+ if (t === "string") return v.slice(0, CONTEXT_MAX_STRING);
120
+ if (t === "number" || t === "boolean") return v;
121
+ if (t !== "object") return void 0;
122
+ if (depth >= CONTEXT_MAX_DEPTH || nodes >= CONTEXT_MAX_NODES) return void 0;
123
+ const obj = v;
124
+ if (seen.has(obj)) return void 0;
125
+ seen.add(obj);
126
+ let isArr = false;
127
+ try {
128
+ isArr = Array.isArray(v);
129
+ } catch {
130
+ return void 0;
131
+ }
132
+ if (isArr) {
133
+ const arr = v;
134
+ const out2 = [];
135
+ let len = 0;
136
+ try {
137
+ len = arr.length;
138
+ } catch {
139
+ return out2;
140
+ }
141
+ for (let i = 0; i < len && i < CONTEXT_MAX_ARRAY; i++) {
142
+ if (nodes >= CONTEXT_MAX_NODES) break;
143
+ nodes++;
144
+ let el;
145
+ try {
146
+ el = walk(arr[i], depth + 1);
147
+ } catch {
148
+ el = void 0;
149
+ }
150
+ if (el !== void 0) out2.push(el);
151
+ }
152
+ return out2;
153
+ }
154
+ let keys;
155
+ try {
156
+ keys = Object.keys(obj);
157
+ } catch {
158
+ return void 0;
159
+ }
160
+ const out = {};
161
+ for (let i = 0; i < keys.length && i < CONTEXT_MAX_KEYS; i++) {
162
+ if (nodes >= CONTEXT_MAX_NODES) break;
163
+ const key = keys[i];
164
+ if (key === void 0) continue;
165
+ nodes++;
166
+ let child;
167
+ try {
168
+ child = walk(obj[key], depth + 1);
169
+ } catch {
170
+ child = void 0;
171
+ }
172
+ if (child !== void 0) out[key] = child;
173
+ }
174
+ return out;
175
+ };
176
+ let result;
177
+ try {
178
+ result = walk(value, 0);
179
+ } catch {
180
+ result = void 0;
181
+ }
182
+ return result && typeof result === "object" ? result : {};
183
+ }
184
+ async function readBodyBounded(request, maxBody) {
185
+ const body = request.body;
186
+ if (!body) {
187
+ const text = await request.text();
188
+ return byteLength(text) > maxBody ? { tooLarge: true } : { tooLarge: false, text };
189
+ }
190
+ const reader = body.getReader();
191
+ const chunks = [];
192
+ let total = 0;
193
+ for (; ; ) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+ if (!value) continue;
197
+ total += value.byteLength;
198
+ if (total > maxBody) {
199
+ void Promise.resolve().then(() => reader.cancel()).catch(() => {
200
+ });
201
+ return { tooLarge: true };
202
+ }
203
+ chunks.push(value);
204
+ }
205
+ const buf = new Uint8Array(total);
206
+ let offset = 0;
207
+ for (const chunk of chunks) {
208
+ buf.set(chunk, offset);
209
+ offset += chunk.byteLength;
210
+ }
211
+ return { tooLarge: false, text: new TextDecoder().decode(buf) };
212
+ }
213
+ function relayContext(raw, redactor) {
214
+ const bounded = boundContext(raw);
215
+ return Array.isArray(bounded) ? {} : { context: redactor(bounded) };
216
+ }
217
+ var defaultRelayRedactor = makeRedactor(true);
218
+ function sanitizeBrowserPayload(raw, redactor = defaultRelayRedactor) {
40
219
  if (typeof raw !== "object" || raw === null) return null;
41
220
  const p = raw;
42
221
  if (p.version !== 1) return null;
@@ -53,16 +232,16 @@ function sanitizeBrowserPayload(raw) {
53
232
  type: e.type,
54
233
  timestamp: e.timestamp,
55
234
  ...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) } : {},
235
+ message: typeof e.message === "string" ? redactor.text(e.message.slice(0, 4512)).slice(0, 4e3) : "",
236
+ ...typeof e.name === "string" ? { name: redactor.text(e.name.slice(0, 712)).slice(0, 200) } : {},
58
237
  ...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) } : {},
238
+ ...typeof e.stack === "string" ? { stack: redactor.text(e.stack.slice(0, 32512)).slice(0, 32e3) } : {},
60
239
  ...typeof e.errorType === "string" ? { errorType: e.errorType.slice(0, 200) } : {},
61
240
  ...typeof e.filename === "string" ? { filename: e.filename.split("?")[0].slice(0, 1e3) } : {},
62
241
  ...typeof e.line === "number" ? { line: e.line } : {},
63
242
  ...typeof e.column === "number" ? { column: e.column } : {},
64
243
  ...typeof e.route === "string" ? { route: e.route.split("?")[0].slice(0, 1e3) } : {},
65
- ...typeof e.context === "object" && e.context !== null ? { context: e.context } : {}
244
+ ...typeof e.context === "object" && e.context !== null ? relayContext(e.context, redactor) : {}
66
245
  });
67
246
  }
68
247
  return {
@@ -71,11 +250,16 @@ function sanitizeBrowserPayload(raw) {
71
250
  service: p.service.slice(0, 200),
72
251
  environment: p.environment.slice(0, 100),
73
252
  ...typeof p.release === "string" ? { release: p.release.slice(0, 200) } : {},
253
+ // @autter/runtime-browser version, for the ingester's compat records.
254
+ ...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
255
  events
75
256
  };
76
257
  }
77
258
  function forward(payload, opts) {
78
- const url = `${(opts.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "")}/v1/browser`;
259
+ const endpoint = (opts.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
260
+ const url = `${endpoint}/v1/browser`;
261
+ const compat = opts.compatCheck !== false;
262
+ if (compat) ensureCompatConfigured(endpoint);
79
263
  void fetch(url, {
80
264
  method: "POST",
81
265
  headers: {
@@ -84,6 +268,14 @@ function forward(payload, opts) {
84
268
  },
85
269
  body: JSON.stringify(payload),
86
270
  signal: AbortSignal.timeout(1e4)
271
+ }).then((response) => {
272
+ if (!compat) return;
273
+ const types = new Set(
274
+ (payload.events ?? []).map((e) => String(e.type))
275
+ );
276
+ observeIngesterResponse({ status: response.status, headers: response.headers });
277
+ for (const feature of featuresForBrowserEvents(types)) noteCompatFeature(feature);
278
+ noteRelayRejection(types, response.status, response.headers);
87
279
  }).catch((err) => {
88
280
  (opts.onError ?? ((e) => console.warn("autter relay forward failed:", e)))(
89
281
  err
@@ -92,6 +284,7 @@ function forward(payload, opts) {
92
284
  }
93
285
  function createBrowserRelayFetchHandler(opts) {
94
286
  const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY;
287
+ const redactor = opts.redact === void 0 || opts.redact === true ? defaultRelayRedactor : makeRedactor(opts.redact);
95
288
  const limiter = opts.perIpRateLimit === false ? null : new IpWindow(opts.perIpRateLimit ?? 120);
96
289
  return async (request) => {
97
290
  if (request.method !== "POST") {
@@ -105,21 +298,28 @@ function createBrowserRelayFetchHandler(opts) {
105
298
  });
106
299
  }
107
300
  }
108
- const text = await request.text();
109
- if (text.length > maxBody) {
301
+ let bounded;
302
+ try {
303
+ bounded = await readBodyBounded(request, maxBody);
304
+ } catch {
305
+ return new Response(JSON.stringify({ error: "invalid json" }), {
306
+ status: 400
307
+ });
308
+ }
309
+ if (bounded.tooLarge) {
110
310
  return new Response(JSON.stringify({ error: "payload too large" }), {
111
311
  status: 413
112
312
  });
113
313
  }
114
314
  let raw;
115
315
  try {
116
- raw = JSON.parse(text);
316
+ raw = JSON.parse(bounded.text);
117
317
  } catch {
118
318
  return new Response(JSON.stringify({ error: "invalid json" }), {
119
319
  status: 400
120
320
  });
121
321
  }
122
- const payload = sanitizeBrowserPayload(raw);
322
+ const payload = sanitizeBrowserPayload(raw, redactor);
123
323
  if (!payload) {
124
324
  return new Response(JSON.stringify({ error: "invalid payload" }), {
125
325
  status: 400
@@ -131,6 +331,7 @@ function createBrowserRelayFetchHandler(opts) {
131
331
  }
132
332
  function createBrowserRelayHandler(opts) {
133
333
  const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY;
334
+ const redactor = opts.redact === void 0 || opts.redact === true ? defaultRelayRedactor : makeRedactor(opts.redact);
134
335
  const limiter = opts.perIpRateLimit === false ? null : new IpWindow(opts.perIpRateLimit ?? 120);
135
336
  function respond(res, status, body) {
136
337
  res.statusCode = status;
@@ -142,7 +343,7 @@ function createBrowserRelayHandler(opts) {
142
343
  }
143
344
  }
144
345
  function handleParsed(raw, res) {
145
- const payload = sanitizeBrowserPayload(raw);
346
+ const payload = sanitizeBrowserPayload(raw, redactor);
146
347
  if (!payload) {
147
348
  respond(res, 400, { error: "invalid payload" });
148
349
  return;
@@ -204,9 +405,9 @@ function createBrowserRelayHandler(opts) {
204
405
 
205
406
  // src/server.ts
206
407
  import { ServerResponse } from "http";
207
- import { randomUUID as randomUUID2 } from "crypto";
408
+ import { randomUUID } from "crypto";
208
409
  import {
209
- context,
410
+ context as context2,
210
411
  trace as trace2,
211
412
  SpanKind,
212
413
  SpanStatusCode,
@@ -219,7 +420,7 @@ import {
219
420
  } from "@opentelemetry/exporter-metrics-otlp-http";
220
421
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
221
422
  import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
222
- import { Resource } from "@opentelemetry/resources";
423
+ import { Resource as Resource2 } from "@opentelemetry/resources";
223
424
  import { ExplicitBucketHistogramAggregation, PeriodicExportingMetricReader, View } from "@opentelemetry/sdk-metrics";
224
425
  import { NodeSDK } from "@opentelemetry/sdk-node";
225
426
  import {
@@ -235,50 +436,83 @@ import {
235
436
  ATTR_SERVICE_VERSION
236
437
  } from "@opentelemetry/semantic-conventions";
237
438
 
238
- // src/lifecycle.ts
439
+ // src/redact-exporter.ts
239
440
  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
- }
441
+ import { Resource } from "@opentelemetry/resources";
442
+ import {
443
+ AggregationTemporality
444
+ } from "@opentelemetry/sdk-metrics";
445
+ var scrubbedResources = /* @__PURE__ */ new WeakMap();
446
+ function redactResource(resource, redactor) {
447
+ const cached = scrubbedResources.get(resource);
448
+ if (cached) return cached;
449
+ const scrubbed = new Resource(redactor.exported(resource.attributes));
450
+ scrubbedResources.set(resource, scrubbed);
451
+ return scrubbed;
452
+ }
453
+ function redactSpan(span, redactor) {
454
+ const status = span.status.message ? { ...span.status, message: redactor.text(span.status.message) } : span.status;
455
+ return {
456
+ name: redactor.text(span.name),
457
+ kind: span.kind,
458
+ spanContext: () => span.spanContext(),
459
+ parentSpanId: span.parentSpanId,
460
+ startTime: span.startTime,
461
+ endTime: span.endTime,
462
+ status,
463
+ attributes: redactor.exported(span.attributes),
464
+ links: span.links.map(
465
+ (link) => link.attributes ? { ...link, attributes: redactor.exported(link.attributes) } : link
466
+ ),
467
+ events: span.events.map((event) => ({
468
+ ...event,
469
+ name: redactor.text(event.name),
470
+ ...event.attributes ? { attributes: redactor.exported(event.attributes) } : {}
471
+ })),
472
+ duration: span.duration,
473
+ ended: span.ended,
474
+ resource: redactResource(span.resource, redactor),
475
+ instrumentationLibrary: span.instrumentationLibrary,
476
+ droppedAttributesCount: span.droppedAttributesCount,
477
+ droppedEventsCount: span.droppedEventsCount,
478
+ droppedLinksCount: span.droppedLinksCount
479
+ };
251
480
  }
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) {
481
+ var RedactingSpanExporter = class {
482
+ constructor(inner, redactor) {
272
483
  this.inner = inner;
484
+ this.redactor = redactor;
273
485
  }
274
486
  inner;
487
+ redactor;
275
488
  export(spans, resultCallback) {
276
- this.inner.export(spans, (result) => {
277
- if (result.code === ExportResultCode.SUCCESS) {
278
- telemetryStats.onExported(spans.length);
489
+ const redactor = this.redactor();
490
+ let scrubbed = spans;
491
+ let failed = 0;
492
+ if (redactor.enabled) {
493
+ scrubbed = [];
494
+ for (const span of spans) {
495
+ try {
496
+ scrubbed.push(redactSpan(span, redactor));
497
+ } catch {
498
+ failed++;
499
+ }
279
500
  }
280
- resultCallback(result);
281
- });
501
+ }
502
+ if (!failed) {
503
+ this.inner.export(scrubbed, resultCallback);
504
+ return;
505
+ }
506
+ const error = new Error(`autter-runtime: ${failed} span(s) could not be redacted and were not exported`);
507
+ const fail = () => resultCallback({ code: ExportResultCode.FAILED, error });
508
+ if (!scrubbed.length) {
509
+ fail();
510
+ return;
511
+ }
512
+ this.inner.export(
513
+ scrubbed,
514
+ (result) => result.code === ExportResultCode.SUCCESS ? fail() : resultCallback(result)
515
+ );
282
516
  }
283
517
  shutdown() {
284
518
  return this.inner.shutdown();
@@ -287,394 +521,38 @@ var CountingExporter = class {
287
521
  return this.inner.forceFlush?.() ?? Promise.resolve();
288
522
  }
289
523
  };
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
- }
524
+ var RedactingMetricExporter = class {
525
+ constructor(inner, redactor) {
526
+ this.inner = inner;
527
+ this.redactor = redactor;
476
528
  }
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
- }
529
+ inner;
530
+ redactor;
531
+ export(metrics2, resultCallback) {
532
+ const redactor = this.redactor();
533
+ if (!redactor.enabled) {
534
+ this.inner.export(metrics2, resultCallback);
535
+ return;
482
536
  }
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;
537
+ let resource;
544
538
  try {
545
- item = value2[i];
546
- } catch {
547
- out.push(r.mask);
548
- continue;
549
- }
550
- out.push(redactValue(item, r, state, depth + 1));
551
- }
552
- if (length > limit) {
553
- out.push(r.mask);
554
- }
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);
539
+ resource = redactResource(metrics2.resource, redactor);
540
+ } catch (error) {
541
+ resultCallback({ code: ExportResultCode.FAILED, error });
542
+ return;
662
543
  }
663
- } catch {
664
- truncated = true;
544
+ this.inner.export({ ...metrics2, resource }, resultCallback);
545
+ }
546
+ forceFlush() {
547
+ return this.inner.forceFlush();
665
548
  }
666
- if (truncated) {
667
- out.__redaction_truncated__ = r.mask;
549
+ shutdown() {
550
+ return this.inner.shutdown();
668
551
  }
669
- return out;
670
- }
671
- function makeRedactor(options) {
672
- if (options === false) {
673
- return (attributes) => ({ ...attributes ?? {} });
552
+ selectAggregationTemporality(instrumentType) {
553
+ return this.inner.selectAggregationTemporality ? this.inner.selectAggregationTemporality(instrumentType) : AggregationTemporality.CUMULATIVE;
674
554
  }
675
- const r = compile(options === true ? void 0 : options);
676
- return (attributes) => redactWith(attributes, r);
677
- }
555
+ };
678
556
 
679
557
  // src/memory.ts
680
558
  import { PerformanceObserver } from "perf_hooks";
@@ -721,388 +599,381 @@ function startMemoryMetrics() {
721
599
  };
722
600
  }
723
601
 
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));
763
- }
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
- );
602
+ // src/requests.ts
603
+ import { AsyncResource } from "async_hooks";
604
+ import { context, trace } from "@opentelemetry/api";
605
+ var REQUEST_STATE = /* @__PURE__ */ Symbol.for("autter.runtime.request");
606
+ var BOUND = /* @__PURE__ */ Symbol.for("autter.runtime.bound");
607
+ var CAPTURED = /* @__PURE__ */ new WeakSet();
608
+ var capturer = () => {
609
+ };
610
+ function setRequestExceptionCapturer(fn) {
611
+ capturer = fn;
612
+ }
613
+ function markCaptured(error) {
614
+ if (error && typeof error === "object") CAPTURED.add(error);
615
+ }
616
+ function expressRouteOf(req) {
617
+ const props = req;
618
+ const path = props?.route?.path;
619
+ if (typeof path !== "string" || path === "") return null;
620
+ const base = typeof props?.baseUrl === "string" ? props.baseUrl : "";
621
+ const route = base + path;
622
+ return route.startsWith("/") ? route : null;
623
+ }
624
+ function defaultShouldCapture(error) {
625
+ return error instanceof RuntimeError || errorCodeOf(error) !== void 0 || responseStatusOf(error) >= 500;
626
+ }
627
+ function captureAtBoundary(error, state, rule = void 0) {
628
+ const wanted = typeof rule === "function" ? rule(error) : rule === void 0 ? defaultShouldCapture(error) : rule;
629
+ if (!wanted) return;
630
+ if (error && typeof error === "object" && CAPTURED.has(error)) return;
631
+ markCaptured(error);
632
+ try {
633
+ if (state && !state.sealed) local.run(state, () => capturer(error));
634
+ else capturer(error);
635
+ } catch {
636
+ }
637
+ }
638
+ function bindEmitter(emitter, state) {
639
+ const target = emitter;
640
+ if (!target || typeof target !== "object" || target[BOUND]) return;
641
+ target[BOUND] = true;
642
+ const wrappers = /* @__PURE__ */ new WeakMap();
643
+ const wrap = (listener) => {
644
+ if (typeof listener !== "function") return listener;
645
+ let wrapped = wrappers.get(listener);
646
+ if (!wrapped) {
647
+ wrapped = function(...args) {
648
+ return local.run(
649
+ state,
650
+ () => listener.apply(this, args)
651
+ );
652
+ };
653
+ wrappers.set(listener, wrapped);
774
654
  }
775
- return void 0;
655
+ return wrapped;
776
656
  };
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
- );
657
+ for (const method of [
658
+ "on",
659
+ "addListener",
660
+ "prependListener",
661
+ "once",
662
+ "prependOnceListener"
663
+ ]) {
664
+ const original = target[method];
665
+ if (typeof original !== "function") continue;
666
+ target[method] = function(event, listener) {
667
+ return original.call(this, event, wrap(listener));
668
+ };
669
+ }
670
+ for (const method of ["removeListener", "off"]) {
671
+ const original = target[method];
672
+ if (typeof original !== "function") continue;
673
+ target[method] = function(event, listener) {
674
+ const wrapped = typeof listener === "function" ? wrappers.get(listener) ?? listener : listener;
675
+ return original.call(this, event, wrapped);
676
+ };
677
+ }
678
+ }
679
+ function exposeHeaderValue(current, header) {
680
+ const text = Array.isArray(current) ? current.join(", ") : String(current ?? "");
681
+ const names = text.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
682
+ if (names.includes("*") || names.includes(header)) return null;
683
+ return names.length ? `${text}, ${header}` : header;
795
684
  }
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;
685
+ function exposeOnCors(res, header) {
686
+ const original = res.writeHead;
687
+ res.writeHead = function(...args) {
688
+ try {
689
+ const inline = args.slice(1).find(
690
+ (arg) => !!arg && typeof arg === "object" && !Array.isArray(arg)
691
+ );
692
+ const inlineKey = (name) => inline ? Object.keys(inline).find((key) => key.toLowerCase() === name) : void 0;
693
+ const cors = res.getHeader("access-control-allow-origin") !== void 0 || inlineKey("access-control-allow-origin") !== void 0;
694
+ if (cors) {
695
+ const key = inlineKey("access-control-expose-headers");
696
+ const value = exposeHeaderValue(
697
+ key && inline ? inline[key] : res.getHeader("access-control-expose-headers"),
698
+ header
699
+ );
700
+ if (value && key && inline) inline[key] = value;
701
+ else if (value) res.setHeader("access-control-expose-headers", value);
702
+ }
703
+ } catch {
802
704
  }
803
- return result;
705
+ return original.apply(this, args);
804
706
  };
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
707
  }
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))
708
+ function pathOf(req) {
709
+ const raw = req.originalUrl ?? req.url ?? "/";
710
+ return raw.split(/[?#]/)[0] || "/";
711
+ }
712
+ function finishRequest(state, input) {
713
+ if (state.sealed) return;
714
+ state.name = (input.name ?? `${input.method} ${input.route}`).slice(0, 200);
715
+ state.outcome = requestOutcome({
716
+ ...state.explicitOutcome ? { explicit: state.outcome } : {},
717
+ ...state.error !== void 0 ? { error: state.error } : {},
718
+ ...state.errorThrown ? { thrown: true } : {},
719
+ status: input.status,
720
+ aborted: input.aborted
721
+ });
722
+ const span = trace.getActiveSpan();
723
+ if (span?.isRecording()) span.setAttribute("autter.operation.outcome", state.outcome);
724
+ if (input.span) {
725
+ const remote = trace.wrapSpanContext(input.span);
726
+ context.with(
727
+ trace.setSpan(context.active(), remote),
728
+ () => reportExplicitFailure(state)
824
729
  );
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) };
730
+ } else reportExplicitFailure(state);
731
+ emitSummary(state, {
732
+ http: {
733
+ "http.request.method": input.method,
734
+ "http.route": input.route,
735
+ "http.response.status_code": input.status,
736
+ "autter.request.aborted": input.aborted
737
+ },
738
+ ...input.span ? { span: input.span } : {}
739
+ });
840
740
  }
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."))
741
+ function beginNodeRequest(req, res, options, route) {
742
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
743
+ const requestId = resolveRequestId(
744
+ options.trustRequestId === false ? void 0 : req.headers[header]
850
745
  );
851
- const attrs = safe({
852
- "autter.event.id": randomUUID(),
853
- ...extra,
854
- ...exceptions,
855
- ...operationAttributes(state),
856
- ...user
746
+ const method = (req.method ?? "GET").toUpperCase();
747
+ const path = pathOf(req);
748
+ const state = createRequestState({
749
+ name: `${method} ${normalizeRoutePath(path)}`,
750
+ requestId,
751
+ req,
752
+ res
857
753
  });
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 } : {}
868
- };
869
- if (config?.options?.console !== false)
870
- console.log(
871
- JSON.stringify({ ...attrs, level, message: record.body.stringValue })
872
- );
873
- if (!config) return;
874
- if (stopping) {
875
- dropped++;
876
- return;
754
+ req[REQUEST_STATE] = state;
755
+ try {
756
+ if (!res.headersSent) res.setHeader(header, requestId);
757
+ exposeOnCors(res, header);
758
+ } catch {
877
759
  }
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"
760
+ const span = trace.getActiveSpan();
761
+ if (span?.isRecording())
762
+ span.setAttributes({
763
+ "autter.request.id": requestId,
764
+ "autter.operation.kind": "request"
765
+ });
766
+ const spanContext = span?.spanContext();
767
+ bindEmitter(req, state);
768
+ bindEmitter(res, state);
769
+ let finished = false;
770
+ const done = () => {
771
+ finishRequest(state, {
772
+ method,
773
+ route: route?.() ?? expressRouteOf(req) ?? normalizeRoutePath(path),
774
+ status: res.statusCode,
775
+ aborted: !finished,
776
+ ...spanContext ? { span: spanContext } : {}
777
+ });
778
+ };
779
+ res.once("finish", () => {
780
+ finished = true;
781
+ done();
782
+ });
783
+ res.once("close", done);
784
+ return state;
785
+ }
786
+ function autterRequests(options = {}) {
787
+ const ignored = compileIgnore(options.ignore);
788
+ return function autterRequestsMiddleware(req, res, next) {
789
+ if (req[REQUEST_STATE] || ignored(pathOf(req))) {
790
+ next();
791
+ return;
792
+ }
793
+ const state = beginNodeRequest(req, res, options);
794
+ local.run(state, () => next());
795
+ };
796
+ }
797
+ function autterErrorResponse(options = {}) {
798
+ return function autterErrorResponseMiddleware(error, req, res, next) {
799
+ const state = req[REQUEST_STATE];
800
+ if (state) noteOperationError(error, true, state);
801
+ captureAtBoundary(error, state, options.capture);
802
+ if (res.headersSent) {
803
+ next(error);
804
+ return;
805
+ }
806
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
807
+ const inbound = req.headers?.[header];
808
+ const requestId = state?.requestId ?? (typeof inbound === "string" ? inbound.slice(0, 128) : void 0);
809
+ res.statusCode = responseStatusOf(error);
810
+ res.setHeader("content-type", "application/json; charset=utf-8");
811
+ res.end(JSON.stringify(toClientError(error, requestId)));
812
+ };
813
+ }
814
+ var RESOURCE = /* @__PURE__ */ Symbol.for("autter.runtime.resource");
815
+ function autterFastify(fastify, options, done) {
816
+ const opts = options ?? {};
817
+ const header = (opts.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
818
+ const ignored = compileIgnore(opts.ignore);
819
+ const stateOf = (request) => request[REQUEST_STATE];
820
+ fastify.addHook("onRequest", ((request, reply, next) => {
821
+ if (ignored(request.url) || request.raw[REQUEST_STATE]) {
822
+ next();
823
+ return;
824
+ }
825
+ const requestId = resolveRequestId(
826
+ opts.trustRequestId === false ? void 0 : request.headers[header]
883
827
  );
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
- ]
828
+ const method = String(request.method ?? "GET").toUpperCase();
829
+ const state = createRequestState({
830
+ name: `${method} ${normalizeRoutePath(request.url)}`,
831
+ requestId,
832
+ req: request.raw,
833
+ res: reply.raw
834
+ });
835
+ request[REQUEST_STATE] = state;
836
+ request.raw[REQUEST_STATE] = state;
837
+ reply.header(header, requestId);
838
+ const span = trace.getActiveSpan();
839
+ if (span?.isRecording())
840
+ span.setAttributes({
841
+ "autter.request.id": requestId,
842
+ "autter.operation.kind": "request"
939
843
  });
940
- let failure;
941
- for (let attempt = 0; attempt < 3; attempt++) {
942
- 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;
960
- } catch (error) {
961
- failure = error;
962
- }
963
- }
964
- inFlight = 0;
965
- if (failure) {
966
- failureStreak++;
967
- queue = [...batch, ...queue];
968
- queueBytes += inFlightBytes;
969
- inFlightBytes = 0;
970
- throw failure;
844
+ const spanContext = span?.spanContext();
845
+ bindEmitter(request.raw, state);
846
+ bindEmitter(reply.raw, state);
847
+ let finished = false;
848
+ const finish = (aborted) => finishRequest(state, {
849
+ method,
850
+ route: request.routeOptions?.url ?? request.routerPath ?? normalizeRoutePath(request.url),
851
+ status: reply.raw.statusCode || reply.statusCode,
852
+ aborted,
853
+ ...spanContext ? { span: spanContext } : {}
854
+ });
855
+ reply.raw.once("finish", () => {
856
+ finished = true;
857
+ finish(false);
858
+ });
859
+ reply.raw.once("close", () => finish(!finished));
860
+ local.run(state, () => {
861
+ request[RESOURCE] = new AsyncResource("autter-request");
862
+ next();
863
+ });
864
+ }));
865
+ fastify.addHook("preHandler", ((request, _reply, next) => {
866
+ const resource = request[RESOURCE];
867
+ if (resource) resource.runInAsyncScope(() => next());
868
+ else next();
869
+ }));
870
+ fastify.addHook("onSend", ((request, reply, payload, next) => {
871
+ try {
872
+ if (stateOf(request) && reply.getHeader("access-control-allow-origin") !== void 0) {
873
+ const value = exposeHeaderValue(reply.getHeader("access-control-expose-headers"), header);
874
+ if (value) reply.header("access-control-expose-headers", value);
971
875
  }
972
- inFlightBytes = 0;
973
- failureStreak = 0;
876
+ } catch {
974
877
  }
975
- })().finally(() => {
976
- flushing = null;
977
- scheduleFlush();
978
- });
979
- return flushing;
980
- }
981
- async function shutdownRuntimeLogger() {
982
- stopping = true;
983
- 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`
878
+ next(null, payload);
879
+ }));
880
+ fastify.addHook("onError", ((request, _reply, error, next) => {
881
+ const state = stateOf(request);
882
+ if (state) noteOperationError(error, true, state);
883
+ captureAtBoundary(error, state);
884
+ next();
885
+ }));
886
+ done();
887
+ }
888
+ autterFastify[/* @__PURE__ */ Symbol.for("skip-override")] = true;
889
+ autterFastify[/* @__PURE__ */ Symbol.for("fastify.display-name")] = "autter-runtime";
890
+ function withHeaders(response, header, requestId) {
891
+ const apply = (target) => {
892
+ target.headers.set(header, requestId);
893
+ if (target.headers.get("access-control-allow-origin") !== null) {
894
+ const value = exposeHeaderValue(
895
+ target.headers.get("access-control-expose-headers") ?? "",
896
+ header
990
897
  );
898
+ if (value) target.headers.set("access-control-expose-headers", value);
991
899
  }
992
- queue = [];
993
- queueBytes = 0;
994
- config = null;
995
- if (timer) clearTimeout(timer);
996
- timer = void 0;
997
- }
998
- }
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
- })
900
+ return target;
1013
901
  };
902
+ try {
903
+ return apply(response);
904
+ } catch {
905
+ try {
906
+ return apply(new Response(response.body, response));
907
+ } catch {
908
+ return response;
909
+ }
910
+ }
1014
911
  }
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: []
1029
- };
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));
1036
- }
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
- });
1060
- }
912
+ function withRuntimeRequest(handler, options = {}) {
913
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
914
+ const ignored = compileIgnore(options.ignore);
915
+ return async (request, ...rest) => {
916
+ let pathname = "/";
917
+ try {
918
+ pathname = new URL(request.url, "http://localhost").pathname;
919
+ } catch {
1061
920
  }
1062
- };
1063
- const run = () => local.run(state, async () => {
1064
- let thrown;
921
+ if (ignored(pathname)) return handler(request, ...rest);
922
+ const requestId = resolveRequestId(
923
+ options.trustRequestId === false ? void 0 : request.headers.get(header)
924
+ );
925
+ const method = String(request.method ?? "GET").toUpperCase();
926
+ const route = options.route ?? normalizeRoutePath(pathname);
927
+ const state = createRequestState({
928
+ name: `${method} ${options.name ?? route}`,
929
+ requestId,
930
+ req: request
931
+ });
932
+ const span = trace.getActiveSpan();
933
+ if (span?.isRecording())
934
+ span.setAttributes({
935
+ "autter.request.id": requestId,
936
+ "autter.operation.kind": "request"
937
+ });
938
+ const spanContext = span?.spanContext();
939
+ let status = 500;
1065
940
  try {
1066
- return await fn(operation);
1067
- } catch (error) {
1068
- state.outcome = "failed";
1069
- thrown = error;
1070
- throw error;
941
+ const response = await local.run(state, async () => {
942
+ try {
943
+ return await handler(request, ...rest);
944
+ } catch (error) {
945
+ noteOperationError(error, true, state);
946
+ captureAtBoundary(error, state, true);
947
+ throw error;
948
+ }
949
+ });
950
+ status = response.status;
951
+ return withHeaders(response, header, requestId);
1071
952
  } finally {
1072
- trace.getActiveSpan()?.setAttributes({
1073
- ...traceAttributes(state),
1074
- "autter.operation.outcome": state.outcome
953
+ const aborted = request.signal?.aborted === true;
954
+ finishRequest(state, {
955
+ method,
956
+ route,
957
+ name: `${method} ${options.name ?? route}`,
958
+ status,
959
+ aborted,
960
+ ...spanContext ? { span: spanContext } : {}
1075
961
  });
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
962
+ if (options.waitUntil) {
963
+ try {
964
+ options.waitUntil(flushRuntimeLogs().catch(() => {
965
+ }));
966
+ } catch {
1100
967
  }
1101
- );
1102
- state.sealed = true;
968
+ }
1103
969
  }
1104
- });
1105
- return config ? config.run(state.name, run, traceAttributes(state)) : run();
970
+ };
971
+ }
972
+ function startHookRequest(res) {
973
+ const req = res.req;
974
+ if (!req || req[REQUEST_STATE]) return;
975
+ const state = beginNodeRequest(req, res, {});
976
+ local.enterWith(state);
1106
977
  }
1107
978
 
1108
979
  // src/server.ts
@@ -1326,36 +1197,43 @@ async function runLlmSpan(tracer, info, fn) {
1326
1197
  kind: SpanKind.CLIENT,
1327
1198
  attributes: llmBaseAttributes(info)
1328
1199
  });
1329
- const setResponseModel = (model) => span.setAttribute("gen_ai.response.model", model);
1200
+ const usage = {};
1201
+ let responseModel;
1202
+ const setResponseModel = (model) => {
1203
+ responseModel = model;
1204
+ span.setAttribute("gen_ai.response.model", model);
1205
+ };
1330
1206
  const handle = {
1331
- setUsage: (usage) => span.setAttributes(usageAttributes(usage)),
1332
- setCost: (usd) => span.setAttribute("autter.llm.cost_usd", usd),
1207
+ setUsage: (next) => {
1208
+ Object.assign(usage, next);
1209
+ span.setAttributes(usageAttributes(next));
1210
+ },
1211
+ setCost: (usd) => {
1212
+ usage.costUsd = usd;
1213
+ span.setAttribute("autter.llm.cost_usd", usd);
1214
+ },
1333
1215
  setResponseModel,
1334
1216
  setModel: setResponseModel,
1335
- setAttributes: (attributes) => span.setAttributes(attributes)
1217
+ setAttributes: (attributes) => span.setAttributes(activeRedactor(attributes))
1336
1218
  };
1337
1219
  try {
1338
- const result = await context.with(
1339
- trace2.setSpan(context.active(), span),
1220
+ const result = await context2.with(
1221
+ trace2.setSpan(context2.active(), span),
1340
1222
  () => fn(handle)
1341
1223
  );
1342
1224
  span.setStatus({ code: SpanStatusCode.OK });
1343
1225
  return result;
1344
1226
  } 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
- });
1227
+ recordError(span, err);
1228
+ if (err instanceof Error) span.setAttribute("error.type", err.name);
1353
1229
  throw err;
1354
1230
  } finally {
1355
1231
  span.end();
1232
+ recordOperationAiUsage({ ...usage, model: responseModel ?? info.model });
1356
1233
  }
1357
1234
  }
1358
1235
  function recordLlmCall(tracer, call) {
1236
+ recordOperationAiUsage({ ...call, model: call.model });
1359
1237
  const durationMs = Math.max(0, call.durationMs ?? 0);
1360
1238
  const endTime = /* @__PURE__ */ new Date();
1361
1239
  const startTime = new Date(endTime.getTime() - durationMs);
@@ -1368,50 +1246,52 @@ function recordLlmCall(tracer, call) {
1368
1246
  }
1369
1247
  );
1370
1248
  if (call.error !== void 0 && call.error !== null) {
1249
+ recordError(span, call.error);
1371
1250
  if (call.error instanceof Error) {
1372
- span.recordException(call.error);
1373
1251
  span.setAttribute("error.type", call.error.name);
1374
1252
  }
1375
- span.setStatus({
1376
- code: SpanStatusCode.ERROR,
1377
- message: call.error instanceof Error ? call.error.message : String(call.error)
1378
- });
1379
1253
  } else {
1380
1254
  span.setStatus({ code: SpanStatusCode.OK });
1381
1255
  }
1382
1256
  span.end(endTime);
1383
1257
  }
1384
- async function runWithSpan(tracer, name, fn, attributes) {
1385
- const span = tracer.startSpan(name, { attributes });
1258
+ async function runWithSpan(tracer, name, fn, attributes, links) {
1259
+ const span = tracer.startSpan(name, {
1260
+ attributes,
1261
+ ...links?.length ? { links } : {}
1262
+ });
1386
1263
  try {
1387
- const result = await context.with(
1388
- trace2.setSpan(context.active(), span),
1264
+ const result = await context2.with(
1265
+ trace2.setSpan(context2.active(), span),
1389
1266
  fn
1390
1267
  );
1391
1268
  span.setStatus({ code: SpanStatusCode.OK });
1392
1269
  return result;
1393
1270
  } 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
- });
1271
+ recordError(span, err);
1399
1272
  throw err;
1400
1273
  } finally {
1401
1274
  span.end();
1402
1275
  }
1403
1276
  }
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;
1277
+ function recordError(span, err) {
1278
+ const isError = err instanceof Error;
1279
+ const message = activeRedactor.text(isError ? err.message : String(err));
1280
+ if (isError) {
1281
+ span.addEvent("exception", {
1282
+ "exception.type": err.name,
1283
+ "exception.message": message,
1284
+ ...err.stack ? { "exception.stacktrace": activeRedactor.text(err.stack) } : {}
1285
+ });
1286
+ }
1287
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
1288
+ }
1289
+ function redactWithActive(attributes) {
1290
+ return activeRedactor(attributes);
1411
1291
  }
1412
1292
  function captureExpressRoute(_span, response) {
1413
1293
  if (!(response instanceof ServerResponse)) return;
1414
- const rpcMetadata = getRPCMetadata(context.active());
1294
+ const rpcMetadata = getRPCMetadata(context2.active());
1415
1295
  if (rpcMetadata?.type !== RPCType.HTTP) return;
1416
1296
  const setRoute = () => {
1417
1297
  if (rpcMetadata.route) return;
@@ -1420,6 +1300,21 @@ function captureExpressRoute(_span, response) {
1420
1300
  };
1421
1301
  response.prependListener("finish", setRoute);
1422
1302
  response.prependListener("close", setRoute);
1303
+ if (hookRequests) startHookRequest(response);
1304
+ }
1305
+ var hookRequests = false;
1306
+ function structuredErrorAttributes(error) {
1307
+ const attrs = { ...errorAttributes(error, { causes: false }) };
1308
+ const internal = errorInternal(error);
1309
+ if (internal) {
1310
+ try {
1311
+ attrs["autter.error.internal"] = JSON.stringify(
1312
+ activeRedactor(internal)
1313
+ ).slice(0, 4e3);
1314
+ } catch {
1315
+ }
1316
+ }
1317
+ return attrs;
1423
1318
  }
1424
1319
  var active = null;
1425
1320
  var activeAlwaysOnProvider = null;
@@ -1431,15 +1326,27 @@ function initAutterServer(options) {
1431
1326
  ""
1432
1327
  );
1433
1328
  const headers = { authorization: `Bearer ${options.apiKey}` };
1329
+ const traceExporter = () => new CountingExporter(
1330
+ new RedactingSpanExporter(
1331
+ new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers }),
1332
+ () => activeRedactor
1333
+ )
1334
+ );
1434
1335
  const environment = options.environment ?? process.env.NODE_ENV ?? "production";
1435
1336
  if (isDebugEnabled() || options.debug === true) setDebugMode(true);
1436
1337
  debugLog(`initialising service=${options.service} endpoint=${endpoint}`);
1437
1338
  activeRedactor = makeRedactor(options.redactAttributes ?? true);
1438
- const resource = new Resource({
1339
+ const distro = options.distro ?? SDK_IDENTITY;
1340
+ configureCompatCheck({ endpoint, enabled: options.compatCheck !== false, sdk: distro });
1341
+ const resource = new Resource2({
1439
1342
  [ATTR_SERVICE_NAME]: options.service,
1440
- "service.instance.id": (options.instanceId || process.env.AUTTER_RUNTIME_INSTANCE_ID || randomUUID2()).slice(0, 128),
1343
+ "service.instance.id": (options.instanceId || process.env.AUTTER_RUNTIME_INSTANCE_ID || randomUUID()).slice(0, 128),
1441
1344
  ...options.release ? { [ATTR_SERVICE_VERSION]: options.release } : {},
1442
1345
  "deployment.environment": environment,
1346
+ // OTel semconv for SDK distributions: the ingester records which
1347
+ // Autter SDK version each service runs (version compatibility checks).
1348
+ "telemetry.distro.name": distro.name,
1349
+ "telemetry.distro.version": distro.version,
1443
1350
  // Tells the ingester request metrics arrive on the metrics pipe, so
1444
1351
  // it must not also fold our server spans into usage rollups (that
1445
1352
  // would double-count every sampled request).
@@ -1454,30 +1361,24 @@ function initAutterServer(options) {
1454
1361
  let sampler = retainTraces ? new RecordUnsampledSampler(headSampler) : headSampler;
1455
1362
  if (options.llmTracing !== false) sampler = new LlmAwareSampler(sampler);
1456
1363
  const errorTraceBuffer = retainTraces ? new ErrorTraceRetentionProcessor(
1457
- new BatchSpanProcessor(
1458
- new CountingExporter(
1459
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1460
- ),
1461
- { scheduledDelayMillis: 2e3 }
1462
- ),
1364
+ new BatchSpanProcessor(traceExporter(), { scheduledDelayMillis: 2e3 }),
1463
1365
  retainOnError,
1464
1366
  slowThresholdMs
1465
1367
  ) : null;
1466
- const mainSpanProcessor = new BatchSpanProcessor(
1467
- new CountingExporter(
1468
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1469
- )
1470
- );
1368
+ const mainSpanProcessor = new BatchSpanProcessor(traceExporter());
1471
1369
  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
- }),
1370
+ exporter: new RedactingMetricExporter(
1371
+ new OTLPMetricExporter({
1372
+ url: `${endpoint}/v1/metrics`,
1373
+ headers,
1374
+ // Deltas, not lifetime totals: the ingester SUMs data points
1375
+ // into runtime_metrics_1m, and the default (cumulative)
1376
+ // temporality would re-count every past request on each
1377
+ // 60 s export.
1378
+ temporalityPreference: AggregationTemporalityPreference.DELTA
1379
+ }),
1380
+ () => activeRedactor
1381
+ ),
1481
1382
  exportIntervalMillis: options.metricIntervalMs ?? 6e4
1482
1383
  });
1483
1384
  const sdk = new NodeSDK({
@@ -1503,16 +1404,14 @@ function initAutterServer(options) {
1503
1404
  });
1504
1405
  sdk.start();
1505
1406
  const stopMemoryMetrics = options.memoryMetrics === false ? null : startMemoryMetrics();
1407
+ noteCompatFeature("endpoint_latency");
1408
+ if (stopMemoryMetrics) noteCompatFeature("memory_metrics");
1409
+ if (options.llmTracing !== false) noteCompatFeature("llm_calls");
1506
1410
  const alwaysOnProvider = new BasicTracerProvider({
1507
1411
  resource,
1508
1412
  sampler: new AlwaysOnSampler(),
1509
1413
  spanProcessors: [
1510
- new BatchSpanProcessor(
1511
- new CountingExporter(
1512
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1513
- ),
1514
- { scheduledDelayMillis: 2e3 }
1515
- )
1414
+ new BatchSpanProcessor(traceExporter(), { scheduledDelayMillis: 2e3 })
1516
1415
  ]
1517
1416
  });
1518
1417
  const errorTracer = alwaysOnProvider.getTracer("autter-errors");
@@ -1522,52 +1421,65 @@ function initAutterServer(options) {
1522
1421
  errorTraceBuffer?.retainActiveTrace();
1523
1422
  telemetryStats.markCaptured();
1524
1423
  const isError = error instanceof Error;
1525
- const message = isError ? error.message : String(error);
1424
+ const message = activeRedactor.text(isError ? error.message : String(error));
1425
+ const stackText = isError && error.stack ? activeRedactor.text(error.stack) : "";
1526
1426
  const redacted = activeRedactor(attributes);
1527
1427
  const activeSpan = trace2.getActiveSpan();
1528
1428
  const reuseActive = activeSpan?.isRecording() === true;
1529
1429
  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
- }
1430
+ const structured = structuredErrorAttributes(error);
1431
+ const causes = errorAttributes(error);
1432
+ const declared = Object.fromEntries(
1433
+ Object.entries(causes).filter(([key]) => key !== "autter.error.internal")
1434
+ );
1435
+ const requestId = currentOperation()?.requestId;
1436
+ span.setAttributes({
1437
+ "autter.severity": "error",
1438
+ ...redacted,
1439
+ ...structured,
1440
+ ...requestId ? { "autter.request.id": requestId } : {}
1441
+ });
1442
+ markCaptured(error);
1443
+ noteOperationError(error);
1444
+ notifyException(error, redacted);
1445
+ if (isError && stackText) {
1446
+ span.addEvent("exception", {
1447
+ "exception.type": error.name,
1448
+ "exception.message": message,
1449
+ "exception.stacktrace": stackText,
1450
+ ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1451
+ ...declared
1452
+ });
1543
1453
  } else {
1544
1454
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
1545
1455
  span.addEvent("exception", {
1546
1456
  "exception.type": isError ? error.name : "Error",
1547
1457
  "exception.message": message,
1548
- ...stack ? { "exception.stacktrace": stack } : {},
1549
- ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {}
1458
+ ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1459
+ ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1460
+ ...declared
1550
1461
  });
1551
1462
  }
1552
1463
  span.setStatus({ code: SpanStatusCode.ERROR, message });
1553
1464
  if (!reuseActive) span.end();
1554
1465
  }
1555
- function captureMessage2(message, severity2 = "warning", attributes) {
1556
- if (severity2 === "error" || severity2 === "fatal") {
1466
+ function captureMessage2(message, severity = "warning", attributes) {
1467
+ if (severity === "error" || severity === "fatal") {
1557
1468
  errorTraceBuffer?.retainActiveTrace();
1558
1469
  }
1559
1470
  telemetryStats.markCaptured();
1560
1471
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureMessage")).join("\n");
1472
+ const safeMessage = activeRedactor.text(message);
1561
1473
  const span = errorTracer.startSpan("Message", {
1562
- attributes: { "autter.severity": severity2, ...activeRedactor(attributes) }
1474
+ attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
1563
1475
  });
1564
1476
  span.addEvent("exception", {
1565
1477
  "exception.type": "Message",
1566
- "exception.message": message,
1567
- ...stack ? { "exception.stacktrace": stack } : {},
1568
- "autter.severity": severity2
1478
+ "exception.message": safeMessage,
1479
+ ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1480
+ "autter.severity": severity
1569
1481
  });
1570
- span.setStatus({ code: SpanStatusCode.ERROR, message });
1482
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1571
1483
  span.end();
1572
1484
  }
1573
1485
  function reportOutcome2(name, message, attributes) {
@@ -1593,12 +1505,14 @@ function initAutterServer(options) {
1593
1505
  apiKey: options.apiKey,
1594
1506
  service: options.service,
1595
1507
  environment,
1508
+ distro,
1596
1509
  release: options.release,
1597
1510
  options: options.logging,
1598
1511
  redact: activeRedactor,
1599
- run: (name, fn, attributes) => runWithSpan(processTracer, name, fn, activeRedactor(attributes)),
1512
+ run: (name, fn, attributes, links) => runWithSpan(processTracer, name, fn, activeRedactor(attributes), links),
1600
1513
  reportOutcome: reportOutcome2
1601
1514
  });
1515
+ hookRequests = options.logging?.requests === true;
1602
1516
  if (options.captureGlobalErrors !== false) {
1603
1517
  process.on("uncaughtExceptionMonitor", (err) => {
1604
1518
  captureException2(err, { "autter.unhandled": true });
@@ -1637,6 +1551,7 @@ function initAutterServer(options) {
1637
1551
  trackLlmCall: (call) => recordLlmCall(llmTracer, call),
1638
1552
  shutdown: async () => {
1639
1553
  const logShutdown = await Promise.allSettled([shutdownRuntimeLogger()]);
1554
+ hookRequests = false;
1640
1555
  stopMemoryMetrics?.();
1641
1556
  active = null;
1642
1557
  activeAlwaysOnProvider = null;
@@ -1657,17 +1572,35 @@ function captureException(error, attributes) {
1657
1572
  active.captureException(error, attributes);
1658
1573
  return;
1659
1574
  }
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)
1575
+ if (loggerOnlyCapture) {
1576
+ loggerOnlyCapture(error, attributes);
1577
+ return;
1578
+ }
1579
+ markCaptured(error);
1580
+ noteOperationError(error);
1581
+ notifyException(error, attributes);
1582
+ const span = trace2.getTracer("autter-errors").startSpan(error instanceof Error ? error.name : "Error", {
1583
+ attributes: activeRedactor(attributes)
1665
1584
  });
1585
+ recordError(span, error);
1666
1586
  span.end();
1667
1587
  }
1588
+ var loggerOnlyCapture = null;
1589
+ function setLoggerOnlyCapture(fn) {
1590
+ loggerOnlyCapture = fn;
1591
+ }
1592
+ function serverActive() {
1593
+ return active !== null;
1594
+ }
1595
+ setRequestExceptionCapturer((error) => captureException(error));
1668
1596
  function withProcessSpan(name, fn, attributes) {
1669
1597
  if (active) return active.withProcessSpan(name, fn, attributes);
1670
- return runWithSpan(trace2.getTracer("autter-processes"), name, fn, attributes);
1598
+ return runWithSpan(
1599
+ trace2.getTracer("autter-processes"),
1600
+ name,
1601
+ fn,
1602
+ activeRedactor(attributes)
1603
+ );
1671
1604
  }
1672
1605
  function withLlmCall(info, fn) {
1673
1606
  if (active) return active.withLlmCall(info, fn);
@@ -1680,20 +1613,21 @@ function trackLlmCall(call) {
1680
1613
  }
1681
1614
  recordLlmCall(trace2.getTracer("autter-llm"), call);
1682
1615
  }
1683
- function captureMessage(message, severity2 = "warning", attributes) {
1616
+ function captureMessage(message, severity = "warning", attributes) {
1684
1617
  if (active) {
1685
- active.captureMessage(message, severity2, attributes);
1618
+ active.captureMessage(message, severity, attributes);
1686
1619
  return;
1687
1620
  }
1621
+ const safeMessage = activeRedactor.text(message);
1688
1622
  const span = trace2.getTracer("autter-errors").startSpan("Message", {
1689
- attributes: { "autter.severity": severity2, ...attributes }
1623
+ attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
1690
1624
  });
1691
1625
  span.addEvent("exception", {
1692
1626
  "exception.type": "Message",
1693
- "exception.message": message,
1694
- "autter.severity": severity2
1627
+ "exception.message": safeMessage,
1628
+ "autter.severity": severity
1695
1629
  });
1696
- span.setStatus({ code: SpanStatusCode.ERROR, message });
1630
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1697
1631
  span.end();
1698
1632
  }
1699
1633
  function reportOutcome(name, message, attributes) {
@@ -1715,7 +1649,7 @@ function makeSafeCapture(options) {
1715
1649
  const clean = (attributes) => attributes ? { ...redactAttributes(attributes, options) } : void 0;
1716
1650
  return {
1717
1651
  captureException: (error, attributes) => captureException(error, clean(attributes)),
1718
- captureMessage: (message, severity2, attributes) => captureMessage(message, severity2 ?? "warning", clean(attributes))
1652
+ captureMessage: (message, severity, attributes) => captureMessage(message, severity ?? "warning", clean(attributes))
1719
1653
  };
1720
1654
  }
1721
1655
  function alwaysOnTracer(name) {
@@ -1787,15 +1721,26 @@ function extractUsage(result) {
1787
1721
  const r = result;
1788
1722
  const usage = r?.usage ?? r?.message?.usage;
1789
1723
  const meta = r?.usageMetadata;
1724
+ const details = usage?.prompt_tokens_details;
1725
+ const cached = usage?.cache_read_input_tokens ?? (typeof details?.cached_tokens === "number" ? details.cached_tokens : void 0) ?? meta?.cachedContentTokenCount;
1790
1726
  const input = usage?.prompt_tokens ?? usage?.input_tokens ?? meta?.promptTokenCount ?? void 0;
1791
1727
  const output = usage?.completion_tokens ?? usage?.output_tokens ?? meta?.candidatesTokenCount ?? void 0;
1792
1728
  const responseModel = r?.model ?? r?.modelVersion;
1793
1729
  return {
1794
1730
  inputTokens: typeof input === "number" ? input : void 0,
1795
1731
  outputTokens: typeof output === "number" ? output : void 0,
1732
+ cacheReadTokens: typeof cached === "number" ? cached : void 0,
1796
1733
  responseModel: typeof responseModel === "string" ? responseModel : void 0
1797
1734
  };
1798
1735
  }
1736
+ function rollUp(model, usage) {
1737
+ recordOperationAiUsage({
1738
+ model: usage.responseModel ?? model,
1739
+ ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
1740
+ ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
1741
+ ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {}
1742
+ });
1743
+ }
1799
1744
  function applyUsage(span, usage) {
1800
1745
  if (usage.inputTokens !== void 0) {
1801
1746
  span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
@@ -1808,14 +1753,10 @@ function applyUsage(span, usage) {
1808
1753
  }
1809
1754
  }
1810
1755
  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
- });
1756
+ recordError(span, err);
1816
1757
  span.end();
1817
1758
  }
1818
- function wrapStream(stream, span) {
1759
+ function wrapStream(stream, span, model) {
1819
1760
  let ended = false;
1820
1761
  const seen = {
1821
1762
  inputTokens: void 0,
@@ -1826,6 +1767,7 @@ function wrapStream(stream, span) {
1826
1767
  if (ended) return;
1827
1768
  ended = true;
1828
1769
  applyUsage(span, seen);
1770
+ rollUp(model, seen);
1829
1771
  span.setAttribute("autter.llm.streamed", true);
1830
1772
  if (err !== void 0) {
1831
1773
  endWithError(span, err);
@@ -1847,6 +1789,7 @@ function wrapStream(stream, span) {
1847
1789
  const usage = extractUsage(step.value);
1848
1790
  if (usage.inputTokens !== void 0) seen.inputTokens = usage.inputTokens;
1849
1791
  if (usage.outputTokens !== void 0) seen.outputTokens = usage.outputTokens;
1792
+ if (usage.cacheReadTokens !== void 0) seen.cacheReadTokens = usage.cacheReadTokens;
1850
1793
  if (usage.responseModel) seen.responseModel = usage.responseModel;
1851
1794
  } else {
1852
1795
  finish();
@@ -1857,9 +1800,9 @@ function wrapStream(stream, span) {
1857
1800
  throw err;
1858
1801
  }
1859
1802
  },
1860
- async return(value3) {
1803
+ async return(value2) {
1861
1804
  finish();
1862
- return inner.return ? inner.return(value3) : { done: true, value: void 0 };
1805
+ return inner.return ? inner.return(value2) : { done: true, value: void 0 };
1863
1806
  },
1864
1807
  async throw(err) {
1865
1808
  finish(err);
@@ -1869,8 +1812,8 @@ function wrapStream(stream, span) {
1869
1812
  };
1870
1813
  };
1871
1814
  }
1872
- const value2 = Reflect.get(target, prop, receiver);
1873
- return typeof value2 === "function" ? value2.bind(target) : value2;
1815
+ const value = Reflect.get(target, prop, receiver);
1816
+ return typeof value === "function" ? value.bind(target) : value;
1874
1817
  }
1875
1818
  });
1876
1819
  }
@@ -1892,7 +1835,7 @@ function instrumentLlmClient(client, options) {
1892
1835
  "gen_ai.system": provider,
1893
1836
  "gen_ai.request.model": request.model,
1894
1837
  ...userId ? { "autter.user_id": userId } : {},
1895
- ...options?.attributes
1838
+ ...redactWithActive(options?.attributes)
1896
1839
  }
1897
1840
  });
1898
1841
  let result;
@@ -1904,9 +1847,11 @@ function instrumentLlmClient(client, options) {
1904
1847
  }
1905
1848
  if (!(result instanceof Promise)) {
1906
1849
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
1907
- return wrapStream(result, span);
1850
+ return wrapStream(result, span, request.model);
1908
1851
  }
1909
- applyUsage(span, extractUsage(result));
1852
+ const usage = extractUsage(result);
1853
+ applyUsage(span, usage);
1854
+ rollUp(request.model, usage);
1910
1855
  span.setStatus({ code: SpanStatusCode2.OK });
1911
1856
  span.end();
1912
1857
  return result;
@@ -1914,9 +1859,11 @@ function instrumentLlmClient(client, options) {
1914
1859
  return result.then(
1915
1860
  (resolved) => {
1916
1861
  if (resolved && typeof resolved === "object" && Symbol.asyncIterator in resolved) {
1917
- return wrapStream(resolved, span);
1862
+ return wrapStream(resolved, span, request.model);
1918
1863
  }
1919
- applyUsage(span, extractUsage(resolved));
1864
+ const usage = extractUsage(resolved);
1865
+ applyUsage(span, usage);
1866
+ rollUp(request.model, usage);
1920
1867
  span.setStatus({ code: SpanStatusCode2.OK });
1921
1868
  span.end();
1922
1869
  return resolved;
@@ -1932,30 +1879,30 @@ function instrumentLlmClient(client, options) {
1932
1879
  if (cached) return cached;
1933
1880
  const proxy = new Proxy(target, {
1934
1881
  get(t, prop, receiver) {
1935
- const value2 = Reflect.get(t, prop, receiver);
1936
- if (typeof prop === "symbol") return value2;
1882
+ const value = Reflect.get(t, prop, receiver);
1883
+ if (typeof prop === "symbol") return value;
1937
1884
  const propPath = path ? `${path}.${prop}` : prop;
1938
- if (typeof value2 === "function") {
1885
+ if (typeof value === "function") {
1939
1886
  if (CALL_METHODS.has(prop)) {
1940
1887
  return (...args) => {
1941
1888
  const first = args[0];
1942
1889
  if (first && typeof first === "object" && typeof first.model === "string") {
1943
1890
  return traceCall(
1944
- value2,
1891
+ value,
1945
1892
  t,
1946
1893
  propPath,
1947
1894
  args
1948
1895
  );
1949
1896
  }
1950
- return value2.apply(t, args);
1897
+ return value.apply(t, args);
1951
1898
  };
1952
1899
  }
1953
- return value2.bind(t);
1900
+ return value.bind(t);
1954
1901
  }
1955
- if (value2 !== null && typeof value2 === "object" && !(value2 instanceof Promise)) {
1956
- return deepProxy(value2, propPath);
1902
+ if (value !== null && typeof value === "object" && !(value instanceof Promise)) {
1903
+ return deepProxy(value, propPath);
1957
1904
  }
1958
- return value2;
1905
+ return value;
1959
1906
  }
1960
1907
  });
1961
1908
  proxied.set(target, proxy);
@@ -2007,26 +1954,245 @@ ${frames.join("\n")}`;
2007
1954
  });
2008
1955
  } };
2009
1956
  }
1957
+
1958
+ // src/logging-only.ts
1959
+ import {
1960
+ SpanStatusCode as SpanStatusCode3,
1961
+ trace as trace3
1962
+ } from "@opentelemetry/api";
1963
+ var activeLogging = null;
1964
+ function initAutterLogging(options) {
1965
+ if (activeLogging) return activeLogging;
1966
+ if (serverActive()) {
1967
+ console.warn(
1968
+ "[autter-runtime] initAutterLogging() ignored: initAutterServer() is already active and includes logging"
1969
+ );
1970
+ return {
1971
+ captureException,
1972
+ flush: flushRuntimeLogs,
1973
+ shutdown: async () => {
1974
+ }
1975
+ };
1976
+ }
1977
+ if (isDebugEnabled() || options.debug === true) setDebugMode(true);
1978
+ const endpoint = (options.endpoint ?? "https://otlp.autter.dev").replace(/\/$/, "");
1979
+ const environment = options.environment ?? process.env.NODE_ENV ?? "production";
1980
+ const redact = makeRedactor(options.redactAttributes ?? true);
1981
+ const mode = options.exceptions ?? "auto";
1982
+ debugLog(`initialising logger-only service=${options.service} endpoint=${endpoint}`);
1983
+ function captureException2(error, attributes) {
1984
+ markCaptured(error);
1985
+ const redacted = redact(attributes);
1986
+ const span = trace3.getActiveSpan();
1987
+ if (mode === "auto" && span?.isRecording()) {
1988
+ noteOperationError(error);
1989
+ notifyException(error, redacted);
1990
+ span.setAttributes({ "autter.severity": "error", ...redacted, ...structuredErrorAttributes(error) });
1991
+ const declared = errorAttributes(error);
1992
+ if (error instanceof Error) {
1993
+ span.addEvent("exception", {
1994
+ "exception.type": error.name,
1995
+ "exception.message": error.message,
1996
+ ...error.stack ? { "exception.stacktrace": error.stack } : {},
1997
+ ...declared
1998
+ });
1999
+ } else {
2000
+ span.addEvent("exception", {
2001
+ "exception.type": "Error",
2002
+ "exception.message": String(error),
2003
+ ...declared
2004
+ });
2005
+ }
2006
+ span.setStatus({
2007
+ code: SpanStatusCode3.ERROR,
2008
+ message: error instanceof Error ? error.message : String(error)
2009
+ });
2010
+ return;
2011
+ }
2012
+ notifyException(error, redacted);
2013
+ captureExceptionAsLog(error, redacted);
2014
+ }
2015
+ const captured = /* @__PURE__ */ new WeakSet();
2016
+ configureRuntimeLogger({
2017
+ endpoint,
2018
+ apiKey: options.apiKey ?? "",
2019
+ service: options.service,
2020
+ environment,
2021
+ ...options.release ? { release: options.release } : {},
2022
+ ...options.logging ? { options: options.logging } : {},
2023
+ redact,
2024
+ reportOutcome: (name, message, attributes) => {
2025
+ runtimeLogger.error(message, {
2026
+ ...attributes,
2027
+ "autter.outcome.name": name.slice(0, 200),
2028
+ "autter.outcome.message": message.slice(0, 1e3)
2029
+ });
2030
+ },
2031
+ captureThrown: (error) => {
2032
+ if (error && typeof error === "object") {
2033
+ if (captured.has(error)) return;
2034
+ captured.add(error);
2035
+ }
2036
+ captureException2(error);
2037
+ }
2038
+ });
2039
+ setLoggerOnlyCapture(captureException2);
2040
+ const onCrash = (error) => captureException2(error, { "autter.unhandled": true });
2041
+ if (options.captureGlobalErrors !== false)
2042
+ process.on("uncaughtExceptionMonitor", onCrash);
2043
+ registerFlushTarget("logging", { forceFlush: () => flushRuntimeLogs() });
2044
+ let autoFlushHandle = options.autoFlush === false ? null : installAutterAutoFlush();
2045
+ const handle = {
2046
+ captureException: captureException2,
2047
+ flush: flushRuntimeLogs,
2048
+ shutdown: async () => {
2049
+ process.off("uncaughtExceptionMonitor", onCrash);
2050
+ setLoggerOnlyCapture(null);
2051
+ autoFlushHandle?.dispose();
2052
+ autoFlushHandle = null;
2053
+ unregisterFlushTargets();
2054
+ activeLogging = null;
2055
+ await shutdownRuntimeLogger();
2056
+ }
2057
+ };
2058
+ activeLogging = handle;
2059
+ return handle;
2060
+ }
2061
+
2062
+ // src/enrichers.ts
2063
+ function headerOf(req, name) {
2064
+ const headers = req?.headers;
2065
+ if (!headers) return void 0;
2066
+ if (typeof headers.get === "function") {
2067
+ return headers.get(name) ?? void 0;
2068
+ }
2069
+ const value = headers[name.toLowerCase()];
2070
+ if (Array.isArray(value)) return value[0] === void 0 ? void 0 : String(value[0]);
2071
+ return value === void 0 || value === null ? void 0 : String(value);
2072
+ }
2073
+ function enrichUserAgent() {
2074
+ return (event, { req }) => {
2075
+ const ua = headerOf(req, "user-agent");
2076
+ if (!ua) return;
2077
+ const info = parseUserAgent(ua);
2078
+ if (info.browser) event.attributes["user_agent.browser"] = info.browser;
2079
+ if (info.os) event.attributes["user_agent.os"] = info.os;
2080
+ event.attributes["user_agent.device"] = info.device;
2081
+ event.attributes["user_agent.bot"] = info.bot;
2082
+ };
2083
+ }
2084
+ function enrichRequestSize() {
2085
+ return (event, { req, res }) => {
2086
+ const request = Number(headerOf(req, "content-length"));
2087
+ if (Number.isFinite(request) && request >= 0)
2088
+ event.attributes["http.request.body.size"] = request;
2089
+ const raw = res?.getHeader?.(
2090
+ "content-length"
2091
+ );
2092
+ const response = Number(raw);
2093
+ if (raw !== void 0 && Number.isFinite(response) && response >= 0)
2094
+ event.attributes["http.response.body.size"] = response;
2095
+ };
2096
+ }
2097
+ var COUNTRY_HEADERS = [
2098
+ "cf-ipcountry",
2099
+ "x-vercel-ip-country",
2100
+ "cloudfront-viewer-country",
2101
+ "x-country-code",
2102
+ "fastly-geo-country-code"
2103
+ ];
2104
+ function enrichEdgeGeo() {
2105
+ return (event, { req }) => {
2106
+ for (const name of COUNTRY_HEADERS) {
2107
+ const value = headerOf(req, name)?.trim().toUpperCase();
2108
+ if (value && /^[A-Z]{2}$/.test(value) && value !== "XX" && value !== "T1") {
2109
+ event.attributes["geo.country.iso_code"] = value;
2110
+ return;
2111
+ }
2112
+ }
2113
+ };
2114
+ }
2115
+ var REGION_VARS = [
2116
+ "AUTTER_REGION",
2117
+ "AWS_REGION",
2118
+ "VERCEL_REGION",
2119
+ "FLY_REGION",
2120
+ "RAILWAY_REPLICA_REGION",
2121
+ "GOOGLE_CLOUD_REGION",
2122
+ "CLOUD_RUN_REGION",
2123
+ "RENDER_REGION",
2124
+ "AZURE_REGION"
2125
+ ];
2126
+ var COMMIT_VARS = [
2127
+ "AUTTER_COMMIT",
2128
+ "GIT_SHA",
2129
+ "GIT_COMMIT",
2130
+ "COMMIT_SHA",
2131
+ "SOURCE_COMMIT",
2132
+ "VERCEL_GIT_COMMIT_SHA",
2133
+ "GITHUB_SHA",
2134
+ "RENDER_GIT_COMMIT",
2135
+ "RAILWAY_GIT_COMMIT_SHA",
2136
+ "CF_PAGES_COMMIT_SHA",
2137
+ "HEROKU_SLUG_COMMIT",
2138
+ "SOURCE_VERSION"
2139
+ ];
2140
+ function enrichDeployment(env = process.env) {
2141
+ const pick = (names) => names.map((name) => env[name]?.trim()).find((value) => value);
2142
+ const region = pick(REGION_VARS)?.slice(0, 64);
2143
+ const commit = pick(COMMIT_VARS)?.slice(0, 64);
2144
+ return (event) => {
2145
+ if (region) event.attributes["deployment.region"] = region;
2146
+ if (commit) event.attributes["deployment.commit"] = commit;
2147
+ };
2148
+ }
2010
2149
  export {
2150
+ CODE_PATTERN,
2151
+ COMPAT_MANIFEST,
2152
+ INGESTER_VERSION_HEADER,
2153
+ RuntimeError,
2154
+ SDK_IDENTITY,
2155
+ autterErrorResponse,
2156
+ autterFastify,
2157
+ autterRequests,
2011
2158
  captureException,
2012
2159
  captureMessage,
2160
+ consoleSink,
2013
2161
  createBrowserRelayFetchHandler,
2014
2162
  createBrowserRelayHandler,
2015
2163
  createRuntimeLogger,
2164
+ defineRuntimeErrors,
2016
2165
  emitLlmSelftestTrace,
2166
+ enrichDeployment,
2167
+ enrichEdgeGeo,
2168
+ enrichRequestSize,
2169
+ enrichUserAgent,
2170
+ errorAttributes,
2171
+ evaluateCompat,
2172
+ fetchIngesterCompat,
2173
+ fileSink,
2017
2174
  flushRuntimeLogs,
2175
+ initAutterLogging,
2018
2176
  initAutterServer,
2019
2177
  installAutterAutoFlush,
2020
2178
  instrumentLlmClient,
2179
+ isRuntimeErrorLike,
2021
2180
  makeSafeCapture,
2181
+ otlpSink,
2022
2182
  redactAttributes,
2183
+ redactText,
2023
2184
  reportOutcome,
2185
+ runDoctor,
2186
+ runInBackground,
2187
+ runtimeContext,
2024
2188
  runtimeLogStats,
2025
2189
  runtimeLogger,
2026
2190
  sanitizeBrowserPayload,
2027
2191
  startCaughtExceptionSampler,
2192
+ toClientError,
2028
2193
  trackLlmCall,
2029
2194
  withLlmCall,
2030
2195
  withProcessSpan,
2031
- withRuntimeOperation
2196
+ withRuntimeOperation,
2197
+ withRuntimeRequest
2032
2198
  };