@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.
@@ -1,3 +1,198 @@
1
+ // src/lifecycle.ts
2
+ import { ExportResultCode } from "@opentelemetry/core";
3
+ var debugEnabled = process.env.AUTTER_DEBUG === "1" || process.env.AUTTER_DEBUG === "true";
4
+ function setDebugMode(enabled) {
5
+ debugEnabled = enabled;
6
+ }
7
+ function isDebugEnabled() {
8
+ return debugEnabled;
9
+ }
10
+ function debugLog(...args) {
11
+ if (debugEnabled) {
12
+ console.error("[autter]", ...args);
13
+ }
14
+ }
15
+ var TelemetryStats = class {
16
+ /** Captures queued via our APIs and not yet seen leaving an exporter. */
17
+ pendingCaptures = 0;
18
+ markCaptured(count = 1) {
19
+ this.pendingCaptures += count;
20
+ }
21
+ /** An exporter batch left the process successfully. */
22
+ onExported(spanCount) {
23
+ this.pendingCaptures = Math.max(0, this.pendingCaptures - spanCount);
24
+ if (debugEnabled && spanCount > 0) {
25
+ debugLog(`exported ${spanCount} span${spanCount === 1 ? "" : "s"}`);
26
+ }
27
+ }
28
+ markAllFlushed() {
29
+ this.pendingCaptures = 0;
30
+ }
31
+ };
32
+ var telemetryStats = new TelemetryStats();
33
+ var CountingExporter = class {
34
+ constructor(inner) {
35
+ this.inner = inner;
36
+ }
37
+ inner;
38
+ export(spans, resultCallback) {
39
+ this.inner.export(spans, (result) => {
40
+ if (result.code === ExportResultCode.SUCCESS) {
41
+ telemetryStats.onExported(spans.length);
42
+ }
43
+ resultCallback(result);
44
+ });
45
+ }
46
+ shutdown() {
47
+ return this.inner.shutdown();
48
+ }
49
+ forceFlush() {
50
+ return this.inner.forceFlush?.() ?? Promise.resolve();
51
+ }
52
+ };
53
+ var flushTargets = /* @__PURE__ */ new Map();
54
+ function registerFlushTarget(key, target) {
55
+ flushTargets.set(key, target);
56
+ }
57
+ function unregisterFlushTargets() {
58
+ flushTargets.clear();
59
+ }
60
+ function activeFlushTargets() {
61
+ return [...flushTargets.values()];
62
+ }
63
+ var DEFAULT_SIGNALS = ["SIGINT", "SIGTERM"];
64
+ var SIGNAL_EXIT_CODES = {
65
+ SIGINT: 130,
66
+ SIGTERM: 143
67
+ };
68
+ function installAutterAutoFlush(options = {}) {
69
+ const logger = options.logger ?? console;
70
+ const log = options.log !== false;
71
+ const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
72
+ const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
73
+ const signals = options.signals ?? DEFAULT_SIGNALS;
74
+ const flushOnUncaughtException = options.flushOnUncaughtException !== false;
75
+ let disposed = false;
76
+ let inFlight = null;
77
+ const doFlush = async () => {
78
+ const targets = options.targets ?? activeFlushTargets();
79
+ if (targets.length === 0) return true;
80
+ let timer;
81
+ const timedOut = new Promise((resolve) => {
82
+ timer = setTimeout(() => resolve(false), timeoutMs);
83
+ });
84
+ const drained = Promise.allSettled(
85
+ targets.map(
86
+ (target) => Promise.resolve().then(() => target.forceFlush())
87
+ )
88
+ ).then(
89
+ (results) => results.every((result) => result.status === "fulfilled")
90
+ );
91
+ let ok;
92
+ try {
93
+ ok = await Promise.race([drained, timedOut]);
94
+ } finally {
95
+ clearTimeout(timer);
96
+ }
97
+ if (ok) {
98
+ telemetryStats.markAllFlushed();
99
+ if (log) {
100
+ logger.log("[autter] telemetry flushed");
101
+ }
102
+ } else {
103
+ logger.error(
104
+ `[autter] flush did not finish within ${timeoutMs}ms \u2014 some telemetry may be lost`
105
+ );
106
+ }
107
+ return ok;
108
+ };
109
+ const flush = (reason = "exit") => {
110
+ if (disposed) return Promise.resolve(false);
111
+ if (!inFlight) {
112
+ inFlight = doFlush().finally(() => {
113
+ inFlight = null;
114
+ });
115
+ }
116
+ if (isDebugEnabled()) debugLog(`auto-flush requested on ${reason}`);
117
+ return inFlight;
118
+ };
119
+ const signalCounts = /* @__PURE__ */ new Map();
120
+ let handlingUncaughtException = false;
121
+ const onUncaughtException = (error) => {
122
+ if (process.listenerCount("uncaughtException") > 1) return;
123
+ if (handlingUncaughtException) return;
124
+ handlingUncaughtException = true;
125
+ logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
126
+ const keepAlive = setTimeout(() => {
127
+ }, timeoutMs + 50);
128
+ void flush("uncaughtException").finally(() => {
129
+ clearTimeout(keepAlive);
130
+ process.exit(1);
131
+ });
132
+ };
133
+ const onSignal = (signal) => {
134
+ const count = (signalCounts.get(signal) ?? 0) + 1;
135
+ signalCounts.set(signal, count);
136
+ const exitCode = SIGNAL_EXIT_CODES[signal] ?? 1;
137
+ if (count > 1) {
138
+ process.exit(exitCode);
139
+ }
140
+ const otherHandlers = process.listenerCount(signal) - 1;
141
+ if (otherHandlers > 0) {
142
+ void flush(signal);
143
+ return;
144
+ }
145
+ void flush(signal).finally(() => {
146
+ process.exit(exitCode);
147
+ });
148
+ };
149
+ const MAX_BEFORE_EXIT_FLUSHES = 10;
150
+ let beforeExitFlushes = 0;
151
+ const onBeforeExit = () => {
152
+ if (disposed) return;
153
+ const targets = options.targets ?? activeFlushTargets();
154
+ const worthIt = beforeExitFlushes === 0 ? targets.length > 0 : telemetryStats.pendingCaptures > 0;
155
+ if (!worthIt || beforeExitFlushes >= MAX_BEFORE_EXIT_FLUSHES) return;
156
+ beforeExitFlushes++;
157
+ const keepAlive = setTimeout(() => {
158
+ }, timeoutMs);
159
+ void flush("beforeExit").finally(() => clearTimeout(keepAlive));
160
+ };
161
+ const onExit = () => {
162
+ if (!warnOnUnflushedExit || disposed) return;
163
+ const pending = telemetryStats.pendingCaptures;
164
+ if (pending > 0) {
165
+ logger.error(
166
+ `[autter] process exiting with ~${pending} captured item(s) never confirmed exported \u2014 await autter.shutdown() on graceful shutdown`
167
+ );
168
+ }
169
+ };
170
+ const signalHandlers = /* @__PURE__ */ new Map();
171
+ process.on("beforeExit", onBeforeExit);
172
+ if (flushOnUncaughtException) {
173
+ process.on("uncaughtException", onUncaughtException);
174
+ }
175
+ for (const signal of signals) {
176
+ const handler = () => onSignal(signal);
177
+ signalHandlers.set(signal, handler);
178
+ process.on(signal, handler);
179
+ }
180
+ process.on("exit", onExit);
181
+ return {
182
+ flush,
183
+ dispose() {
184
+ disposed = true;
185
+ process.removeListener("beforeExit", onBeforeExit);
186
+ process.removeListener("uncaughtException", onUncaughtException);
187
+ process.removeListener("exit", onExit);
188
+ for (const [signal, handler] of signalHandlers) {
189
+ process.removeListener(signal, handler);
190
+ }
191
+ signalHandlers.clear();
192
+ }
193
+ };
194
+ }
195
+
1
196
  // ../otlp-ingester/src/compat-manifest.json
2
197
  var compat_manifest_default = {
3
198
  manifestVersion: 1,
@@ -350,201 +545,6 @@ function ingesterInfoFromReport(body) {
350
545
  };
351
546
  }
352
547
 
353
- // src/lifecycle.ts
354
- import { ExportResultCode } from "@opentelemetry/core";
355
- var debugEnabled = process.env.AUTTER_DEBUG === "1" || process.env.AUTTER_DEBUG === "true";
356
- function setDebugMode(enabled) {
357
- debugEnabled = enabled;
358
- }
359
- function isDebugEnabled() {
360
- return debugEnabled;
361
- }
362
- function debugLog(...args) {
363
- if (debugEnabled) {
364
- console.error("[autter]", ...args);
365
- }
366
- }
367
- var TelemetryStats = class {
368
- /** Captures queued via our APIs and not yet seen leaving an exporter. */
369
- pendingCaptures = 0;
370
- markCaptured(count = 1) {
371
- this.pendingCaptures += count;
372
- }
373
- /** An exporter batch left the process successfully. */
374
- onExported(spanCount) {
375
- this.pendingCaptures = Math.max(0, this.pendingCaptures - spanCount);
376
- if (debugEnabled && spanCount > 0) {
377
- debugLog(`exported ${spanCount} span${spanCount === 1 ? "" : "s"}`);
378
- }
379
- }
380
- markAllFlushed() {
381
- this.pendingCaptures = 0;
382
- }
383
- };
384
- var telemetryStats = new TelemetryStats();
385
- var CountingExporter = class {
386
- constructor(inner) {
387
- this.inner = inner;
388
- }
389
- inner;
390
- export(spans, resultCallback) {
391
- this.inner.export(spans, (result) => {
392
- if (result.code === ExportResultCode.SUCCESS) {
393
- telemetryStats.onExported(spans.length);
394
- }
395
- resultCallback(result);
396
- });
397
- }
398
- shutdown() {
399
- return this.inner.shutdown();
400
- }
401
- forceFlush() {
402
- return this.inner.forceFlush?.() ?? Promise.resolve();
403
- }
404
- };
405
- var flushTargets = /* @__PURE__ */ new Map();
406
- function registerFlushTarget(key, target) {
407
- flushTargets.set(key, target);
408
- }
409
- function unregisterFlushTargets() {
410
- flushTargets.clear();
411
- }
412
- function activeFlushTargets() {
413
- return [...flushTargets.values()];
414
- }
415
- var DEFAULT_SIGNALS = ["SIGINT", "SIGTERM"];
416
- var SIGNAL_EXIT_CODES = {
417
- SIGINT: 130,
418
- SIGTERM: 143
419
- };
420
- function installAutterAutoFlush(options = {}) {
421
- const logger = options.logger ?? console;
422
- const log = options.log !== false;
423
- const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
424
- const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
425
- const signals = options.signals ?? DEFAULT_SIGNALS;
426
- const flushOnUncaughtException = options.flushOnUncaughtException !== false;
427
- let disposed = false;
428
- let inFlight = null;
429
- const doFlush = async () => {
430
- const targets = options.targets ?? activeFlushTargets();
431
- if (targets.length === 0) return true;
432
- let timer;
433
- const timedOut = new Promise((resolve) => {
434
- timer = setTimeout(() => resolve(false), timeoutMs);
435
- });
436
- const drained = Promise.allSettled(
437
- targets.map(
438
- (target) => Promise.resolve().then(() => target.forceFlush())
439
- )
440
- ).then(
441
- (results) => results.every((result) => result.status === "fulfilled")
442
- );
443
- let ok;
444
- try {
445
- ok = await Promise.race([drained, timedOut]);
446
- } finally {
447
- clearTimeout(timer);
448
- }
449
- if (ok) {
450
- telemetryStats.markAllFlushed();
451
- if (log) {
452
- logger.log("[autter] telemetry flushed");
453
- }
454
- } else {
455
- logger.error(
456
- `[autter] flush did not finish within ${timeoutMs}ms \u2014 some telemetry may be lost`
457
- );
458
- }
459
- return ok;
460
- };
461
- const flush = (reason = "exit") => {
462
- if (disposed) return Promise.resolve(false);
463
- if (!inFlight) {
464
- inFlight = doFlush().finally(() => {
465
- inFlight = null;
466
- });
467
- }
468
- if (isDebugEnabled()) debugLog(`auto-flush requested on ${reason}`);
469
- return inFlight;
470
- };
471
- const signalCounts = /* @__PURE__ */ new Map();
472
- let handlingUncaughtException = false;
473
- const onUncaughtException = (error) => {
474
- if (process.listenerCount("uncaughtException") > 1) return;
475
- if (handlingUncaughtException) return;
476
- handlingUncaughtException = true;
477
- logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
478
- const keepAlive = setTimeout(() => {
479
- }, timeoutMs + 50);
480
- void flush("uncaughtException").finally(() => {
481
- clearTimeout(keepAlive);
482
- process.exit(1);
483
- });
484
- };
485
- const onSignal = (signal) => {
486
- const count = (signalCounts.get(signal) ?? 0) + 1;
487
- signalCounts.set(signal, count);
488
- const exitCode = SIGNAL_EXIT_CODES[signal] ?? 1;
489
- if (count > 1) {
490
- process.exit(exitCode);
491
- }
492
- const otherHandlers = process.listenerCount(signal) - 1;
493
- if (otherHandlers > 0) {
494
- void flush(signal);
495
- return;
496
- }
497
- void flush(signal).finally(() => {
498
- process.exit(exitCode);
499
- });
500
- };
501
- const MAX_BEFORE_EXIT_FLUSHES = 10;
502
- let beforeExitFlushes = 0;
503
- const onBeforeExit = () => {
504
- if (disposed) return;
505
- const targets = options.targets ?? activeFlushTargets();
506
- const worthIt = beforeExitFlushes === 0 ? targets.length > 0 : telemetryStats.pendingCaptures > 0;
507
- if (!worthIt || beforeExitFlushes >= MAX_BEFORE_EXIT_FLUSHES) return;
508
- beforeExitFlushes++;
509
- const keepAlive = setTimeout(() => {
510
- }, timeoutMs);
511
- void flush("beforeExit").finally(() => clearTimeout(keepAlive));
512
- };
513
- const onExit = () => {
514
- if (!warnOnUnflushedExit || disposed) return;
515
- const pending = telemetryStats.pendingCaptures;
516
- if (pending > 0) {
517
- logger.error(
518
- `[autter] process exiting with ~${pending} captured item(s) never confirmed exported \u2014 await autter.shutdown() on graceful shutdown`
519
- );
520
- }
521
- };
522
- const signalHandlers = /* @__PURE__ */ new Map();
523
- process.on("beforeExit", onBeforeExit);
524
- if (flushOnUncaughtException) {
525
- process.on("uncaughtException", onUncaughtException);
526
- }
527
- for (const signal of signals) {
528
- const handler = () => onSignal(signal);
529
- signalHandlers.set(signal, handler);
530
- process.on(signal, handler);
531
- }
532
- process.on("exit", onExit);
533
- return {
534
- flush,
535
- dispose() {
536
- disposed = true;
537
- process.removeListener("beforeExit", onBeforeExit);
538
- process.removeListener("uncaughtException", onUncaughtException);
539
- process.removeListener("exit", onExit);
540
- for (const [signal, handler] of signalHandlers) {
541
- process.removeListener(signal, handler);
542
- }
543
- signalHandlers.clear();
544
- }
545
- };
546
- }
547
-
548
548
  // src/compat.ts
549
549
  import { request as httpRequest } from "http";
550
550
  import { request as httpsRequest } from "https";
@@ -552,7 +552,7 @@ import { request as httpsRequest } from "https";
552
552
  // package.json
553
553
  var package_default = {
554
554
  name: "@autter/runtime-node",
555
- version: "1.9.0",
555
+ version: "1.10.0",
556
556
  description: "Autter Runtime for Node.js: request wide events, coded errors, operation logging, same-origin browser relay and a curated OpenTelemetry server tracker",
557
557
  license: "MIT",
558
558
  type: "module",
@@ -605,6 +605,7 @@ var package_default = {
605
605
  "@types/node": "^22.10.0",
606
606
  bunyan: "^1.8.15",
607
607
  express: "^4.21.2",
608
+ express5: "npm:express@^5.3.0",
608
609
  fastify: "^5.12.5",
609
610
  koa: "^3.2.1",
610
611
  pino: "^10.4.0",
@@ -926,12 +927,103 @@ function noteRelayRejection(eventTypes, status, headers) {
926
927
  }
927
928
  }
928
929
 
930
+ // src/ingest-auth.ts
931
+ var NOT_A_KEY = /* @__PURE__ */ new Set(["", "undefined", "null"]);
932
+ function resolveIngestKey(explicit, env = process.env) {
933
+ for (const candidate of [explicit, env.AUTTER_RUNTIME_KEY]) {
934
+ if (typeof candidate !== "string") continue;
935
+ const key = candidate.trim();
936
+ if (!NOT_A_KEY.has(key.toLowerCase())) return key;
937
+ }
938
+ return null;
939
+ }
940
+ function authHeaders(key) {
941
+ return key ? { authorization: `Bearer ${key}` } : {};
942
+ }
943
+ var defaultWarn2 = (message) => console.warn(`[autter-runtime] ${message}`);
944
+ var state = sharedSlot("ingest-auth", () => ({ warned: /* @__PURE__ */ new Set(), warn: defaultWarn2 }));
945
+ function once(id, message) {
946
+ if (state.warned.has(id)) return false;
947
+ state.warned.add(id);
948
+ try {
949
+ state.warn(message);
950
+ } catch {
951
+ }
952
+ return true;
953
+ }
954
+ var VERIFY_HINT = "Then run: npx @autter/runtime-node verify";
955
+ function warnMissingKey(where, service) {
956
+ const what = where === "relay" ? "the browser relay has no ingest key (apiKey is unset and AUTTER_RUNTIME_KEY is not set), so browser events are refused (500) instead of being sent" : `initAutterServer() got no apiKey and AUTTER_RUNTIME_KEY is not set, so traces, metrics and logs${service ? ` from "${service}"` : ""} are not sent`;
957
+ return once(
958
+ `missing:${where}`,
959
+ `No ingest key: ${what}. Set AUTTER_RUNTIME_KEY to a server key (autter_rt_\u2026). ${VERIFY_HINT}`
960
+ );
961
+ }
962
+ function warnKeyKind(key) {
963
+ if (!key) return false;
964
+ if (key.startsWith("autter_rtc_")) {
965
+ return once(
966
+ "kind:client",
967
+ "AUTTER_RUNTIME_KEY is a client (browser) key (autter_rtc_\u2026). Server SDKs and the relay need a server key (autter_rt_\u2026); the ingester refuses this one with 403."
968
+ );
969
+ }
970
+ if (key.startsWith("autter_rtu_")) {
971
+ return once(
972
+ "kind:upload",
973
+ "AUTTER_RUNTIME_KEY is an upload-only key (autter_rtu_\u2026) for source maps. Server SDKs need a server key (autter_rt_\u2026); the ingester refuses this one with 403."
974
+ );
975
+ }
976
+ return false;
977
+ }
978
+ function refusalReason(body) {
979
+ let text = "";
980
+ if (typeof body === "string") {
981
+ try {
982
+ const parsed = JSON.parse(body);
983
+ text = [parsed.error, parsed.detail].filter((v) => typeof v === "string" && v).join(": ");
984
+ } catch {
985
+ text = body;
986
+ }
987
+ }
988
+ return text.replace(/\s+/g, " ").trim().slice(0, 300);
989
+ }
990
+ function noteIngestRefusal(input) {
991
+ try {
992
+ const { status, route, endpoint } = input;
993
+ const reason = refusalReason(input.body);
994
+ const said = reason ? ` (${status}: ${reason})` : ` (${status})`;
995
+ const who = input.via === "relay" ? "browser events forwarded by the relay are" : "this data is";
996
+ if (status === 401) {
997
+ return once(
998
+ "status:401",
999
+ `The ingester at ${endpoint} refused the ingest key on ${route}${said}. Nothing is stored until it is fixed: ${who} dropped. Check AUTTER_RUNTIME_KEY (a server key, autter_rt_\u2026, for this repository). ${VERIFY_HINT}`
1000
+ );
1001
+ }
1002
+ if (status === 403) {
1003
+ return once(
1004
+ `status:403:${route}`,
1005
+ `The ingester at ${endpoint} refused ${route}${said}; ${who} dropped. ${VERIFY_HINT}`
1006
+ );
1007
+ }
1008
+ if (status === 404) {
1009
+ return once(
1010
+ `status:404:${route}`,
1011
+ `${endpoint}${route} answered 404: this ingester has no ${route} route, so ${who} dropped. Either the ingester is older than the feature (run: npx @autter/runtime-node doctor) or the endpoint has an extra path (set it to the ingester's base URL).`
1012
+ );
1013
+ }
1014
+ if (status === 400 || status === 413 || status === 415) {
1015
+ return once(
1016
+ `status:${status}:${route}`,
1017
+ `The ingester at ${endpoint} rejected a ${route} batch${said}; ${who} dropped.`
1018
+ );
1019
+ }
1020
+ return false;
1021
+ } catch {
1022
+ return false;
1023
+ }
1024
+ }
1025
+
929
1026
  export {
930
- COMPAT_MANIFEST,
931
- INGESTER_VERSION_HEADER,
932
- versionAtLeast,
933
- featuresForBrowserEvents,
934
- evaluateCompat,
935
1027
  setDebugMode,
936
1028
  isDebugEnabled,
937
1029
  debugLog,
@@ -941,6 +1033,17 @@ export {
941
1033
  unregisterFlushTargets,
942
1034
  installAutterAutoFlush,
943
1035
  sharedSlot,
1036
+ resolveIngestKey,
1037
+ authHeaders,
1038
+ warnMissingKey,
1039
+ warnKeyKind,
1040
+ refusalReason,
1041
+ noteIngestRefusal,
1042
+ COMPAT_MANIFEST,
1043
+ INGESTER_VERSION_HEADER,
1044
+ versionAtLeast,
1045
+ featuresForBrowserEvents,
1046
+ evaluateCompat,
944
1047
  SDK_IDENTITY,
945
1048
  compatRequest,
946
1049
  fetchIngesterCompat,