@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.
- package/README.md +51 -0
- package/dist/breadcrumbs.d.ts +41 -0
- package/dist/breadcrumbs.js +151 -0
- package/dist/clickhouse.d.ts +42 -5
- package/dist/clickhouse.js +168 -7
- package/dist/compat-manifest.json +123 -0
- package/dist/compat.d.ts +149 -0
- package/dist/compat.js +221 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +19 -0
- package/dist/context.js +24 -11
- package/dist/error-fields.d.ts +41 -0
- package/dist/error-fields.js +103 -0
- package/dist/fingerprint-override.d.ts +42 -0
- package/dist/fingerprint-override.js +76 -0
- package/dist/fingerprint-v3.d.ts +54 -0
- package/dist/fingerprint-v3.js +96 -0
- package/dist/fingerprint.d.ts +54 -0
- package/dist/fingerprint.js +46 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -3
- package/dist/llm.js +16 -3
- package/dist/log-promotion.d.ts +55 -0
- package/dist/log-promotion.js +119 -0
- package/dist/logs.d.ts +57 -1
- package/dist/logs.js +148 -11
- package/dist/memory.d.ts +2 -2
- package/dist/migrations.d.ts +13 -0
- package/dist/migrations.js +75 -2
- package/dist/normalize-browser.d.ts +67 -22
- package/dist/normalize-browser.js +46 -19
- package/dist/normalize-otlp.js +18 -2
- package/dist/redact.d.ts +33 -0
- package/dist/redact.js +96 -0
- package/dist/sdk-versions.d.ts +58 -0
- package/dist/sdk-versions.js +167 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +219 -15
- package/dist/sink.d.ts +13 -0
- package/dist/sink.js +37 -4
- package/dist/source-maps-lookup.d.ts +31 -0
- package/dist/source-maps-lookup.js +64 -0
- package/dist/source-maps.d.ts +63 -2
- package/dist/source-maps.js +121 -13
- package/dist/stack-fixtures.d.ts +10 -2
- package/dist/stack-fixtures.js +13 -3
- package/dist/stack-frames.d.ts +48 -0
- package/dist/stack-frames.js +441 -0
- package/dist/symbolicate.d.ts +79 -0
- package/dist/symbolicate.js +257 -0
- package/dist/types.d.ts +86 -0
- 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
|
+
}
|
package/dist/source-maps.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/source-maps.js
CHANGED
|
@@ -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" ||
|
|
66
|
+
if (typeof input.release !== "string" || !RELEASE_RE.test(input.release))
|
|
14
67
|
return null;
|
|
15
|
-
if (typeof input.filename !== "string"
|
|
68
|
+
if (typeof input.filename !== "string")
|
|
16
69
|
return null;
|
|
17
|
-
|
|
18
|
-
|
|
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 >
|
|
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
|
-
|
|
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
|
}
|
package/dist/stack-fixtures.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/stack-fixtures.js
CHANGED
|
@@ -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
|
|
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:
|
|
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[];
|