@autter/otlp-ingester 1.6.0 → 1.7.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,7 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { scrubText } from "./redact.js";
3
- /** Largest stored map (after `sourcesContent` is removed), in characters. */
4
- export const SOURCE_MAP_MAX_CHARS = 5 * 1024 * 1024;
3
+ /**
4
+ * Largest stored map (after `sourcesContent` is removed), in characters:
5
+ * 32 MiB, room for vendor chunks of about 20 MB of minified code. Storage
6
+ * is a ZSTD-compressed String column, so size only costs disk; a map this
7
+ * big is still symbolicated, without being kept in the parse cache unless
8
+ * SOURCE_MAP_CACHE_MB allows it (symbolicate.ts). Was 5 MiB up to 1.6.0.
9
+ */
10
+ export const SOURCE_MAP_MAX_CHARS = 32 * 1024 * 1024;
11
+ /** `/v1/sourcemaps` body limit, counted after a gzip or deflate body is
12
+ * inflated: the largest map plus room for the rest of the request. */
13
+ export const SOURCE_MAP_MAX_BODY_BYTES = SOURCE_MAP_MAX_CHARS + 1024 * 1024;
5
14
  /** Bundles a map can be uploaded for: `.js`, `.mjs` and `.cjs` (1.6.0+). */
6
15
  const BUNDLE_PATH_RE = /\.[cm]?js$/;
7
16
  /** A debug id: 8-4-4-4-12 lowercase hex. Written by `autter-runtime
@@ -83,7 +92,10 @@ export function validateSourceMap(body) {
83
92
  const debugId = rawDebugId === undefined || rawDebugId === null || rawDebugId === "" ? "" : normalizeDebugId(rawDebugId);
84
93
  if (debugId === null)
85
94
  return null;
86
- // Source contents are unnecessary for position lookup and may contain secrets.
95
+ // Source contents are unnecessary for position lookup and may contain
96
+ // secrets. Everything else is stored as sent, extension fields included:
97
+ // symbolication reads `x_autter_scopes` (function scopes) from it, and the
98
+ // CLI hashes exactly this JSON to skip maps already uploaded.
87
99
  const { sourcesContent: _omitted, ...safe } = parsed;
88
100
  const map = JSON.stringify(safe);
89
101
  if (map.length > SOURCE_MAP_MAX_CHARS)
@@ -94,9 +106,12 @@ export function validateSourceMap(body) {
94
106
  export const SOURCE_MAP_CHECK_MAX = 500;
95
107
  /**
96
108
  * Validate a `/v1/sourcemaps/check` request:
97
- * `{ release, files: [{ filename, debugId?, hash }] }` (≤ 500 files). The
109
+ * `{ release, files: [{ filename, debugId?, hash? }] }` (≤ 500 files). The
98
110
  * filenames and debug ids are normalised exactly as an upload would store
99
- * them. Null when invalid.
111
+ * them. Each file needs a hash (is this exact map stored? — what `upload`
112
+ * asks to skip maps) or, from 1.7.0, a debug id alone (is any map stored
113
+ * for this bundle? — what `sourcemaps verify` asks after the maps were
114
+ * deleted). Null when invalid.
100
115
  */
101
116
  export function validateSourceMapCheck(body) {
102
117
  if (!body || typeof body !== "object")
@@ -111,7 +126,10 @@ export function validateSourceMapCheck(body) {
111
126
  if (!raw || typeof raw !== "object")
112
127
  return null;
113
128
  const file = raw;
114
- if (typeof file.filename !== "string" || typeof file.hash !== "string" || !/^[0-9a-f]{64}$/.test(file.hash))
129
+ if (typeof file.filename !== "string")
130
+ return null;
131
+ const hash = file.hash === undefined || file.hash === null || file.hash === "" ? "" : file.hash;
132
+ if (typeof hash !== "string" || (hash !== "" && !/^[0-9a-f]{64}$/.test(hash)))
115
133
  return null;
116
134
  const filename = sourceMapPath(file.filename);
117
135
  if (!filename)
@@ -119,7 +137,9 @@ export function validateSourceMapCheck(body) {
119
137
  const debugId = file.debugId === undefined || file.debugId === null || file.debugId === "" ? "" : normalizeDebugId(file.debugId);
120
138
  if (debugId === null)
121
139
  return null;
122
- entries.push({ filename, debugId, hash: file.hash });
140
+ if (!hash && !debugId)
141
+ return null;
142
+ entries.push({ filename, debugId, hash });
123
143
  }
124
144
  return { release: input.release, entries };
125
145
  }
@@ -1,4 +1,6 @@
1
1
  import { TraceMap } from "@jridgewell/trace-mapping";
2
+ import { type GeneratedPosition, type MappedSegment } from "./source-map-mappings.js";
3
+ import { ScopeIndex } from "./source-map-scopes.js";
2
4
  import type { SourceMapKey } from "./source-maps-lookup.js";
3
5
  import type { StackFrame } from "./types.js";
4
6
  /**
@@ -7,11 +9,17 @@ import type { StackFrame } from "./types.js";
7
9
  * fingerprinted, so `frames-v3` hashes `checkout.tsx`, not
8
10
  * `index-5f3a9c1e.js` — and an issue survives deploys and preview domains.
9
11
  *
10
- * - Function names come from the map's `names`: the name mapped at the
11
- * CALLER's position is the callee's original name (the call site names
12
- * what it calls). Without one, a resolved frame keeps its parsed name
13
- * only when that is not an obviously mangled one (≤ 2 characters), since
14
- * a mangled name changes on every build.
12
+ * - Function names: when the map carries function scopes
13
+ * (`x_autter_scopes`, uploaded by @autter/runtime-cli after 1.0.0), a
14
+ * frame is named after the innermost function containing it, with that
15
+ * function's original name (null in an anonymous function or top-level
16
+ * code) — see source-map-scopes.ts. Maps without scopes keep the earlier
17
+ * rule: the name mapped at the CALLER's position is the callee's original
18
+ * name; without one, a resolved frame keeps its parsed name only when
19
+ * that is not an obviously mangled one (≤ 2 characters), since a mangled
20
+ * name changes on every build. That rule is wrong for calls through a
21
+ * variable (`const run = actions.captured; run()` names the frame `run`)
22
+ * and for handlers a framework calls, which is why scopes exist.
15
23
  * - Sources the map lists in `ignoreList` / `x_google_ignoreList` are not
16
24
  * in-app; a resolved path under node_modules/ etc. is not in-app either.
17
25
  * - Context lines only when the map carries `sourcesContent` (the upload
@@ -19,7 +27,10 @@ import type { StackFrame } from "./types.js";
19
27
  * - Never fails ingest: any error leaves the frame as parsed.
20
28
  *
21
29
  * Parsed maps are cached per (org, repository, release, file) — or per debug
22
- * id — in a byte-bounded LRU, so a busy issue never refetches its map.
30
+ * id — in a byte-bounded LRU, so a busy issue never refetches its map. A map
31
+ * that would take more than half of that budget is used for its lookup and
32
+ * not kept (source-map-mappings.ts reads it without decoding it), so one huge
33
+ * vendor chunk cannot flush every other map.
23
34
  */
24
35
  export type MapFetcher = (key: SourceMapKey) => Promise<string | null>;
25
36
  export interface SymbolicationScope {
@@ -32,7 +43,8 @@ export interface SymbolicationScope {
32
43
  /** Path-only form of a script URL — the key uploads are stored under. */
33
44
  export declare function scriptPath(filename: string): string | null;
34
45
  export interface SourceMapCacheOptions {
35
- /** Budget for the estimated in-memory size of parsed maps (default 64 MiB). */
46
+ /** Budget for the estimated in-memory size of parsed maps (default 64 MiB).
47
+ * A map that would take more than half of it is not cached. */
36
48
  maxBytes?: number;
37
49
  maxEntries?: number;
38
50
  /** How long a found map is reused (default 1 h). */
@@ -43,6 +55,19 @@ export interface SourceMapCacheOptions {
43
55
  errorTtlMs?: number;
44
56
  now?: () => number;
45
57
  }
58
+ /** A parsed map: its metadata, its function scopes, and position lookups. */
59
+ export interface LoadedSourceMap {
60
+ /** Sources, names, ignore list and sourcesContent. Its own `mappings`
61
+ * are left empty: positions go through `lookup`. */
62
+ map: TraceMap;
63
+ scopes: ScopeIndex | null;
64
+ /** Original positions for 0-based generated positions (one call per stack). */
65
+ lookup(positions: readonly GeneratedPosition[]): Array<MappedSegment | null>;
66
+ /** Estimated heap: decoded mappings (when decoded), strings and scopes. */
67
+ bytes: number;
68
+ /** False when the map was too big to keep, so its mappings were not decoded. */
69
+ cacheable: boolean;
70
+ }
46
71
  export declare class SourceMapCache {
47
72
  private readonly entries;
48
73
  private readonly inFlight;
@@ -55,15 +80,28 @@ export declare class SourceMapCache {
55
80
  private readonly now;
56
81
  /** Fetches made (cache misses) — for tests and /healthz-style debugging. */
57
82
  fetches: number;
83
+ /** Maps used without being cached because they were too big to keep. */
84
+ bypassed: number;
58
85
  constructor(options?: SourceMapCacheOptions);
59
86
  static keyOf(key: SourceMapKey): string;
60
87
  get size(): number;
88
+ /** Estimated bytes of the cached maps (what `maxBytes` bounds). */
89
+ get bytes(): number;
61
90
  /** The parsed map for `key` (null when none), fetching at most once at a time. */
62
- get(key: SourceMapKey, fetchMap: MapFetcher): Promise<TraceMap | null>;
91
+ get(key: SourceMapKey, fetchMap: MapFetcher): Promise<LoadedSourceMap | null>;
63
92
  private load;
64
93
  private store;
65
94
  private remove;
66
95
  }
96
+ /**
97
+ * Parse a stored map once: metadata, scopes (null when absent or malformed
98
+ * — a bad scope field never loses the map) and position lookups. When the
99
+ * estimated heap is at most `maxDecodedBytes`, the mappings are decoded
100
+ * once into a compact table; otherwise they are left encoded and each
101
+ * lookup reads them in one pass (`cacheable: false`). Throws when the map
102
+ * itself is unusable.
103
+ */
104
+ export declare function parseSourceMap(json: string, maxDecodedBytes?: number): LoadedSourceMap;
67
105
  export declare class Symbolicator {
68
106
  private readonly fetchMap;
69
107
  readonly cache: SourceMapCache;
@@ -1,4 +1,6 @@
1
- import { isIgnored, originalPositionFor, sourceContentFor, TraceMap, } from "@jridgewell/trace-mapping";
1
+ import { isIgnored, sourceContentFor, TraceMap, } from "@jridgewell/trace-mapping";
2
+ import { DecodedMappings, decodedMappingsBytes, findInMappings, } from "./source-map-mappings.js";
3
+ import { SCOPES_KEY, ScopeIndex } from "./source-map-scopes.js";
2
4
  import { isInAppPath, stripQueryAndHash } from "./stack-frames.js";
3
5
  const JS_FILE_RE = /\.[cm]?js$/i;
4
6
  const MAX_CONTEXT_CHARS = 300;
@@ -13,10 +15,11 @@ export function scriptPath(filename) {
13
15
  }
14
16
  }
15
17
  /**
16
- * Decoded mappings take several times the JSON's size; the budget counts an
17
- * estimate so a handful of large bundles cannot exhaust a small container.
18
+ * The largest share of the budget one map may take. A bigger one is used
19
+ * for its lookup and dropped: keeping it would evict most other maps, and
20
+ * every event in those files would then refetch theirs.
18
21
  */
19
- const DECODED_WEIGHT_FACTOR = 8;
22
+ const MAX_SHARE_OF_BUDGET = 0.5;
20
23
  export class SourceMapCache {
21
24
  entries = new Map();
22
25
  inFlight = new Map();
@@ -29,6 +32,8 @@ export class SourceMapCache {
29
32
  now;
30
33
  /** Fetches made (cache misses) — for tests and /healthz-style debugging. */
31
34
  fetches = 0;
35
+ /** Maps used without being cached because they were too big to keep. */
36
+ bypassed = 0;
32
37
  constructor(options = {}) {
33
38
  this.maxBytes = options.maxBytes ?? 64 * 1024 * 1024;
34
39
  this.maxEntries = options.maxEntries ?? 500;
@@ -46,6 +51,10 @@ export class SourceMapCache {
46
51
  get size() {
47
52
  return this.entries.size;
48
53
  }
54
+ /** Estimated bytes of the cached maps (what `maxBytes` bounds). */
55
+ get bytes() {
56
+ return this.weight;
57
+ }
49
58
  /** The parsed map for `key` (null when none), fetching at most once at a time. */
50
59
  async get(key, fetchMap) {
51
60
  const id = SourceMapCache.keyOf(key);
@@ -81,17 +90,20 @@ export class SourceMapCache {
81
90
  this.store(id, null, 0, this.missTtlMs);
82
91
  return null;
83
92
  }
84
- let map;
93
+ let loaded;
85
94
  try {
86
- map = new TraceMap(json);
95
+ loaded = parseSourceMap(json, this.maxBytes * MAX_SHARE_OF_BUDGET);
87
96
  }
88
97
  catch {
89
98
  // A malformed upload is a miss until it is replaced.
90
99
  this.store(id, null, 0, this.missTtlMs);
91
100
  return null;
92
101
  }
93
- this.store(id, map, json.length * DECODED_WEIGHT_FACTOR, this.hitTtlMs);
94
- return map;
102
+ if (loaded.cacheable)
103
+ this.store(id, loaded, loaded.bytes, this.hitTtlMs);
104
+ else
105
+ this.bypassed += 1;
106
+ return loaded;
95
107
  }
96
108
  store(id, map, weight, ttlMs) {
97
109
  // A map bigger than the whole budget serves this lookup but is not kept:
@@ -115,6 +127,61 @@ export class SourceMapCache {
115
127
  this.weight -= entry.weight;
116
128
  }
117
129
  }
130
+ /** Rough heap of a list of strings (V8 strings plus array slots). */
131
+ function stringsBytes(list) {
132
+ if (!Array.isArray(list))
133
+ return 0;
134
+ let bytes = 16;
135
+ for (const item of list)
136
+ bytes += 8 + (typeof item === "string" ? 2 * item.length + 24 : 0);
137
+ return bytes;
138
+ }
139
+ /**
140
+ * Parse a stored map once: metadata, scopes (null when absent or malformed
141
+ * — a bad scope field never loses the map) and position lookups. When the
142
+ * estimated heap is at most `maxDecodedBytes`, the mappings are decoded
143
+ * once into a compact table; otherwise they are left encoded and each
144
+ * lookup reads them in one pass (`cacheable: false`). Throws when the map
145
+ * itself is unusable.
146
+ */
147
+ export function parseSourceMap(json, maxDecodedBytes = Number.POSITIVE_INFINITY) {
148
+ const raw = JSON.parse(json);
149
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
150
+ throw new Error("not a source map");
151
+ const fields = raw;
152
+ const { mappings } = fields;
153
+ if (typeof mappings !== "string")
154
+ throw new Error("no mappings");
155
+ // TraceMap copies only the standard fields, but given an object that has
156
+ // its private `_decodedMemo` it would return that object as is. It gets
157
+ // no mappings: decoding them is this module's job.
158
+ delete fields._decodedMemo;
159
+ const map = new TraceMap({ ...fields, mappings: "" });
160
+ let scopes = null;
161
+ try {
162
+ scopes = ScopeIndex.from(fields[SCOPES_KEY], map.names);
163
+ }
164
+ catch {
165
+ scopes = null;
166
+ }
167
+ const bytes = decodedMappingsBytes(mappings) +
168
+ stringsBytes(map.names) +
169
+ stringsBytes(map.sources) +
170
+ stringsBytes(map.resolvedSources) +
171
+ stringsBytes(map.sourcesContent) +
172
+ (scopes?.bytes ?? 0);
173
+ if (bytes <= maxDecodedBytes) {
174
+ const decoded = DecodedMappings.decode(mappings);
175
+ return {
176
+ map,
177
+ scopes,
178
+ bytes,
179
+ cacheable: true,
180
+ lookup: (positions) => positions.map((position) => decoded.find(position.line, position.column)),
181
+ };
182
+ }
183
+ return { map, scopes, bytes, cacheable: false, lookup: (positions) => findInMappings(mappings, positions) };
184
+ }
118
185
  // ── Resolution ────────────────────────────────────────────────────────────
119
186
  /** A name a minifier evidently produced ("a", "Xe", "Object.t"). */
120
187
  function looksMangled(name) {
@@ -201,24 +268,20 @@ export class Symbolicator {
201
268
  if (!keys.some(Boolean))
202
269
  return frames;
203
270
  const maps = await Promise.all(keys.map((key) => (key ? this.cache.get(key, this.fetchMap) : Promise.resolve(null))));
204
- const resolutions = frames.map((frame, i) => {
205
- const map = maps[i];
206
- if (!map)
207
- return null;
208
- try {
209
- return resolveFrame(map, frame);
210
- }
211
- catch {
212
- return null;
213
- }
214
- });
271
+ const resolutions = resolveFrames(frames, maps);
215
272
  if (!resolutions.some(Boolean))
216
273
  return frames;
217
274
  return frames.map((frame, i) => {
218
275
  const resolution = resolutions[i];
219
276
  if (!resolution)
220
277
  return frame;
221
- // Innermost first: the caller is the next frame.
278
+ const { scope } = resolution;
279
+ if (scope?.kind === "named")
280
+ return { ...resolution.frame, function: scope.name };
281
+ if (scope?.kind === "anonymous")
282
+ return { ...resolution.frame, function: null };
283
+ // No scopes (an older upload), or a function whose name the CLI
284
+ // could not map: innermost first, so the caller is the next frame.
222
285
  const callerName = resolutions[i + 1]?.nameHere ?? null;
223
286
  const parsed = frame.function;
224
287
  const name = callerName ?? (parsed && !looksMangled(parsed) ? parsed : null);
@@ -226,32 +289,75 @@ export class Symbolicator {
226
289
  });
227
290
  }
228
291
  }
229
- function resolveFrame(map, frame) {
230
- const position = originalPositionFor(map, {
231
- line: frame.lineno,
232
- // Stack columns are 1-based, source map columns 0-based.
233
- column: Math.max(0, (frame.colno ?? 1) - 1),
292
+ /** A frame's 0-based generated position (stack lines and columns are 1-based). */
293
+ function generatedPosition(frame) {
294
+ return { line: frame.lineno - 1, column: Math.max(0, (frame.colno ?? 1) - 1) };
295
+ }
296
+ /** Resolve every frame that has a map, with one lookup per map: a map too
297
+ * big to keep is read in a single pass for the whole stack. */
298
+ function resolveFrames(frames, maps) {
299
+ const resolutions = frames.map(() => null);
300
+ const byMap = new Map();
301
+ maps.forEach((loaded, index) => {
302
+ if (!loaded)
303
+ return;
304
+ const indexes = byMap.get(loaded);
305
+ if (indexes)
306
+ indexes.push(index);
307
+ else
308
+ byMap.set(loaded, [index]);
234
309
  });
235
- if (!position.source || position.line == null)
310
+ for (const [loaded, indexes] of byMap) {
311
+ const positions = indexes.map((index) => generatedPosition(frames[index]));
312
+ let segments;
313
+ try {
314
+ segments = loaded.lookup(positions);
315
+ }
316
+ catch {
317
+ continue;
318
+ }
319
+ indexes.forEach((frameIndex, k) => {
320
+ try {
321
+ resolutions[frameIndex] = resolveFrame(loaded, frames[frameIndex], positions[k], segments[k] ?? null);
322
+ }
323
+ catch {
324
+ resolutions[frameIndex] = null;
325
+ }
326
+ });
327
+ }
328
+ return resolutions;
329
+ }
330
+ function resolveFrame(loaded, frame, position, segment) {
331
+ const { map } = loaded;
332
+ if (!segment)
333
+ return null;
334
+ const source = map.resolvedSources[segment.source];
335
+ if (!source)
236
336
  return null;
237
- const filename = stripQueryAndHash(position.source);
337
+ const filename = stripQueryAndHash(source);
238
338
  const ignored = (() => {
239
339
  try {
240
- return isIgnored(map, position.source);
340
+ return isIgnored(map, source);
241
341
  }
242
342
  catch {
243
343
  return false;
244
344
  }
245
345
  })();
346
+ const line = segment.line + 1;
246
347
  const resolved = {
247
348
  function: frame.function,
248
349
  module: null,
249
350
  filename,
250
- lineno: position.line,
251
- colno: position.column == null ? null : position.column + 1,
351
+ lineno: line,
352
+ colno: segment.column + 1,
252
353
  inApp: !ignored && isInAppPath(filename),
253
354
  resolved: true,
254
- ...contextFor(map, position.source, position.line),
355
+ ...contextFor(map, source, line),
356
+ };
357
+ const name = segment.name >= 0 ? map.names[segment.name] : undefined;
358
+ return {
359
+ frame: resolved,
360
+ nameHere: typeof name === "string" ? name : null,
361
+ scope: loaded.scopes ? loaded.scopes.nameAt(position.line, position.column) : null,
255
362
  };
256
- return { frame: resolved, nameHere: position.name ?? null };
257
363
  }
@@ -0,0 +1,98 @@
1
+ import type { RuntimeOccurrenceInput } from "./types.js";
2
+ /**
3
+ * One error can be recorded on several spans of a trace. In a Next.js app a
4
+ * route handler's error is recorded by @autter/runtime-node on the request's
5
+ * HTTP server span AND by Next's own "executing api route" / render span
6
+ * (OpenTelemetry `recordException`); Express, Fastify, NestJS and gRPC
7
+ * instrumentations do the same, and errored ancestor spans with no event
8
+ * would add a stackless `HttpServerError` / `SpanError` on top. Each of
9
+ * those used to become its own occurrence, so one thrown error was counted
10
+ * two or three times. This pass, run on one export batch, keeps one
11
+ * occurrence per thrown error.
12
+ *
13
+ * Rule 1, duplicate recordings. Two exception occurrences are recordings of
14
+ * the same thrown error when ALL of these hold:
15
+ * - same trace, service, environment and release (a thrown error lives in
16
+ * one process);
17
+ * - on DIFFERENT spans, one an ancestor of the other through parent links
18
+ * present in the batch (an error propagating outward through nested
19
+ * spans). Two events on one span are two recordings made on purpose (a
20
+ * retry loop capturing each attempt on the request span) and are never
21
+ * merged; nor are errors on sibling spans (one span per attempt);
22
+ * - the same error type, or the OpenTelemetry JS `recordException`
23
+ * convention: a third-party recording (no `autter.*` on its event) of an
24
+ * error with a `code` reports the code as `exception.type` ("P2002",
25
+ * "ECONNREFUSED"), so its type may instead equal the other's declared
26
+ * `autter.error.code`, or its stack header may name the other's type;
27
+ * - the same message after scrubbing and `normalizeMessage`;
28
+ * - the same stack: the parsed in-app frames (function, file, line) are
29
+ * equal or one list is a prefix (truncated) or suffix (captured at record
30
+ * time, higher up) of the other. A side without a stack is no evidence
31
+ * against; without in-app frames all frames are compared;
32
+ * - recorded within DUPLICATE_WINDOW_MS of the group's latest recording.
33
+ * Recordings are grouped greedily in time order: each joins the eligible
34
+ * group (no member on its span, every member on its ancestor/descendant
35
+ * chain and matching it) whose latest recording is the most recent, so when
36
+ * every attempt of a retry loop is recorded on a child span and again on the
37
+ * parent, each attempt pairs with its own parent recording.
38
+ *
39
+ * The group keeps the occurrence with the most information: the one the
40
+ * Autter SDK recorded (`autter.*` on the event, or `autter.severity` on the
41
+ * span), then the one carrying runtime-next's `onRequestError` context, then
42
+ * the one on the span closest to the root. It keeps that occurrence's type,
43
+ * message, severity, time and batch position (so its deterministic id), and
44
+ * takes from the others what it lacks: attributes (its own win), stack,
45
+ * route, method, status, request id, declared error fields, fingerprint
46
+ * override and breadcrumbs. `autter.merged_recordings` counts the merged
47
+ * recordings.
48
+ *
49
+ * Rule 2, redundant status-only occurrences. A status-only `HttpServerError`
50
+ * / `SpanError` (an errored span without exception or failed-outcome events)
51
+ * is dropped when the same trace and service in this batch has an exception
52
+ * or failed outcome of severity error or fatal, not `autter.handled`, on an
53
+ * ancestor or descendant of its span: the span failed because of that
54
+ * error. It is kept when it is the trace's only signal, when the only
55
+ * errors are warnings or handled ones, or when no ancestor/descendant link
56
+ * is visible in the batch (sibling branches, a parent exported elsewhere).
57
+ *
58
+ * Scope: one export batch. A request's spans normally end, and are
59
+ * exported, together; when the exporter's timer splits them across batches,
60
+ * the recordings in different batches are still counted separately.
61
+ */
62
+ /** Two recordings of one thrown error happen as it propagates outward:
63
+ * milliseconds apart, seconds at most for an error handler that awaits. */
64
+ export declare const DUPLICATE_WINDOW_MS = 5000;
65
+ export interface TraceSignal {
66
+ occurrence: RuntimeOccurrenceInput;
67
+ traceId: string;
68
+ spanId: string;
69
+ /** What produced it: an `exception` event, a failed `autter.outcome`
70
+ * event, or the span's error status alone. */
71
+ signal: "exception" | "outcome" | "status";
72
+ /** The Autter SDK recorded it: `autter.*` on the event, or
73
+ * `autter.severity` (captureException's marker) on the span. */
74
+ autterCapture: boolean;
75
+ /** `autter.*` attributes on the event itself. */
76
+ eventAutter: boolean;
77
+ /** runtime-next's `onRequestError` context on the span or event. */
78
+ nextContext: boolean;
79
+ /** `autter.handled` on the event: handled on purpose, not why a span failed. */
80
+ handled: boolean;
81
+ }
82
+ export interface SpanLink {
83
+ traceId: string;
84
+ spanId: string;
85
+ parentSpanId: string | null;
86
+ }
87
+ export interface DedupedSignals {
88
+ /** The surviving occurrences, in batch order (merged where rule 1 applied). */
89
+ occurrences: RuntimeOccurrenceInput[];
90
+ /** Input index of each survivor: its position in the batch, which its
91
+ * deterministic occurrence id is derived from. */
92
+ sourceIndexes: number[];
93
+ /** Input indexes each survivor stands for (itself first). */
94
+ members: number[][];
95
+ }
96
+ export declare function hasNextContext(...sources: Array<Map<string, string>>): boolean;
97
+ export declare function hasAutterKey(attrs: Map<string, string>): boolean;
98
+ export declare function dedupeTraceSignals(signals: TraceSignal[], spans: SpanLink[]): DedupedSignals;