@autter/otlp-ingester 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +51 -0
  2. package/dist/breadcrumbs.d.ts +41 -0
  3. package/dist/breadcrumbs.js +151 -0
  4. package/dist/clickhouse.d.ts +42 -5
  5. package/dist/clickhouse.js +168 -7
  6. package/dist/compat-manifest.json +123 -0
  7. package/dist/compat.d.ts +149 -0
  8. package/dist/compat.js +221 -0
  9. package/dist/config.d.ts +23 -0
  10. package/dist/config.js +19 -0
  11. package/dist/context.js +24 -11
  12. package/dist/error-fields.d.ts +41 -0
  13. package/dist/error-fields.js +103 -0
  14. package/dist/fingerprint-override.d.ts +42 -0
  15. package/dist/fingerprint-override.js +76 -0
  16. package/dist/fingerprint-v3.d.ts +54 -0
  17. package/dist/fingerprint-v3.js +96 -0
  18. package/dist/fingerprint.d.ts +54 -0
  19. package/dist/fingerprint.js +46 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +5 -3
  22. package/dist/llm.js +16 -3
  23. package/dist/log-promotion.d.ts +55 -0
  24. package/dist/log-promotion.js +119 -0
  25. package/dist/logs.d.ts +57 -1
  26. package/dist/logs.js +148 -11
  27. package/dist/memory.d.ts +2 -2
  28. package/dist/migrations.d.ts +13 -0
  29. package/dist/migrations.js +75 -2
  30. package/dist/normalize-browser.d.ts +67 -22
  31. package/dist/normalize-browser.js +46 -19
  32. package/dist/normalize-otlp.js +18 -2
  33. package/dist/redact.d.ts +33 -0
  34. package/dist/redact.js +96 -0
  35. package/dist/sdk-versions.d.ts +58 -0
  36. package/dist/sdk-versions.js +167 -0
  37. package/dist/server.d.ts +3 -0
  38. package/dist/server.js +219 -15
  39. package/dist/sink.d.ts +13 -0
  40. package/dist/sink.js +37 -4
  41. package/dist/source-maps-lookup.d.ts +31 -0
  42. package/dist/source-maps-lookup.js +64 -0
  43. package/dist/source-maps.d.ts +63 -2
  44. package/dist/source-maps.js +121 -13
  45. package/dist/stack-fixtures.d.ts +10 -2
  46. package/dist/stack-fixtures.js +13 -3
  47. package/dist/stack-frames.d.ts +48 -0
  48. package/dist/stack-frames.js +441 -0
  49. package/dist/symbolicate.d.ts +79 -0
  50. package/dist/symbolicate.js +257 -0
  51. package/dist/types.d.ts +86 -0
  52. package/package.json +2 -1
@@ -0,0 +1,64 @@
1
+ import { normalizeDebugId, sourceMapPath, } from "./source-maps.js";
2
+ async function firstMap(client, database, where, params) {
3
+ const rows = await client.query({
4
+ query: `SELECT map FROM ${database}.runtime_source_maps
5
+ WHERE org_id = {org:String} AND repository_id = {repo:String} AND ${where}
6
+ ORDER BY uploaded_at DESC LIMIT 1`,
7
+ query_params: params,
8
+ format: "JSONEachRow",
9
+ });
10
+ const [row] = await rows.json();
11
+ return row?.map ?? null;
12
+ }
13
+ /** The stored map JSON for a minified file: by debug id first when given, else by release + filename (path only, no origin/query). Null when none. */
14
+ export async function lookupSourceMap(client, database, key) {
15
+ const tenant = { org: key.orgId, repo: key.repositoryId };
16
+ // A debug id names the exact bundle bytes, so it matches whatever release
17
+ // the SDK reported (or none). Fall through to release + path when no map
18
+ // was uploaded with this id (old CLI, plain curl upload).
19
+ const debugId = normalizeDebugId(key.debugId);
20
+ if (debugId) {
21
+ const map = await firstMap(client, database, "debug_id = {debugId:String}", { ...tenant, debugId });
22
+ if (map !== null)
23
+ return map;
24
+ }
25
+ const filename = sourceMapPath(key.filename);
26
+ if (!key.release || !filename)
27
+ return null;
28
+ return firstMap(client, database, "release = {release:String} AND filename = {filename:String}", { ...tenant, release: key.release, filename });
29
+ }
30
+ /**
31
+ * Re-uploading a map refreshes its 30-day TTL, so a map only counts as
32
+ * already uploaded when it was stored this recently; older ones are sent
33
+ * again and stay alive while the release is still being built.
34
+ */
35
+ export const SOURCE_MAP_FRESH_DAYS = 7;
36
+ /**
37
+ * Which check entries the tenant already has: same release, path, debug id
38
+ * and content hash, uploaded in the last SOURCE_MAP_FRESH_DAYS. Returns the
39
+ * indexes of `check.entries` that need no upload.
40
+ */
41
+ export async function presentSourceMaps(client, database, tenant, check) {
42
+ if (check.entries.length === 0)
43
+ return [];
44
+ const rows = await client.query({
45
+ query: `SELECT DISTINCT filename, debug_id, content_hash FROM ${database}.runtime_source_maps
46
+ WHERE org_id = {org:String} AND repository_id = {repo:String} AND release = {release:String}
47
+ AND has({hashes:Array(String)}, content_hash)
48
+ AND uploaded_at >= now64(3) - INTERVAL ${SOURCE_MAP_FRESH_DAYS} DAY`,
49
+ query_params: {
50
+ org: tenant.orgId,
51
+ repo: tenant.repositoryId,
52
+ release: check.release,
53
+ hashes: [...new Set(check.entries.map((entry) => entry.hash))],
54
+ },
55
+ format: "JSONEachRow",
56
+ });
57
+ const stored = new Set((await rows.json()).map((row) => `${row.filename}\u0000${row.debug_id}\u0000${row.content_hash}`));
58
+ const present = [];
59
+ check.entries.forEach((entry, index) => {
60
+ if (stored.has(`${entry.filename}\u0000${entry.debugId}\u0000${entry.hash}`))
61
+ present.push(index);
62
+ });
63
+ return present;
64
+ }
@@ -1,6 +1,67 @@
1
+ /** Largest stored map (after `sourcesContent` is removed), in characters. */
2
+ export declare const SOURCE_MAP_MAX_CHARS: number;
3
+ /** A debug id: 8-4-4-4-12 lowercase hex. Written by `autter-runtime
4
+ * sourcemaps inject`, and by Rollup, Turbopack, esbuild and Sentry tooling. */
5
+ export declare const DEBUG_ID_RE: RegExp;
6
+ /** Lowercased debug id, or null when the value isn't one. */
7
+ export declare function normalizeDebugId(value: unknown): string | null;
8
+ /**
9
+ * The stored form of a bundle's filename: path only, with no origin, query
10
+ * or hash ("https://cdn.example.com/assets/app.js?v=2" → "/assets/app.js").
11
+ * Uploads and lookups both go through it so they always agree.
12
+ */
13
+ export declare function sourceMapPath(filename: string): string | null;
14
+ /** Migration 0016 columns — shared with the baseline DDL below. */
15
+ export declare const SOURCE_MAP_DEBUG_COLUMNS: string[];
16
+ export declare const SOURCE_MAP_DEBUG_ID_INDEX = "INDEX idx_sm_debug_id debug_id TYPE bloom_filter GRANULARITY 1";
1
17
  export declare function sourceMapTableDDL(db: string): string;
2
- export declare function validateSourceMap(body: unknown): {
18
+ export interface ValidSourceMap {
3
19
  release: string;
20
+ /** sourceMapPath() of the uploaded filename. */
4
21
  filename: string;
22
+ /** The map JSON without `sourcesContent`. */
5
23
  map: string;
6
- } | null;
24
+ /** Lowercase debug id, or "" when the upload has none. */
25
+ debugId: string;
26
+ /** sha256 hex of `map`. */
27
+ contentHash: string;
28
+ }
29
+ /** The content hash of a stored map — `autter-runtime sourcemaps upload`
30
+ * computes the same value to skip maps the ingester already has. */
31
+ export declare function sourceMapContentHash(map: string): string;
32
+ /**
33
+ * Validate one `/v1/sourcemaps` upload: `{ release, filename, map, debugId? }`.
34
+ * `map` may be an object or a JSON string. The debug id comes from the body,
35
+ * else from the map's own `debugId` (or legacy `debug_id`); an invalid one
36
+ * rejects the upload. Null when the upload is invalid.
37
+ */
38
+ export declare function validateSourceMap(body: unknown): ValidSourceMap | null;
39
+ /** One entry of a `/v1/sourcemaps/check` request. */
40
+ export interface SourceMapCheckEntry {
41
+ filename: string;
42
+ debugId: string;
43
+ hash: string;
44
+ }
45
+ export interface SourceMapCheck {
46
+ release: string;
47
+ entries: SourceMapCheckEntry[];
48
+ }
49
+ /** Most entries one check request may name. */
50
+ export declare const SOURCE_MAP_CHECK_MAX = 500;
51
+ /**
52
+ * Validate a `/v1/sourcemaps/check` request:
53
+ * `{ release, files: [{ filename, debugId?, hash }] }` (≤ 500 files). The
54
+ * filenames and debug ids are normalised exactly as an upload would store
55
+ * them. Null when invalid.
56
+ */
57
+ export declare function validateSourceMapCheck(body: unknown): SourceMapCheck | null;
58
+ /** Most scripts one browser event may name in `debugIds`. */
59
+ export declare const DEBUG_IDS_MAX = 50;
60
+ /**
61
+ * The `debugIds` of a browser event (runtime-browser 1.6.0+, EI-13): script
62
+ * URL → debug id. Keys lose their query and hash and are scrubbed, invalid ids
63
+ * are dropped one by one, at most 50 are kept. `{ debugIds }` for the occurrence, or `{}`.
64
+ */
65
+ export declare function browserDebugIds(raw: Record<string, unknown> | undefined): {
66
+ debugIds?: Record<string, string>;
67
+ };
@@ -1,27 +1,74 @@
1
+ import { createHash } from "node:crypto";
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;
5
+ /** Bundles a map can be uploaded for: `.js`, `.mjs` and `.cjs` (1.6.0+). */
6
+ const BUNDLE_PATH_RE = /\.[cm]?js$/;
7
+ /** A debug id: 8-4-4-4-12 lowercase hex. Written by `autter-runtime
8
+ * sourcemaps inject`, and by Rollup, Turbopack, esbuild and Sentry tooling. */
9
+ export const DEBUG_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10
+ /** Lowercased debug id, or null when the value isn't one. */
11
+ export function normalizeDebugId(value) {
12
+ if (typeof value !== "string")
13
+ return null;
14
+ const id = value.trim().toLowerCase();
15
+ return DEBUG_ID_RE.test(id) ? id : null;
16
+ }
17
+ /**
18
+ * The stored form of a bundle's filename: path only, with no origin, query
19
+ * or hash ("https://cdn.example.com/assets/app.js?v=2" → "/assets/app.js").
20
+ * Uploads and lookups both go through it so they always agree.
21
+ */
22
+ export function sourceMapPath(filename) {
23
+ if (filename.length > 1000)
24
+ return null;
25
+ try {
26
+ return new URL(filename, "https://autter.invalid").pathname;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ /** Migration 0016 columns — shared with the baseline DDL below. */
33
+ export const SOURCE_MAP_DEBUG_COLUMNS = [
34
+ // The bundle's debug id ('' for maps uploaded without one).
35
+ "debug_id String DEFAULT ''",
36
+ // sha256 of the stored map, so CI can skip maps it already uploaded.
37
+ "content_hash String DEFAULT ''",
38
+ ];
39
+ export const SOURCE_MAP_DEBUG_ID_INDEX = "INDEX idx_sm_debug_id debug_id TYPE bloom_filter GRANULARITY 1";
1
40
  export function sourceMapTableDDL(db) {
2
41
  return `CREATE TABLE IF NOT EXISTS ${db}.runtime_source_maps (
3
42
  org_id String, repository_id String, release String, filename String,
4
- map String CODEC(ZSTD(3)), uploaded_at DateTime64(3, 'UTC') DEFAULT now64(3)
43
+ map String CODEC(ZSTD(3)), uploaded_at DateTime64(3, 'UTC') DEFAULT now64(3),
44
+ ${SOURCE_MAP_DEBUG_COLUMNS.join(", ")},
45
+ ${SOURCE_MAP_DEBUG_ID_INDEX}
5
46
  ) ENGINE = ReplacingMergeTree(uploaded_at)
6
47
  ORDER BY (org_id, repository_id, release, filename)
7
48
  TTL toDateTime(uploaded_at) + INTERVAL 30 DAY`;
8
49
  }
50
+ const RELEASE_RE = /^[a-zA-Z0-9._-]{1,200}$/;
51
+ /** The content hash of a stored map — `autter-runtime sourcemaps upload`
52
+ * computes the same value to skip maps the ingester already has. */
53
+ export function sourceMapContentHash(map) {
54
+ return createHash("sha256").update(map).digest("hex");
55
+ }
56
+ /**
57
+ * Validate one `/v1/sourcemaps` upload: `{ release, filename, map, debugId? }`.
58
+ * `map` may be an object or a JSON string. The debug id comes from the body,
59
+ * else from the map's own `debugId` (or legacy `debug_id`); an invalid one
60
+ * rejects the upload. Null when the upload is invalid.
61
+ */
9
62
  export function validateSourceMap(body) {
10
63
  if (!body || typeof body !== "object")
11
64
  return null;
12
65
  const input = body;
13
- if (typeof input.release !== "string" || !/^[a-zA-Z0-9._-]{1,200}$/.test(input.release))
66
+ if (typeof input.release !== "string" || !RELEASE_RE.test(input.release))
14
67
  return null;
15
- if (typeof input.filename !== "string" || input.filename.length > 1000)
68
+ if (typeof input.filename !== "string")
16
69
  return null;
17
- let filename;
18
- try {
19
- filename = new URL(input.filename, "https://autter.invalid").pathname;
20
- }
21
- catch {
22
- return null;
23
- }
24
- if (!filename.endsWith(".js"))
70
+ const filename = sourceMapPath(input.filename);
71
+ if (!filename || !BUNDLE_PATH_RE.test(filename))
25
72
  return null;
26
73
  let parsed;
27
74
  try {
@@ -32,10 +79,71 @@ export function validateSourceMap(body) {
32
79
  }
33
80
  if (!parsed || parsed.version !== 3 || typeof parsed.mappings !== "string" || !Array.isArray(parsed.sources))
34
81
  return null;
82
+ const rawDebugId = input.debugId ?? parsed.debugId ?? parsed.debug_id;
83
+ const debugId = rawDebugId === undefined || rawDebugId === null || rawDebugId === "" ? "" : normalizeDebugId(rawDebugId);
84
+ if (debugId === null)
85
+ return null;
35
86
  // Source contents are unnecessary for position lookup and may contain secrets.
36
87
  const { sourcesContent: _omitted, ...safe } = parsed;
37
88
  const map = JSON.stringify(safe);
38
- if (map.length > 5 * 1024 * 1024)
89
+ if (map.length > SOURCE_MAP_MAX_CHARS)
90
+ return null;
91
+ return { release: input.release, filename, map, debugId, contentHash: sourceMapContentHash(map) };
92
+ }
93
+ /** Most entries one check request may name. */
94
+ export const SOURCE_MAP_CHECK_MAX = 500;
95
+ /**
96
+ * Validate a `/v1/sourcemaps/check` request:
97
+ * `{ release, files: [{ filename, debugId?, hash }] }` (≤ 500 files). The
98
+ * filenames and debug ids are normalised exactly as an upload would store
99
+ * them. Null when invalid.
100
+ */
101
+ export function validateSourceMapCheck(body) {
102
+ if (!body || typeof body !== "object")
39
103
  return null;
40
- return { release: input.release, filename, map };
104
+ const input = body;
105
+ if (typeof input.release !== "string" || !RELEASE_RE.test(input.release))
106
+ return null;
107
+ if (!Array.isArray(input.files) || input.files.length > SOURCE_MAP_CHECK_MAX)
108
+ return null;
109
+ const entries = [];
110
+ for (const raw of input.files) {
111
+ if (!raw || typeof raw !== "object")
112
+ return null;
113
+ const file = raw;
114
+ if (typeof file.filename !== "string" || typeof file.hash !== "string" || !/^[0-9a-f]{64}$/.test(file.hash))
115
+ return null;
116
+ const filename = sourceMapPath(file.filename);
117
+ if (!filename)
118
+ return null;
119
+ const debugId = file.debugId === undefined || file.debugId === null || file.debugId === "" ? "" : normalizeDebugId(file.debugId);
120
+ if (debugId === null)
121
+ return null;
122
+ entries.push({ filename, debugId, hash: file.hash });
123
+ }
124
+ return { release: input.release, entries };
125
+ }
126
+ /** Most scripts one browser event may name in `debugIds`. */
127
+ export const DEBUG_IDS_MAX = 50;
128
+ /**
129
+ * The `debugIds` of a browser event (runtime-browser 1.6.0+, EI-13): script
130
+ * URL → debug id. Keys lose their query and hash and are scrubbed, invalid ids
131
+ * are dropped one by one, at most 50 are kept. `{ debugIds }` for the occurrence, or `{}`.
132
+ */
133
+ export function browserDebugIds(raw) {
134
+ if (!raw)
135
+ return {};
136
+ const debugIds = {};
137
+ let count = 0;
138
+ for (const [url, value] of Object.entries(raw)) {
139
+ const id = normalizeDebugId(value);
140
+ // Scrubbed like the stack it was taken from, so keys still match its frames.
141
+ const script = scrubText(url.split(/[?#]/)[0] ?? "");
142
+ if (!id || !script)
143
+ continue;
144
+ debugIds[script] = id;
145
+ if (++count >= DEBUG_IDS_MAX)
146
+ break;
147
+ }
148
+ return count ? { debugIds } : {};
41
149
  }
@@ -12,8 +12,10 @@
12
12
  * parsing, Go/Rust frames were dropped and these collided into one issue).
13
13
  * - `redeploy` — the primary defect after a rebuild that shifted every line
14
14
  * number (and, for Go, pointer offsets/addresses/goroutine ids). It must
15
- * fingerprint IDENTICALLY to `primary`, proving grouping is stable across
16
- * re-deploys and repeated ingestion.
15
+ * fingerprint IDENTICALLY to `primary` under frames-v3, proving grouping
16
+ * is stable across re-deploys and repeated ingestion — and under the
17
+ * legacy message-v1 too, except where `legacyKeepsLineNumbers` says the
18
+ * frozen legacy normaliser hashes line numbers (Python).
17
19
  */
18
20
  export interface StackFixture {
19
21
  primary: string;
@@ -21,5 +23,11 @@ export interface StackFixture {
21
23
  redeploy: string;
22
24
  /** Golden normalised top frames for `primary` (locks the parser output). */
23
25
  primaryFrames: string[];
26
+ /**
27
+ * message-v1 keeps this language's line numbers (Python `File "…", line
28
+ * N` lines are hashed verbatim, and must stay so for existing issues), so
29
+ * only frames-v3 survives `redeploy`.
30
+ */
31
+ legacyKeepsLineNumbers?: boolean;
24
32
  }
25
33
  export declare const STACK_FIXTURES: Record<string, StackFixture>;
@@ -12,8 +12,10 @@
12
12
  * parsing, Go/Rust frames were dropped and these collided into one issue).
13
13
  * - `redeploy` — the primary defect after a rebuild that shifted every line
14
14
  * number (and, for Go, pointer offsets/addresses/goroutine ids). It must
15
- * fingerprint IDENTICALLY to `primary`, proving grouping is stable across
16
- * re-deploys and repeated ingestion.
15
+ * fingerprint IDENTICALLY to `primary` under frames-v3, proving grouping
16
+ * is stable across re-deploys and repeated ingestion — and under the
17
+ * legacy message-v1 too, except where `legacyKeepsLineNumbers` says the
18
+ * frozen legacy normaliser hashes line numbers (Python).
17
19
  */
18
20
  // ── Go ──────────────────────────────────────────────────────────────────────
19
21
  const GO_PRIMARY = `panic: runtime error: invalid memory address or nil pointer dereference
@@ -154,6 +156,13 @@ const PYTHON_PRIMARY = `Traceback (most recent call last):
154
156
  File "/app/orders/service.py", line 88, in process
155
157
  raise ValueError(f"order {order_id} is invalid")
156
158
  ValueError: order 4821 is invalid`;
159
+ // Same defect after a deploy that shifted both functions down the file.
160
+ const PYTHON_REDEPLOY = `Traceback (most recent call last):
161
+ File "/app/web/handler.py", line 47, in handle
162
+ return self.service.process(order)
163
+ File "/app/orders/service.py", line 95, in process
164
+ raise ValueError(f"order {order_id} is invalid")
165
+ ValueError: order 9113 is invalid`;
157
166
  const PYTHON_SIBLING = `Traceback (most recent call last):
158
167
  File "/app/web/handler.py", line 42, in handle
159
168
  return self.service.process(order)
@@ -222,10 +231,11 @@ export const STACK_FIXTURES = {
222
231
  python: {
223
232
  primary: PYTHON_PRIMARY,
224
233
  sibling: PYTHON_SIBLING,
225
- redeploy: PYTHON_PRIMARY,
234
+ redeploy: PYTHON_REDEPLOY,
226
235
  primaryFrames: [
227
236
  'File "/app/web/handler.py", line 42, in handle',
228
237
  'File "/app/orders/service.py", line 88, in process',
229
238
  ],
239
+ legacyKeepsLineNumbers: true,
230
240
  },
231
241
  };
@@ -0,0 +1,48 @@
1
+ import type { StackFrame } from "./types.js";
2
+ /**
3
+ * Raw stack text → structured frames, INNERMOST (crash site) FIRST.
4
+ *
5
+ * The legacy `message-v1` fingerprint hashes stack lines as text, which
6
+ * keeps Python line numbers, takes Python's outermost frames, and counts
7
+ * dependency frames. `frames-v3` (fingerprint-v3.ts) hashes structured
8
+ * frames instead, so every supported language is parsed into the sink's
9
+ * StackFrame shape here, in one consistent orientation:
10
+ *
11
+ * - V8 (Node, Chrome, Edge): `at fn (file:line:col)` / `at file:line:col`
12
+ * - Firefox / Safari: `fn@url:line:col` / `@url:line:col`
13
+ * - Python: `File "…", line N, in fn` — tracebacks list the OUTERMOST frame
14
+ * first, so the frames are reversed; only the last (reported) traceback
15
+ * of a chained exception is used
16
+ * - Go: function line + `\t<file>.go:<line> +0x…`, first goroutine only
17
+ * - JVM: `at pkg.Class.method(File.java:N)`, first exception block only
18
+ * (before `Caused by:`)
19
+ * - .NET: `at Ns.Type.Method(args) in <file>:line N`; for wrapped
20
+ * exceptions only the frames after the last "End of inner exception"
21
+ * - Rust: `N: path::to::fn` + `at <file>:<line>:<col>`
22
+ * - Ruby / PHP best effort, so those stacks do not collapse to the message
23
+ *
24
+ * `inApp` follows the contract §3 rule (isInAppFrame). `resolved` is always
25
+ * false here; symbolicate.ts sets it for frames a source map resolved.
26
+ * Shared parity vectors: stack-parse-vectors.json.
27
+ */
28
+ /** Frames kept per stack (contract §1: at most 50). */
29
+ export declare const MAX_FRAMES = 50;
30
+ export type FrameLanguage = "javascript" | "python" | "go" | "jvm" | "dotnet" | "rust" | "ruby" | "php";
31
+ /**
32
+ * Path half of §3: dependency directories, Node internals and browser
33
+ * extension scripts are not in-app. Windows separators are normalised so
34
+ * `C:\app\node_modules\x.js` counts too. A frame with no path is in-app.
35
+ */
36
+ export declare function isInAppPath(path: string | null): boolean;
37
+ /**
38
+ * Contract §3. Source-map ignore lists are applied by symbolicate.ts; the
39
+ * language rules apply only to frames of that language, so a JS object
40
+ * that happens to be called `runtime` or `System` stays in-app.
41
+ */
42
+ export declare function isInAppFrame(frame: Pick<StackFrame, "function" | "module" | "filename">, language: FrameLanguage | null): boolean;
43
+ /** Strip a query string and/or hash from a path or URL. */
44
+ export declare function stripQueryAndHash(path: string): string;
45
+ /** The language of a raw stack, or null when it has no recognisable frames. */
46
+ export declare function detectFrameLanguage(lines: string[]): FrameLanguage | null;
47
+ /** Parse a raw stack into frames, innermost first (≤ MAX_FRAMES). */
48
+ export declare function parseStackFrames(stack: string | null | undefined): StackFrame[];