@autter/runtime-node 1.9.0 → 1.10.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
@@ -51,15 +51,17 @@ import {
51
51
  stopLoggedErrorCapture,
52
52
  toClientError,
53
53
  withRuntimeOperation
54
- } from "./chunk-HHKQ3UCH.js";
54
+ } from "./chunk-V65UY6LU.js";
55
55
  import {
56
- runDoctor
57
- } from "./chunk-JSGGUWRD.js";
56
+ runDoctor,
57
+ runVerify
58
+ } from "./chunk-3UODRQBD.js";
58
59
  import {
59
60
  COMPAT_MANIFEST,
60
61
  CountingExporter,
61
62
  INGESTER_VERSION_HEADER,
62
63
  SDK_IDENTITY,
64
+ authHeaders,
63
65
  configureCompatCheck,
64
66
  debugLog,
65
67
  ensureCompatConfigured,
@@ -69,14 +71,18 @@ import {
69
71
  installAutterAutoFlush,
70
72
  isDebugEnabled,
71
73
  noteCompatFeature,
74
+ noteIngestRefusal,
72
75
  noteRelayRejection,
73
76
  observeIngesterResponse,
74
77
  registerFlushTarget,
78
+ resolveIngestKey,
75
79
  setDebugMode,
76
80
  sharedSlot,
77
81
  telemetryStats,
78
- unregisterFlushTargets
79
- } from "./chunk-ZKM76SDP.js";
82
+ unregisterFlushTargets,
83
+ warnKeyKind,
84
+ warnMissingKey
85
+ } from "./chunk-KWAJIELR.js";
80
86
 
81
87
  // src/relay.ts
82
88
  var IpWindow = class {
@@ -358,18 +364,53 @@ function relayFingerprint(raw, redactor) {
358
364
  }
359
365
  var defaultRelayRedactor = makeRedactor(true);
360
366
  function sanitizeBrowserPayload(raw, redactor = defaultRelayRedactor) {
361
- if (typeof raw !== "object" || raw === null) return null;
367
+ const checked = checkBrowserPayload(raw, redactor);
368
+ return "payload" in checked ? checked.payload : null;
369
+ }
370
+ var MAX_RELAY_ISSUES = 5;
371
+ function describe(value) {
372
+ if (value === null) return "null";
373
+ if (Array.isArray(value)) return "an array";
374
+ if (typeof value === "string") return value ? `"${value.slice(0, 40)}"` : "an empty string";
375
+ return typeof value === "undefined" ? "missing" : `a ${typeof value}`;
376
+ }
377
+ function checkBrowserPayload(raw, redactor = defaultRelayRedactor) {
378
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
379
+ return { issues: [{ path: [], message: `expected a JSON object, got ${describe(raw)}` }] };
380
+ }
362
381
  const p = raw;
363
- if (p.version !== 1) return null;
364
- if (typeof p.service !== "string" || !p.service) return null;
365
- if (typeof p.environment !== "string" || !p.environment) return null;
366
- if (!Array.isArray(p.events) || p.events.length > MAX_EVENTS) return null;
382
+ const issues = [];
383
+ const issue = (path, message) => {
384
+ if (issues.length < MAX_RELAY_ISSUES) issues.push({ path, message });
385
+ };
386
+ if (p.version !== 1) issue(["version"], `expected 1, got ${describe(p.version)}`);
387
+ if (typeof p.service !== "string" || !p.service) issue(["service"], `expected a non-empty string, got ${describe(p.service)}`);
388
+ if (typeof p.environment !== "string" || !p.environment) {
389
+ issue(["environment"], `expected a non-empty string, got ${describe(p.environment)}`);
390
+ }
391
+ if (!Array.isArray(p.events)) issue(["events"], `expected an array, got ${describe(p.events)}`);
392
+ else if (p.events.length > MAX_EVENTS) issue(["events"], `at most ${MAX_EVENTS} events per batch, got ${p.events.length}`);
393
+ if (Array.isArray(p.events) && p.events.length <= MAX_EVENTS) {
394
+ p.events.forEach((rawEvent, index) => {
395
+ if (typeof rawEvent !== "object" || rawEvent === null) {
396
+ issue(["events", index], `expected an object, got ${describe(rawEvent)}`);
397
+ return;
398
+ }
399
+ const e = rawEvent;
400
+ if (typeof e.type !== "string" || !EVENT_TYPES.has(e.type)) {
401
+ issue(["events", index, "type"], `expected one of ${[...EVENT_TYPES].join(", ")}, got ${describe(e.type)}`);
402
+ }
403
+ if (typeof e.timestamp !== "string") {
404
+ issue(["events", index, "timestamp"], `expected an ISO 8601 string, got ${describe(e.timestamp)}`);
405
+ }
406
+ });
407
+ }
408
+ if (issues.length) return { issues };
409
+ const service = p.service;
410
+ const environment = p.environment;
367
411
  const events = [];
368
412
  for (const rawEvent of p.events) {
369
- if (typeof rawEvent !== "object" || rawEvent === null) return null;
370
413
  const e = rawEvent;
371
- if (typeof e.type !== "string" || !EVENT_TYPES.has(e.type)) return null;
372
- if (typeof e.timestamp !== "string") return null;
373
414
  events.push({
374
415
  type: e.type,
375
416
  timestamp: e.timestamp,
@@ -390,31 +431,62 @@ function sanitizeBrowserPayload(raw, redactor = defaultRelayRedactor) {
390
431
  ...typeof e.traceId === "string" && TRACE_ID.test(e.traceId) ? { traceId: e.traceId } : {}
391
432
  });
392
433
  }
393
- return {
434
+ return { payload: {
394
435
  version: 1,
395
436
  ...typeof p.sessionId === "string" ? { sessionId: p.sessionId.slice(0, 100) } : {},
396
- service: p.service.slice(0, 200),
397
- environment: p.environment.slice(0, 100),
437
+ service: service.slice(0, 200),
438
+ environment: environment.slice(0, 100),
398
439
  ...typeof p.release === "string" ? { release: p.release.slice(0, 200) } : {},
399
440
  // @autter/runtime-browser version, for the ingester's compat records.
400
441
  ...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 } : {},
401
442
  events
402
- };
443
+ } };
403
444
  }
404
- function forward(payload, opts) {
445
+ var NO_KEY_BODY = {
446
+ error: "relay has no ingest key",
447
+ fix: "set AUTTER_RUNTIME_KEY (a server key, autter_rt_\u2026) where the relay runs"
448
+ };
449
+ var FORWARD_ATTEMPTS = 3;
450
+ var FORWARD_MAX_WAIT_MS = 1e4;
451
+ function forward(payload, opts, apiKey) {
405
452
  const endpoint = (opts.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
406
453
  const url = `${endpoint}/v1/browser`;
407
454
  const compat = opts.compatCheck !== false;
408
455
  if (compat) ensureCompatConfigured(endpoint);
409
- void fetch(url, {
410
- method: "POST",
411
- headers: {
412
- "content-type": "application/json",
413
- authorization: `Bearer ${opts.apiKey}`
414
- },
415
- body: JSON.stringify(payload),
416
- signal: AbortSignal.timeout(1e4)
417
- }).then((response) => {
456
+ const body = JSON.stringify(payload);
457
+ const attempt = async (n) => {
458
+ let response;
459
+ try {
460
+ response = await fetch(url, {
461
+ method: "POST",
462
+ headers: {
463
+ "content-type": "application/json",
464
+ ...authHeaders(apiKey)
465
+ },
466
+ body,
467
+ signal: AbortSignal.timeout(1e4)
468
+ });
469
+ } catch (err) {
470
+ if (n >= FORWARD_ATTEMPTS) throw err;
471
+ await sleep(1e3 * 2 ** (n - 1));
472
+ return attempt(n + 1);
473
+ }
474
+ if ((response.status === 429 || response.status >= 500) && n < FORWARD_ATTEMPTS) {
475
+ const seconds = Number(response.headers.get("retry-after"));
476
+ await response.body?.cancel().catch(() => {
477
+ });
478
+ await sleep(seconds > 0 ? Math.min(seconds * 1e3, FORWARD_MAX_WAIT_MS) : 1e3 * 2 ** (n - 1));
479
+ return attempt(n + 1);
480
+ }
481
+ if (!response.ok) {
482
+ noteIngestRefusal({
483
+ status: response.status,
484
+ route: "/v1/browser",
485
+ endpoint,
486
+ body: await response.clone().text().catch(() => ""),
487
+ via: "relay"
488
+ });
489
+ }
418
490
  if (!compat) return;
419
491
  const types = new Set(
420
492
  (payload.events ?? []).map((e) => String(e.type))
@@ -425,20 +497,33 @@ function forward(payload, opts) {
425
497
  noteCompatFeature("browser_trace_propagation");
426
498
  }
427
499
  noteRelayRejection(types, response.status, response.headers);
428
- }).catch((err) => {
500
+ };
501
+ void attempt(1).catch((err) => {
429
502
  (opts.onError ?? ((e) => console.warn("autter relay forward failed:", e)))(
430
503
  err
431
504
  );
432
505
  });
433
506
  }
507
+ function sleep(ms) {
508
+ return new Promise((resolve) => setTimeout(resolve, ms));
509
+ }
434
510
  function createBrowserRelayFetchHandler(opts) {
435
511
  const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY;
436
512
  const redactor = opts.redact === void 0 || opts.redact === true ? defaultRelayRedactor : makeRedactor(opts.redact);
437
513
  const limiter = opts.perIpRateLimit === false ? null : new IpWindow(opts.perIpRateLimit ?? 120);
514
+ const apiKey = resolveIngestKey(opts.apiKey);
515
+ if (!apiKey) warnMissingKey("relay");
516
+ else warnKeyKind(apiKey);
438
517
  return async (request) => {
439
518
  if (request.method !== "POST") {
440
519
  return new Response(null, { status: 405 });
441
520
  }
521
+ if (!apiKey) {
522
+ return new Response(JSON.stringify(NO_KEY_BODY), {
523
+ status: 500,
524
+ headers: { "content-type": "application/json" }
525
+ });
526
+ }
442
527
  if (limiter) {
443
528
  const ip = fetchClientKey(request, opts);
444
529
  if (!limiter.allow(ip)) {
@@ -468,13 +553,14 @@ function createBrowserRelayFetchHandler(opts) {
468
553
  status: 400
469
554
  });
470
555
  }
471
- const payload = sanitizeBrowserPayload(raw, redactor);
472
- if (!payload) {
473
- return new Response(JSON.stringify({ error: "invalid payload" }), {
474
- status: 400
556
+ const checked = checkBrowserPayload(raw, redactor);
557
+ if (!("payload" in checked)) {
558
+ return new Response(JSON.stringify({ error: "invalid payload", issues: checked.issues }), {
559
+ status: 400,
560
+ headers: { "content-type": "application/json" }
475
561
  });
476
562
  }
477
- forward(payload, opts);
563
+ forward(checked.payload, opts, apiKey);
478
564
  return new Response(null, { status: 202 });
479
565
  };
480
566
  }
@@ -491,13 +577,16 @@ function createBrowserRelayHandler(opts) {
491
577
  res.end();
492
578
  }
493
579
  }
580
+ const apiKey = resolveIngestKey(opts.apiKey);
581
+ if (!apiKey) warnMissingKey("relay");
582
+ else warnKeyKind(apiKey);
494
583
  function handleParsed(raw, res) {
495
- const payload = sanitizeBrowserPayload(raw, redactor);
496
- if (!payload) {
497
- respond(res, 400, { error: "invalid payload" });
584
+ const checked = checkBrowserPayload(raw, redactor);
585
+ if (!("payload" in checked)) {
586
+ respond(res, 400, { error: "invalid payload", issues: checked.issues });
498
587
  return;
499
588
  }
500
- forward(payload, opts);
589
+ forward(checked.payload, opts, apiKey);
501
590
  respond(res, 202);
502
591
  }
503
592
  return (req, res) => {
@@ -505,6 +594,10 @@ function createBrowserRelayHandler(opts) {
505
594
  respond(res, 405);
506
595
  return;
507
596
  }
597
+ if (!apiKey) {
598
+ respond(res, 500, NO_KEY_BODY);
599
+ return;
600
+ }
508
601
  if (limiter) {
509
602
  const ip = nodeClientKey(req, opts);
510
603
  if (!limiter.allow(ip)) {
@@ -554,13 +647,13 @@ function createBrowserRelayHandler(opts) {
554
647
 
555
648
  // src/server.ts
556
649
  import { errorMonitor } from "events";
557
- import { createRequire } from "module";
558
- import { join } from "path";
650
+ import { createRequire as createRequire2 } from "module";
651
+ import { join as join2 } from "path";
559
652
  import { ServerResponse } from "http";
560
653
  import { randomUUID } from "crypto";
561
654
  import {
562
- context as context2,
563
- trace as trace2,
655
+ context as context3,
656
+ trace as trace3,
564
657
  SpanKind,
565
658
  SpanStatusCode,
566
659
  TraceFlags
@@ -806,6 +899,73 @@ var RedactingMetricExporter = class {
806
899
  }
807
900
  };
808
901
 
902
+ // src/ingest-exporters.ts
903
+ import { ExportResultCode as ExportResultCode2 } from "@opentelemetry/core";
904
+ import {
905
+ AggregationTemporality as AggregationTemporality2
906
+ } from "@opentelemetry/sdk-metrics";
907
+ function refusalOf(result) {
908
+ if (result.code === ExportResultCode2.SUCCESS) return null;
909
+ const error = result.error;
910
+ return typeof error?.code === "number" ? { status: error.code, body: error.data } : null;
911
+ }
912
+ var NO_KEY = {
913
+ code: ExportResultCode2.FAILED,
914
+ error: new Error("no Autter ingest key: not sent (set AUTTER_RUNTIME_KEY)")
915
+ };
916
+ function observe(endpoint, route, callback) {
917
+ return (result) => {
918
+ const refusal = refusalOf(result);
919
+ if (refusal) noteIngestRefusal({ ...refusal, endpoint, route });
920
+ callback(result);
921
+ };
922
+ }
923
+ var IngestSpanExporter = class {
924
+ constructor(inner, target) {
925
+ this.inner = inner;
926
+ this.target = target;
927
+ }
928
+ inner;
929
+ target;
930
+ export(spans, resultCallback) {
931
+ if (!this.target.hasKey) {
932
+ resultCallback(NO_KEY);
933
+ return;
934
+ }
935
+ this.inner.export(spans, observe(this.target.endpoint, "/v1/traces", resultCallback));
936
+ }
937
+ shutdown() {
938
+ return this.inner.shutdown();
939
+ }
940
+ forceFlush() {
941
+ return this.inner.forceFlush?.() ?? Promise.resolve();
942
+ }
943
+ };
944
+ var IngestMetricExporter = class {
945
+ constructor(inner, target) {
946
+ this.inner = inner;
947
+ this.target = target;
948
+ }
949
+ inner;
950
+ target;
951
+ export(metrics2, resultCallback) {
952
+ if (!this.target.hasKey) {
953
+ resultCallback(NO_KEY);
954
+ return;
955
+ }
956
+ this.inner.export(metrics2, observe(this.target.endpoint, "/v1/metrics", resultCallback));
957
+ }
958
+ forceFlush() {
959
+ return this.inner.forceFlush();
960
+ }
961
+ shutdown() {
962
+ return this.inner.shutdown();
963
+ }
964
+ selectAggregationTemporality(instrumentType) {
965
+ return this.inner.selectAggregationTemporality ? this.inner.selectAggregationTemporality(instrumentType) : AggregationTemporality2.CUMULATIVE;
966
+ }
967
+ };
968
+
809
969
  // src/memory.ts
810
970
  import { PerformanceObserver } from "perf_hooks";
811
971
  import { getHeapStatistics } from "v8";
@@ -914,6 +1074,27 @@ function captureAtBoundary(error, state, rule = void 0, attributes) {
914
1074
  } catch {
915
1075
  }
916
1076
  }
1077
+ function requestScopeAttributes() {
1078
+ try {
1079
+ const out = {};
1080
+ const state = local.getStore();
1081
+ const active = context.active();
1082
+ const metadata = getRPCMetadata(active);
1083
+ const http = metadata?.type === RPCType.HTTP ? metadata : void 0;
1084
+ const spanAttributes = http?.span?.attributes;
1085
+ const req = state?.req;
1086
+ const nodeRequest = !!req && typeof req === "object" && typeof req.url === "string" && req.url.startsWith("/");
1087
+ const requestId = state?.requestId ?? spanAttributes?.["autter.request.id"];
1088
+ if (typeof requestId === "string" && requestId) out["autter.request.id"] = requestId;
1089
+ const route = http?.route ?? spanAttributes?.["http.route"] ?? (nodeRequest ? expressRouteOf(req) ?? normalizeRoutePath(pathOf(req)) : void 0);
1090
+ if (typeof route === "string" && route) out["http.route"] = route;
1091
+ const method = (nodeRequest ? req.method : void 0) ?? spanAttributes?.["http.request.method"] ?? spanAttributes?.["http.method"];
1092
+ if (typeof method === "string" && method) out["http.request.method"] = method.toUpperCase();
1093
+ return Object.keys(out).length > 0 ? out : void 0;
1094
+ } catch {
1095
+ return void 0;
1096
+ }
1097
+ }
917
1098
  function requestStateOf(req) {
918
1099
  if (!req || typeof req !== "object") return void 0;
919
1100
  return req[REQUEST_STATE];
@@ -1403,6 +1584,173 @@ function installUnhandledRejectionCapture(capture, mode = unhandledRejectionsMod
1403
1584
  };
1404
1585
  }
1405
1586
 
1587
+ // src/express-errors.ts
1588
+ import { createRequire } from "module";
1589
+ import { join } from "path";
1590
+ import { context as context2, trace as trace2 } from "@opentelemetry/api";
1591
+ import { Hook } from "require-in-the-middle";
1592
+ var PATCHED = /* @__PURE__ */ Symbol.for("autter.runtime.express.layer");
1593
+ var WRAPPED = /* @__PURE__ */ Symbol.for("autter.runtime.express.next");
1594
+ var ERRORS = /* @__PURE__ */ Symbol.for("autter.runtime.express.errors");
1595
+ var MAX_ERRORS = 8;
1596
+ function noteExpressError(req, error) {
1597
+ if (error === void 0 || error === null || error === false) return;
1598
+ if (error === "route" || error === "router") return;
1599
+ if (!req || typeof req !== "object") return;
1600
+ try {
1601
+ const target = req;
1602
+ let list = target[ERRORS];
1603
+ if (!list) {
1604
+ list = [];
1605
+ target[ERRORS] = list;
1606
+ }
1607
+ if (list.length < MAX_ERRORS && !list.includes(error)) list.push(error);
1608
+ } catch {
1609
+ }
1610
+ }
1611
+ function wrapNext(req, next) {
1612
+ if (typeof next !== "function") return next;
1613
+ const fn = next;
1614
+ const existing = fn[WRAPPED];
1615
+ if (existing) return existing;
1616
+ const wrapped = function autterExpressNext(...args) {
1617
+ if (args.length > 0) noteExpressError(req, args[0]);
1618
+ return fn.apply(this, args);
1619
+ };
1620
+ try {
1621
+ fn[WRAPPED] = wrapped;
1622
+ } catch {
1623
+ }
1624
+ return wrapped;
1625
+ }
1626
+ function patchLayerPrototype(proto) {
1627
+ if (!proto || typeof proto !== "object") return false;
1628
+ const target = proto;
1629
+ if (target[PATCHED]) return true;
1630
+ const requestName = typeof target.handleRequest === "function" ? "handleRequest" : typeof target.handle_request === "function" ? "handle_request" : null;
1631
+ const errorName = typeof target.handleError === "function" ? "handleError" : typeof target.handle_error === "function" ? "handle_error" : null;
1632
+ if (!requestName || !errorName) return false;
1633
+ const handleRequest = target[requestName];
1634
+ const handleError = target[errorName];
1635
+ target[requestName] = function autterHandleRequest(req, res, next) {
1636
+ let wrapped = next;
1637
+ try {
1638
+ wrapped = wrapNext(req, next);
1639
+ } catch {
1640
+ wrapped = next;
1641
+ }
1642
+ return handleRequest.call(this, req, res, wrapped);
1643
+ };
1644
+ target[errorName] = function autterHandleError(error, req, res, next) {
1645
+ let wrapped = next;
1646
+ try {
1647
+ noteExpressError(req, error);
1648
+ wrapped = wrapNext(req, next);
1649
+ } catch {
1650
+ wrapped = next;
1651
+ }
1652
+ return handleError.call(this, error, req, res, wrapped);
1653
+ };
1654
+ try {
1655
+ Object.defineProperty(target, PATCHED, { value: true });
1656
+ } catch {
1657
+ target[PATCHED] = true;
1658
+ }
1659
+ return true;
1660
+ }
1661
+ var LAYER_FILES = ["express/lib/router/layer.js", "router/lib/layer.js"];
1662
+ function patchLayerModule(exports) {
1663
+ try {
1664
+ const Layer = exports;
1665
+ if (typeof Layer === "function" && patchLayerPrototype(Layer.prototype))
1666
+ debugLog("express: error capture installed");
1667
+ } catch {
1668
+ }
1669
+ }
1670
+ function scanModuleCache() {
1671
+ try {
1672
+ const cache = createRequire(join(process.cwd(), "autter-runtime-express.cjs")).cache;
1673
+ for (const [key, entry] of Object.entries(cache)) {
1674
+ const path = key.replace(/\\/g, "/");
1675
+ if (LAYER_FILES.some((file) => path.endsWith(`/node_modules/${file}`)))
1676
+ patchLayerModule(entry?.exports);
1677
+ }
1678
+ } catch {
1679
+ }
1680
+ }
1681
+ var installed = false;
1682
+ function installExpressErrorCapture() {
1683
+ if (installed) return;
1684
+ installed = true;
1685
+ scanModuleCache();
1686
+ try {
1687
+ new Hook(["express", "router"], { internals: true }, (exports, name) => {
1688
+ const file = name.replace(/\\/g, "/");
1689
+ if (LAYER_FILES.includes(file)) patchLayerModule(exports);
1690
+ return exports;
1691
+ });
1692
+ } catch {
1693
+ }
1694
+ }
1695
+ function patchFromRequest(req) {
1696
+ try {
1697
+ const app = req?.app;
1698
+ if (!app || typeof app !== "function" && typeof app !== "object") return;
1699
+ const router = app._router ?? app.router;
1700
+ const layer = router?.stack?.[0];
1701
+ if (layer && typeof layer === "object" && patchLayerPrototype(Object.getPrototypeOf(layer)))
1702
+ debugLog("express: error capture installed (from a request)");
1703
+ } catch {
1704
+ }
1705
+ }
1706
+ function declaredStatus(error) {
1707
+ if (!error || typeof error !== "object") return void 0;
1708
+ try {
1709
+ const value = error.status ?? error.statusCode;
1710
+ const status = typeof value === "string" ? Number(value) : value;
1711
+ return typeof status === "number" && Number.isInteger(status) && status >= 400 && status < 600 ? status : void 0;
1712
+ } catch {
1713
+ return void 0;
1714
+ }
1715
+ }
1716
+ function watchExpressResponse(span, res, enabled) {
1717
+ const req = res.req;
1718
+ if (!req) return;
1719
+ const ctx = context2.active();
1720
+ let done = false;
1721
+ const settle = (finished) => {
1722
+ if (done) return;
1723
+ done = true;
1724
+ try {
1725
+ const errors = req[ERRORS];
1726
+ if (!errors?.length) {
1727
+ patchFromRequest(req);
1728
+ return;
1729
+ }
1730
+ req[ERRORS] = void 0;
1731
+ if (!enabled()) return;
1732
+ if (finished && !(res.statusCode >= 500)) return;
1733
+ if (errors.some((error2) => isCaptured(error2))) return;
1734
+ const error = errors.find((candidate) => {
1735
+ const status = declaredStatus(candidate);
1736
+ return status === void 0 || status >= 500;
1737
+ });
1738
+ if (error === void 0) return;
1739
+ const state = requestStateOf(req);
1740
+ if (state && !state.sealed) noteOperationError(error, false, state);
1741
+ const run = () => captureAtBoundary(error, state, true);
1742
+ if (span.isRecording()) context2.with(trace2.setSpan(ctx, span), run);
1743
+ else context2.with(ctx, run);
1744
+ } catch {
1745
+ }
1746
+ };
1747
+ try {
1748
+ res.prependListener("finish", () => settle(true));
1749
+ res.prependListener("close", () => settle(false));
1750
+ } catch {
1751
+ }
1752
+ }
1753
+
1406
1754
  // src/route-label.ts
1407
1755
  import { getRPCMetadata as getRPCMetadata2, RPCType as RPCType2 } from "@opentelemetry/core";
1408
1756
  var ours = /* @__PURE__ */ new WeakMap();
@@ -1574,7 +1922,7 @@ var ErrorTraceRetentionProcessor = class {
1574
1922
  }
1575
1923
  /** Promote the active trace, if any — called on captured errors. */
1576
1924
  retainActiveTrace() {
1577
- const active = trace2.getActiveSpan()?.spanContext();
1925
+ const active = trace3.getActiveSpan()?.spanContext();
1578
1926
  if (!active || (active.traceFlags & TraceFlags.SAMPLED) !== 0) return;
1579
1927
  const entry = this.entryFor(active.traceId);
1580
1928
  if (entry.retained) return;
@@ -1687,8 +2035,8 @@ async function runLlmSpan(tracer, info, fn) {
1687
2035
  setAttributes: (attributes) => span.setAttributes(S.redactor(attributes))
1688
2036
  };
1689
2037
  try {
1690
- const result = await context2.with(
1691
- trace2.setSpan(context2.active(), span),
2038
+ const result = await context3.with(
2039
+ trace3.setSpan(context3.active(), span),
1692
2040
  () => fn(handle)
1693
2041
  );
1694
2042
  span.setStatus({ code: SpanStatusCode.OK });
@@ -1731,8 +2079,8 @@ async function runWithSpan(tracer, name, fn, attributes, links) {
1731
2079
  ...links?.length ? { links } : {}
1732
2080
  });
1733
2081
  try {
1734
- const result = await context2.with(
1735
- trace2.setSpan(context2.active(), span),
2082
+ const result = await context3.with(
2083
+ trace3.setSpan(context3.active(), span),
1736
2084
  fn
1737
2085
  );
1738
2086
  span.setStatus({ code: SpanStatusCode.OK });
@@ -1762,7 +2110,7 @@ function redactWithActive(attributes) {
1762
2110
  }
1763
2111
  function patchLoadedHttpModules() {
1764
2112
  try {
1765
- const load = createRequire(join(process.cwd(), "autter-runtime-http.cjs"));
2113
+ const load = createRequire2(join2(process.cwd(), "autter-runtime-http.cjs"));
1766
2114
  load("http");
1767
2115
  load("https");
1768
2116
  } catch {
@@ -1771,7 +2119,7 @@ function patchLoadedHttpModules() {
1771
2119
  var METRIC_ROUTE_ATTR = "autter.metric_has_route";
1772
2120
  function captureRequestRoute(span, response) {
1773
2121
  if (!(response instanceof ServerResponse)) return;
1774
- const rpcMetadata = getRPCMetadata3(context2.active());
2122
+ const rpcMetadata = getRPCMetadata3(context3.active());
1775
2123
  if (rpcMetadata?.type !== RPCType3.HTTP) return;
1776
2124
  let failed = false;
1777
2125
  response.on(errorMonitor, () => {
@@ -1787,6 +2135,7 @@ function captureRequestRoute(span, response) {
1787
2135
  setRoute();
1788
2136
  if (rpcMetadata.route && !failed) span.setAttribute(METRIC_ROUTE_ATTR, true);
1789
2137
  });
2138
+ if (S.expressErrors) watchExpressResponse(span, response, () => S.expressErrors === true);
1790
2139
  if (S.hookRequests) startHookRequest(response);
1791
2140
  }
1792
2141
  function structuredErrorAttributes(error) {
@@ -1808,12 +2157,14 @@ function initAutterServer(options) {
1808
2157
  /\/$/,
1809
2158
  ""
1810
2159
  );
1811
- const headers = { authorization: `Bearer ${options.apiKey}` };
2160
+ const apiKey = resolveIngestKey(options.apiKey);
2161
+ const headers = authHeaders(apiKey);
2162
+ const ingestTarget = { endpoint, hasKey: apiKey !== null };
1812
2163
  const redactor = makeRedactor(options.redactAttributes ?? true);
1813
2164
  const queryPolicy = makeUrlQueryPolicy(options.keepQueryParams);
1814
2165
  const traceExporter = () => new CountingExporter(
1815
2166
  new RedactingSpanExporter(
1816
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers }),
2167
+ new IngestSpanExporter(new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers }), ingestTarget),
1817
2168
  () => redactor,
1818
2169
  queryPolicy
1819
2170
  )
@@ -1821,6 +2172,8 @@ function initAutterServer(options) {
1821
2172
  const environment = options.environment ?? process.env.NODE_ENV ?? "production";
1822
2173
  if (isDebugEnabled() || options.debug === true) setDebugMode(true);
1823
2174
  debugLog(`initialising service=${options.service} endpoint=${endpoint}`);
2175
+ if (apiKey === null) warnMissingKey("server", options.service);
2176
+ else warnKeyKind(apiKey);
1824
2177
  S.redactor = redactor;
1825
2178
  const distro = options.distro ?? SDK_IDENTITY;
1826
2179
  configureCompatCheck({ endpoint, enabled: options.compatCheck !== false, sdk: distro });
@@ -1869,7 +2222,7 @@ function initAutterServer(options) {
1869
2222
  const mainSpanProcessor = new BatchSpanProcessor(traceExporter());
1870
2223
  const metricReader = new PeriodicExportingMetricReader({
1871
2224
  exporter: new RedactingMetricExporter(
1872
- new OTLPMetricExporter({
2225
+ new IngestMetricExporter(new OTLPMetricExporter({
1873
2226
  url: `${endpoint}/v1/metrics`,
1874
2227
  headers,
1875
2228
  // Deltas, not lifetime totals: the ingester SUMs data points
@@ -1877,7 +2230,7 @@ function initAutterServer(options) {
1877
2230
  // temporality would re-count every past request on each
1878
2231
  // 60 s export.
1879
2232
  temporalityPreference: AggregationTemporalityPreference.DELTA
1880
- }),
2233
+ }), ingestTarget),
1881
2234
  () => redactor
1882
2235
  ),
1883
2236
  exportIntervalMillis: options.metricIntervalMs ?? 6e4
@@ -1939,9 +2292,11 @@ function initAutterServer(options) {
1939
2292
  const fingerprint = fingerprintEventAttributes(split.fingerprint, redactor.text);
1940
2293
  if (fingerprint["autter.fingerprint"]) noteCompatFeature("fingerprint_override");
1941
2294
  const redacted = redactor(split.attributes);
1942
- const activeSpan = trace2.getActiveSpan();
1943
- const reuseActive = activeSpan?.isRecording() === true;
2295
+ const crash = attributes?.["autter.unhandled"] === true;
2296
+ const activeSpan = trace3.getActiveSpan();
2297
+ const reuseActive = !crash && activeSpan?.isRecording() === true;
1944
2298
  const span = reuseActive ? activeSpan : errorTracer.startSpan(isErr ? error.name : "Error");
2299
+ const scope = reuseActive ? void 0 : requestScopeAttributes();
1945
2300
  const structured = structuredErrorAttributes(error);
1946
2301
  const causes = errorAttributes(error);
1947
2302
  const declared = {
@@ -1950,6 +2305,7 @@ function initAutterServer(options) {
1950
2305
  };
1951
2306
  const requestId = currentOperation()?.requestId;
1952
2307
  span.setAttributes({
2308
+ ...scope,
1953
2309
  "autter.severity": "error",
1954
2310
  ...redacted,
1955
2311
  ...structured,
@@ -2006,7 +2362,7 @@ function initAutterServer(options) {
2006
2362
  if (!errorFilter({ type: "OutcomeFailure", message: `${name}: ${message}` })) return;
2007
2363
  errorTraceBuffer?.retainActiveTrace();
2008
2364
  telemetryStats.markCaptured();
2009
- const activeSpan = trace2.getActiveSpan();
2365
+ const activeSpan = trace3.getActiveSpan();
2010
2366
  const reuseActive = activeSpan?.isRecording() === true;
2011
2367
  const span = reuseActive ? activeSpan : errorTracer.startSpan("OutcomeFailure");
2012
2368
  const safeOutcome = redactor({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
@@ -2035,7 +2391,7 @@ function initAutterServer(options) {
2035
2391
  const split = splitFingerprint(attributes);
2036
2392
  const fingerprint = fingerprintEventAttributes(split.fingerprint, redactor.text);
2037
2393
  const redacted = redactor(split.attributes);
2038
- const activeSpan = trace2.getActiveSpan();
2394
+ const activeSpan = trace3.getActiveSpan();
2039
2395
  const reuseActive = activeSpan?.isRecording() === true;
2040
2396
  const span = reuseActive ? activeSpan : errorTracer.startSpan(isErr ? error.name : "Error");
2041
2397
  markCaptured(error);
@@ -2054,7 +2410,7 @@ function initAutterServer(options) {
2054
2410
  }
2055
2411
  configureRuntimeLogger({
2056
2412
  endpoint,
2057
- apiKey: options.apiKey,
2413
+ apiKey: apiKey ?? "",
2058
2414
  service: options.service,
2059
2415
  environment,
2060
2416
  distro,
@@ -2065,6 +2421,8 @@ function initAutterServer(options) {
2065
2421
  reportOutcome: reportOutcome2
2066
2422
  });
2067
2423
  S.hookRequests = options.logging?.requests === true;
2424
+ S.expressErrors = options.captureExpressErrors !== false;
2425
+ if (S.expressErrors) installExpressErrorCapture();
2068
2426
  S.handled = captureHandled;
2069
2427
  startLoggedErrorCapture(options.captureLoggedErrors);
2070
2428
  let onCrash = null;
@@ -2123,6 +2481,7 @@ function initAutterServer(options) {
2123
2481
  S.alwaysOnProvider = null;
2124
2482
  S.handled = null;
2125
2483
  S.hookRequests = false;
2484
+ S.expressErrors = false;
2126
2485
  S.redactor = makeRedactor(true);
2127
2486
  }
2128
2487
  autoFlushHandle?.dispose();
@@ -2151,7 +2510,7 @@ function captureException(error, attributes) {
2151
2510
  const split = splitFingerprint(attributes);
2152
2511
  const fingerprint = fingerprintEventAttributes(split.fingerprint, S.redactor.text);
2153
2512
  notifyException(error, { ...split.attributes, ...fingerprint });
2154
- const span = trace2.getTracer("autter-errors").startSpan(isError(error) ? error.name : "Error", {
2513
+ const span = trace3.getTracer("autter-errors").startSpan(isError(error) ? error.name : "Error", {
2155
2514
  attributes: { ...S.redactor(split.attributes), ...fingerprint }
2156
2515
  });
2157
2516
  recordError(span, error);
@@ -2173,7 +2532,7 @@ function captureHandledError(error, attributes, severity) {
2173
2532
  markCaptured(error);
2174
2533
  const split = splitFingerprint(attributes);
2175
2534
  notifyException(error, { ...split.attributes, "autter.severity": severity });
2176
- const span = trace2.getTracer("autter-errors").startSpan(isError(error) ? error.name : "Error");
2535
+ const span = trace3.getTracer("autter-errors").startSpan(isError(error) ? error.name : "Error");
2177
2536
  span.addEvent("exception", {
2178
2537
  ...S.redactor(split.attributes),
2179
2538
  "exception.type": isError(error) ? error.name : "Error",
@@ -2191,13 +2550,13 @@ function serverActive() {
2191
2550
  setRequestExceptionCapturer((error, attributes) => captureException(error, attributes));
2192
2551
  setHandledCapture(captureHandledError);
2193
2552
  setRequestSpanAnnotator((attributes) => {
2194
- const span = trace2.getActiveSpan();
2553
+ const span = trace3.getActiveSpan();
2195
2554
  if (span?.isRecording()) span.setAttributes(S.redactor(splitFingerprint(attributes).attributes));
2196
2555
  });
2197
2556
  function withProcessSpan(name, fn, attributes) {
2198
2557
  if (S.active) return S.active.withProcessSpan(name, fn, attributes);
2199
2558
  return runWithSpan(
2200
- trace2.getTracer("autter-processes"),
2559
+ trace3.getTracer("autter-processes"),
2201
2560
  name,
2202
2561
  fn,
2203
2562
  S.redactor(attributes)
@@ -2205,14 +2564,14 @@ function withProcessSpan(name, fn, attributes) {
2205
2564
  }
2206
2565
  function withLlmCall(info, fn) {
2207
2566
  if (S.active) return S.active.withLlmCall(info, fn);
2208
- return runLlmSpan(trace2.getTracer("autter-llm"), info, fn);
2567
+ return runLlmSpan(trace3.getTracer("autter-llm"), info, fn);
2209
2568
  }
2210
2569
  function trackLlmCall(call) {
2211
2570
  if (S.active) {
2212
2571
  S.active.trackLlmCall(call);
2213
2572
  return;
2214
2573
  }
2215
- recordLlmCall(trace2.getTracer("autter-llm"), call);
2574
+ recordLlmCall(trace3.getTracer("autter-llm"), call);
2216
2575
  }
2217
2576
  function captureMessage(message, severity = "warning", attributes) {
2218
2577
  if (S.active) {
@@ -2220,7 +2579,7 @@ function captureMessage(message, severity = "warning", attributes) {
2220
2579
  return;
2221
2580
  }
2222
2581
  const safeMessage = S.redactor.text(message);
2223
- const span = trace2.getTracer("autter-errors").startSpan("Message", {
2582
+ const span = trace3.getTracer("autter-errors").startSpan("Message", {
2224
2583
  attributes: { "autter.severity": severity, ...S.redactor(attributes) }
2225
2584
  });
2226
2585
  span.addEvent("exception", {
@@ -2234,7 +2593,7 @@ function captureMessage(message, severity = "warning", attributes) {
2234
2593
  }
2235
2594
  function reportOutcome(name, message, attributes) {
2236
2595
  if (S.active) return S.active.reportOutcome(name, message, attributes);
2237
- const span = trace2.getTracer("autter-outcomes").startSpan("OutcomeFailure");
2596
+ const span = trace3.getTracer("autter-outcomes").startSpan("OutcomeFailure");
2238
2597
  const safeOutcome = redactAttributes({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
2239
2598
  const safeName = String(safeOutcome["autter.outcome.name"] ?? "operation");
2240
2599
  const safeMessage = String(safeOutcome["autter.outcome.message"] ?? "failed");
@@ -2255,7 +2614,7 @@ function makeSafeCapture(options) {
2255
2614
  };
2256
2615
  }
2257
2616
  function alwaysOnTracer(name) {
2258
- return S.alwaysOnProvider ? S.alwaysOnProvider.getTracer(name) : trace2.getTracer(name);
2617
+ return S.alwaysOnProvider ? S.alwaysOnProvider.getTracer(name) : trace3.getTracer(name);
2259
2618
  }
2260
2619
  function autterLlmTracer() {
2261
2620
  return alwaysOnTracer("autter-llm");
@@ -2275,7 +2634,7 @@ async function emitLlmSelftestTrace() {
2275
2634
  attributes: { "autter.selftest": true }
2276
2635
  },
2277
2636
  (llm) => {
2278
- traceId = trace2.getActiveSpan()?.spanContext().traceId ?? "";
2637
+ traceId = trace3.getActiveSpan()?.spanContext().traceId ?? "";
2279
2638
  llm.setUsage({ inputTokens: 1, outputTokens: 1 });
2280
2639
  llm.setCost(0);
2281
2640
  }
@@ -2596,7 +2955,7 @@ function autterKoa(options = {}) {
2596
2955
  // src/logging-only.ts
2597
2956
  import {
2598
2957
  SpanStatusCode as SpanStatusCode3,
2599
- trace as trace3
2958
+ trace as trace4
2600
2959
  } from "@opentelemetry/api";
2601
2960
  var L = sharedSlot("logging", () => ({ active: null }));
2602
2961
  function initAutterLogging(options) {
@@ -2631,7 +2990,7 @@ function initAutterLogging(options) {
2631
2990
  const split = splitFingerprint(attributes);
2632
2991
  const fingerprint = fingerprintEventAttributes(split.fingerprint, redact.text);
2633
2992
  const redacted = redact(split.attributes);
2634
- const span = trace3.getActiveSpan();
2993
+ const span = trace4.getActiveSpan();
2635
2994
  if (mode === "auto" && span?.isRecording()) {
2636
2995
  noteOperationError(error);
2637
2996
  notifyException(error, { ...redacted, ...fingerprint });
@@ -2672,7 +3031,7 @@ function initAutterLogging(options) {
2672
3031
  const fingerprint = fingerprintEventAttributes(split.fingerprint, redact.text);
2673
3032
  const redacted = redact(split.attributes);
2674
3033
  notifyException(error, { ...redacted, ...fingerprint, "autter.severity": severity });
2675
- const span = trace3.getActiveSpan();
3034
+ const span = trace4.getActiveSpan();
2676
3035
  if (mode === "auto" && span?.isRecording()) {
2677
3036
  span.addEvent("exception", {
2678
3037
  ...redacted,
@@ -2690,7 +3049,9 @@ function initAutterLogging(options) {
2690
3049
  const captured = /* @__PURE__ */ new WeakSet();
2691
3050
  configureRuntimeLogger({
2692
3051
  endpoint,
2693
- apiKey: options.apiKey ?? "",
3052
+ // Normalised ("undefined"/empty is no key), no env fallback: without
3053
+ // an apiKey this mode logs locally only, by design.
3054
+ apiKey: resolveIngestKey(options.apiKey, {}) ?? "",
2694
3055
  service: options.service,
2695
3056
  environment,
2696
3057
  ...options.release ? { release: options.release } : {},
@@ -2850,6 +3211,7 @@ export {
2850
3211
  captureException,
2851
3212
  captureMessage,
2852
3213
  captureRequestError,
3214
+ checkBrowserPayload,
2853
3215
  consoleSink,
2854
3216
  createBrowserRelayFetchHandler,
2855
3217
  createBrowserRelayHandler,
@@ -2877,6 +3239,7 @@ export {
2877
3239
  reportOutcome,
2878
3240
  runDoctor,
2879
3241
  runInBackground,
3242
+ runVerify,
2880
3243
  runtimeContext,
2881
3244
  runtimeLogStats,
2882
3245
  runtimeLogger,