@autter/otlp-ingester 1.9.0 → 1.11.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 +1 -1
- package/dist/cause-chain.d.ts +32 -0
- package/dist/cause-chain.js +122 -0
- package/dist/clickhouse.js +31 -2
- package/dist/compat-manifest.json +37 -0
- package/dist/fingerprint-v3.d.ts +63 -7
- package/dist/fingerprint-v3.js +96 -7
- package/dist/ingest-drops.d.ts +10 -0
- package/dist/ingest-drops.js +13 -0
- package/dist/latency.d.ts +42 -0
- package/dist/latency.js +80 -0
- package/dist/log-promotion.d.ts +36 -5
- package/dist/log-promotion.js +95 -21
- package/dist/logs.d.ts +3 -3
- package/dist/logs.js +13 -4
- package/dist/migrations.d.ts +3 -2
- package/dist/migrations.js +12 -3
- package/dist/normalize-browser.d.ts +12 -12
- package/dist/normalize-browser.js +17 -3
- package/dist/normalize-otlp.js +39 -5
- package/dist/server.js +58 -20
- package/dist/sink.d.ts +5 -2
- package/dist/sink.js +7 -3
- package/dist/source-maps-lookup.d.ts +14 -1
- package/dist/source-maps-lookup.js +13 -3
- package/dist/source-maps.d.ts +49 -6
- package/dist/source-maps.js +122 -12
- package/dist/symbolicate.d.ts +83 -11
- package/dist/symbolicate.js +183 -31
- package/dist/trace-dedupe.d.ts +1 -1
- package/dist/trace-dedupe.js +3 -1
- package/dist/types.d.ts +26 -11
- package/package.json +1 -1
package/dist/symbolicate.d.ts
CHANGED
|
@@ -4,10 +4,19 @@ import { ScopeIndex } from "./source-map-scopes.js";
|
|
|
4
4
|
import type { SourceMapKey } from "./source-maps-lookup.js";
|
|
5
5
|
import type { StackFrame } from "./types.js";
|
|
6
6
|
/**
|
|
7
|
-
* Symbolication at ingest: minified browser frames
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `index-5f3a9c1e.js` — and an issue
|
|
7
|
+
* Symbolication at ingest: minified browser frames, and bundled Node and
|
|
8
|
+
* edge frames (RF-05), are resolved through the source maps uploaded for
|
|
9
|
+
* the release (`/v1/sourcemaps`) BEFORE they are fingerprinted, so
|
|
10
|
+
* `frames-v3` hashes `checkout.tsx`, not `index-5f3a9c1e.js` — and an issue
|
|
11
|
+
* survives deploys and preview domains.
|
|
12
|
+
*
|
|
13
|
+
* - Browser frames find their map by debug id, else by release + the
|
|
14
|
+
* script's path. Server frames (`server: true`) by debug id (server SDKs
|
|
15
|
+
* send `autter.debug_ids`), else by the release's map stored
|
|
16
|
+
* under the longest suffix of the path the process ran the bundle from
|
|
17
|
+
* (source-maps-lookup.ts): the upload knows `/server.js`, the stack says
|
|
18
|
+
* `/app/dist/server.js`. Only in-app server frames are looked up — a
|
|
19
|
+
* `node_modules/` or `node:` frame never belongs to an uploaded bundle.
|
|
11
20
|
*
|
|
12
21
|
* - Function names: when the map carries function scopes
|
|
13
22
|
* (`x_autter_scopes`, uploaded by @autter/runtime-cli after 1.0.0), a
|
|
@@ -22,24 +31,38 @@ import type { StackFrame } from "./types.js";
|
|
|
22
31
|
* and for handlers a framework calls, which is why scopes exist.
|
|
23
32
|
* - Sources the map lists in `ignoreList` / `x_google_ignoreList` are not
|
|
24
33
|
* in-app; a resolved path under node_modules/ etc. is not in-app either.
|
|
25
|
-
* - Context lines
|
|
26
|
-
*
|
|
34
|
+
* - Context lines (RF-05): an in-app resolved frame gets its original line
|
|
35
|
+
* and up to CONTEXT_LINES lines before and after it, each cut to 300
|
|
36
|
+
* characters and scrubbed like the stack, when the map carries the
|
|
37
|
+
* source's `sourcesContent` (kept at upload for in-app sources,
|
|
38
|
+
* source-maps.ts). Dependency frames get none.
|
|
27
39
|
* - Never fails ingest: any error leaves the frame as parsed.
|
|
28
40
|
*
|
|
29
41
|
* Parsed maps are cached per (org, repository, release, file) — or per debug
|
|
30
42
|
* id — in a byte-bounded LRU, so a busy issue never refetches its map. A map
|
|
31
43
|
* that would take more than half of that budget is used for its lookup and
|
|
32
44
|
* not kept (source-map-mappings.ts reads it without decoding it), so one huge
|
|
33
|
-
* vendor chunk cannot flush every other map.
|
|
45
|
+
* vendor chunk cannot flush every other map. A map's source contents are
|
|
46
|
+
* never part of its entry (it caches exactly as a map without them would):
|
|
47
|
+
* they sit in a separate store, one file per entry, bounded the same way and
|
|
48
|
+
* dropped with the map.
|
|
34
49
|
*/
|
|
35
50
|
export type MapFetcher = (key: SourceMapKey) => Promise<string | null>;
|
|
36
51
|
export interface SymbolicationScope {
|
|
37
52
|
orgId: string;
|
|
38
53
|
repositoryId: string;
|
|
39
54
|
release: string | null;
|
|
40
|
-
/** Script URL → debug id (EI-13), when the
|
|
55
|
+
/** Script URL (or, on a server, script path) → debug id (EI-13), when the SDK sent them. */
|
|
41
56
|
debugIds?: Record<string, string> | null;
|
|
57
|
+
/**
|
|
58
|
+
* A Node or edge stack (RF-05): only in-app frames are looked up, their
|
|
59
|
+
* paths are normalised (serverScriptPath), and without a debug id the
|
|
60
|
+
* release's map is found by path suffix.
|
|
61
|
+
*/
|
|
62
|
+
server?: boolean;
|
|
42
63
|
}
|
|
64
|
+
/** Lines kept before and after an in-app frame's own line (RF-05). */
|
|
65
|
+
export declare const CONTEXT_LINES = 5;
|
|
43
66
|
/**
|
|
44
67
|
* In-app browser frames a source map could resolve, counted after
|
|
45
68
|
* symbolication (EI-27): `resolved` ones, and `unresolved` .js/.mjs/.cjs
|
|
@@ -55,10 +78,25 @@ export declare function inAppFrameResolution(frames: readonly StackFrame[] | und
|
|
|
55
78
|
};
|
|
56
79
|
/** Path-only form of a script URL — the key uploads are stored under. */
|
|
57
80
|
export declare function scriptPath(filename: string): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* The path form of a server frame's file, comparable with stored upload
|
|
83
|
+
* paths: `file:///app/dist/index.mjs` → `/app/dist/index.mjs`,
|
|
84
|
+
* `C:\app\dist\index.js` → `/app/dist/index.js`, `index.js` (a Worker
|
|
85
|
+
* script) → `/index.js`. Null when it is not a path.
|
|
86
|
+
*/
|
|
87
|
+
export declare function serverScriptPath(filename: string): string | null;
|
|
58
88
|
export interface SourceMapCacheOptions {
|
|
59
89
|
/** Budget for the estimated in-memory size of parsed maps (default 64 MiB).
|
|
60
90
|
* A map that would take more than half of it is not cached. */
|
|
61
91
|
maxBytes?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Budget for the source contents of cached maps (RF-05; default:
|
|
94
|
+
* `maxBytes`). They are kept apart from the maps, one entry per source
|
|
95
|
+
* file and bounded the same way (least recently used first, a file over
|
|
96
|
+
* half the budget not kept), so a map caches exactly as it would without
|
|
97
|
+
* them.
|
|
98
|
+
*/
|
|
99
|
+
maxContentBytes?: number;
|
|
62
100
|
maxEntries?: number;
|
|
63
101
|
/** How long a found map is reused (default 1 h). */
|
|
64
102
|
hitTtlMs?: number;
|
|
@@ -70,22 +108,43 @@ export interface SourceMapCacheOptions {
|
|
|
70
108
|
}
|
|
71
109
|
/** A parsed map: its metadata, its function scopes, and position lookups. */
|
|
72
110
|
export interface LoadedSourceMap {
|
|
73
|
-
/** Sources, names
|
|
74
|
-
*
|
|
111
|
+
/** Sources, names and ignore list. Its own `mappings` are left empty
|
|
112
|
+
* (positions go through `lookup`), and it holds no `sourcesContent`
|
|
113
|
+
* (see `contentOf`). */
|
|
75
114
|
map: TraceMap;
|
|
76
115
|
scopes: ScopeIndex | null;
|
|
77
116
|
/** Original positions for 0-based generated positions (one call per stack). */
|
|
78
117
|
lookup(positions: readonly GeneratedPosition[]): Array<MappedSegment | null>;
|
|
79
|
-
/** Estimated heap: decoded mappings (when decoded), strings and scopes
|
|
118
|
+
/** Estimated heap: decoded mappings (when decoded), strings and scopes;
|
|
119
|
+
* source contents are not part of it. */
|
|
80
120
|
bytes: number;
|
|
81
121
|
/** False when the map was too big to keep, so its mappings were not decoded. */
|
|
82
122
|
cacheable: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The stored original source of `sources[index]` (in-app sources only),
|
|
125
|
+
* or null. For a cached map it is read from the cache's separate content
|
|
126
|
+
* store, so a content evicted there gives null (no context lines) until
|
|
127
|
+
* the map is parsed again.
|
|
128
|
+
*/
|
|
129
|
+
contentOf(index: number): string | null;
|
|
130
|
+
}
|
|
131
|
+
/** A parsed map and its source contents, which are never part of the map's cache entry. */
|
|
132
|
+
export interface ParsedSourceMap {
|
|
133
|
+
loaded: LoadedSourceMap;
|
|
134
|
+
/** `sourcesContent` as stored (aligned with `sources`), or null. */
|
|
135
|
+
contents: Array<string | null> | null;
|
|
83
136
|
}
|
|
84
137
|
export declare class SourceMapCache {
|
|
85
138
|
private readonly entries;
|
|
86
139
|
private readonly inFlight;
|
|
87
140
|
private weight;
|
|
141
|
+
/** Source contents of cached maps, keyed by parse id and source index. */
|
|
142
|
+
private readonly contents;
|
|
143
|
+
private contentWeight;
|
|
144
|
+
/** Distinguishes every parse, so a re-parsed (maybe re-uploaded) map never reads an older parse's contents. */
|
|
145
|
+
private parses;
|
|
88
146
|
private readonly maxBytes;
|
|
147
|
+
private readonly maxContentBytes;
|
|
89
148
|
private readonly maxEntries;
|
|
90
149
|
private readonly hitTtlMs;
|
|
91
150
|
private readonly missTtlMs;
|
|
@@ -100,9 +159,14 @@ export declare class SourceMapCache {
|
|
|
100
159
|
get size(): number;
|
|
101
160
|
/** Estimated bytes of the cached maps (what `maxBytes` bounds). */
|
|
102
161
|
get bytes(): number;
|
|
162
|
+
/** Estimated bytes of the cached source contents (what `maxContentBytes` bounds). */
|
|
163
|
+
get contentBytes(): number;
|
|
103
164
|
/** The parsed map for `key` (null when none), fetching at most once at a time. */
|
|
104
165
|
get(key: SourceMapKey, fetchMap: MapFetcher): Promise<LoadedSourceMap | null>;
|
|
105
166
|
private load;
|
|
167
|
+
private contentOf;
|
|
168
|
+
private storeContent;
|
|
169
|
+
private removeContent;
|
|
106
170
|
private store;
|
|
107
171
|
private remove;
|
|
108
172
|
}
|
|
@@ -113,7 +177,15 @@ export declare class SourceMapCache {
|
|
|
113
177
|
* once into a compact table; otherwise they are left encoded and each
|
|
114
178
|
* lookup reads them in one pass (`cacheable: false`). Throws when the map
|
|
115
179
|
* itself is unusable.
|
|
180
|
+
*
|
|
181
|
+
* `sourcesContent` (RF-05) is taken out before anything else: the TraceMap
|
|
182
|
+
* never holds it and `bytes` never counts it, so whether a map is decoded
|
|
183
|
+
* and cached is decided exactly as for a map uploaded without contents.
|
|
184
|
+
* They are returned beside the map; SourceMapCache keeps them in their own
|
|
185
|
+
* store.
|
|
116
186
|
*/
|
|
187
|
+
export declare function parseSourceMapWithContents(json: string, maxDecodedBytes?: number): ParsedSourceMap;
|
|
188
|
+
/** parseSourceMapWithContents, the map alone (its contents through `contentOf`). */
|
|
117
189
|
export declare function parseSourceMap(json: string, maxDecodedBytes?: number): LoadedSourceMap;
|
|
118
190
|
export declare class Symbolicator {
|
|
119
191
|
private readonly fetchMap;
|
package/dist/symbolicate.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { isIgnored,
|
|
1
|
+
import { isIgnored, TraceMap } from "@jridgewell/trace-mapping";
|
|
2
2
|
import { DecodedMappings, decodedMappingsBytes, findInMappings, } from "./source-map-mappings.js";
|
|
3
|
+
import { scrubText } from "./redact.js";
|
|
3
4
|
import { SCOPES_KEY, ScopeIndex } from "./source-map-scopes.js";
|
|
4
5
|
import { isInAppPath, stripQueryAndHash } from "./stack-frames.js";
|
|
5
6
|
const JS_FILE_RE = /\.[cm]?js$/i;
|
|
6
7
|
const MAX_CONTEXT_CHARS = 300;
|
|
7
|
-
|
|
8
|
+
/** Lines kept before and after an in-app frame's own line (RF-05). */
|
|
9
|
+
export const CONTEXT_LINES = 5;
|
|
8
10
|
/**
|
|
9
11
|
* In-app browser frames a source map could resolve, counted after
|
|
10
12
|
* symbolication (EI-27): `resolved` ones, and `unresolved` .js/.mjs/.cjs
|
|
@@ -36,6 +38,25 @@ export function scriptPath(filename) {
|
|
|
36
38
|
return null;
|
|
37
39
|
}
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The path form of a server frame's file, comparable with stored upload
|
|
43
|
+
* paths: `file:///app/dist/index.mjs` → `/app/dist/index.mjs`,
|
|
44
|
+
* `C:\app\dist\index.js` → `/app/dist/index.js`, `index.js` (a Worker
|
|
45
|
+
* script) → `/index.js`. Null when it is not a path.
|
|
46
|
+
*/
|
|
47
|
+
export function serverScriptPath(filename) {
|
|
48
|
+
let path = filename;
|
|
49
|
+
if (/^file:\/\//i.test(path)) {
|
|
50
|
+
try {
|
|
51
|
+
path = new URL(path).pathname;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
path = path.replace(/\\/g, "/").replace(/^\/?[A-Za-z]:(?=\/)/, "");
|
|
58
|
+
return scriptPath(path);
|
|
59
|
+
}
|
|
39
60
|
/**
|
|
40
61
|
* The largest share of the budget one map may take. A bigger one is used
|
|
41
62
|
* for its lookup and dropped: keeping it would evict most other maps, and
|
|
@@ -46,7 +67,13 @@ export class SourceMapCache {
|
|
|
46
67
|
entries = new Map();
|
|
47
68
|
inFlight = new Map();
|
|
48
69
|
weight = 0;
|
|
70
|
+
/** Source contents of cached maps, keyed by parse id and source index. */
|
|
71
|
+
contents = new Map();
|
|
72
|
+
contentWeight = 0;
|
|
73
|
+
/** Distinguishes every parse, so a re-parsed (maybe re-uploaded) map never reads an older parse's contents. */
|
|
74
|
+
parses = 0;
|
|
49
75
|
maxBytes;
|
|
76
|
+
maxContentBytes;
|
|
50
77
|
maxEntries;
|
|
51
78
|
hitTtlMs;
|
|
52
79
|
missTtlMs;
|
|
@@ -58,6 +85,7 @@ export class SourceMapCache {
|
|
|
58
85
|
bypassed = 0;
|
|
59
86
|
constructor(options = {}) {
|
|
60
87
|
this.maxBytes = options.maxBytes ?? 64 * 1024 * 1024;
|
|
88
|
+
this.maxContentBytes = options.maxContentBytes ?? this.maxBytes;
|
|
61
89
|
this.maxEntries = options.maxEntries ?? 500;
|
|
62
90
|
this.hitTtlMs = options.hitTtlMs ?? 60 * 60_000;
|
|
63
91
|
this.missTtlMs = options.missTtlMs ?? 5 * 60_000;
|
|
@@ -66,9 +94,11 @@ export class SourceMapCache {
|
|
|
66
94
|
}
|
|
67
95
|
static keyOf(key) {
|
|
68
96
|
const scope = `${key.orgId}\u0000${key.repositoryId}\u0000`;
|
|
97
|
+
// A suffix lookup (server frame) can find another map than an exact one
|
|
98
|
+
// for the same path, so the two never share an entry.
|
|
69
99
|
return key.debugId
|
|
70
100
|
? `${scope}d\u0000${key.debugId}`
|
|
71
|
-
: `${scope}r\u0000${key.release ?? ""}\u0000${key.filename}`;
|
|
101
|
+
: `${scope}${key.suffix ? "s" : "r"}\u0000${key.release ?? ""}\u0000${key.filename}`;
|
|
72
102
|
}
|
|
73
103
|
get size() {
|
|
74
104
|
return this.entries.size;
|
|
@@ -77,6 +107,10 @@ export class SourceMapCache {
|
|
|
77
107
|
get bytes() {
|
|
78
108
|
return this.weight;
|
|
79
109
|
}
|
|
110
|
+
/** Estimated bytes of the cached source contents (what `maxContentBytes` bounds). */
|
|
111
|
+
get contentBytes() {
|
|
112
|
+
return this.contentWeight;
|
|
113
|
+
}
|
|
80
114
|
/** The parsed map for `key` (null when none), fetching at most once at a time. */
|
|
81
115
|
async get(key, fetchMap) {
|
|
82
116
|
const id = SourceMapCache.keyOf(key);
|
|
@@ -112,28 +146,74 @@ export class SourceMapCache {
|
|
|
112
146
|
this.store(id, null, 0, this.missTtlMs);
|
|
113
147
|
return null;
|
|
114
148
|
}
|
|
115
|
-
let
|
|
149
|
+
let parsed;
|
|
116
150
|
try {
|
|
117
|
-
|
|
151
|
+
parsed = parseSourceMapWithContents(json, this.maxBytes * MAX_SHARE_OF_BUDGET);
|
|
118
152
|
}
|
|
119
153
|
catch {
|
|
120
154
|
// A malformed upload is a miss until it is replaced.
|
|
121
155
|
this.store(id, null, 0, this.missTtlMs);
|
|
122
156
|
return null;
|
|
123
157
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
158
|
+
const { loaded, contents } = parsed;
|
|
159
|
+
if (!loaded.cacheable) {
|
|
160
|
+
// Used for this lookup only: its contents go with it.
|
|
127
161
|
this.bypassed += 1;
|
|
162
|
+
return loaded;
|
|
163
|
+
}
|
|
164
|
+
// The map's entry never holds the contents (it weighs what it did
|
|
165
|
+
// before RF-05): they go to their own store, one file per entry.
|
|
166
|
+
const parse = ++this.parses;
|
|
167
|
+
const contentKeys = [];
|
|
168
|
+
contents?.forEach((content, index) => {
|
|
169
|
+
if (typeof content !== "string" || !content)
|
|
170
|
+
return;
|
|
171
|
+
const key = `${parse}\u0000${index}`;
|
|
172
|
+
contentKeys.push(key);
|
|
173
|
+
this.storeContent(key, content);
|
|
174
|
+
});
|
|
175
|
+
loaded.contentOf = (index) => this.contentOf(`${parse}\u0000${index}`);
|
|
176
|
+
this.store(id, loaded, loaded.bytes, this.hitTtlMs, contentKeys);
|
|
128
177
|
return loaded;
|
|
129
178
|
}
|
|
130
|
-
|
|
179
|
+
contentOf(key) {
|
|
180
|
+
const entry = this.contents.get(key);
|
|
181
|
+
if (!entry)
|
|
182
|
+
return null;
|
|
183
|
+
if (entry.expiresAt <= this.now()) {
|
|
184
|
+
this.removeContent(key);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
this.contents.delete(key);
|
|
188
|
+
this.contents.set(key, entry);
|
|
189
|
+
return entry.content;
|
|
190
|
+
}
|
|
191
|
+
storeContent(key, content) {
|
|
192
|
+
const weight = 2 * content.length + 64;
|
|
193
|
+
// Bounded like the maps: one file may take at most half the budget.
|
|
194
|
+
if (weight > this.maxContentBytes * MAX_SHARE_OF_BUDGET)
|
|
195
|
+
return;
|
|
196
|
+
this.removeContent(key);
|
|
197
|
+
this.contents.set(key, { content, weight, expiresAt: this.now() + this.hitTtlMs });
|
|
198
|
+
this.contentWeight += weight;
|
|
199
|
+
while (this.contentWeight > this.maxContentBytes && this.contents.size > 1) {
|
|
200
|
+
this.removeContent(this.contents.keys().next().value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
removeContent(key) {
|
|
204
|
+
const entry = this.contents.get(key);
|
|
205
|
+
if (!entry)
|
|
206
|
+
return;
|
|
207
|
+
this.contents.delete(key);
|
|
208
|
+
this.contentWeight -= entry.weight;
|
|
209
|
+
}
|
|
210
|
+
store(id, map, weight, ttlMs, contentKeys) {
|
|
131
211
|
// A map bigger than the whole budget serves this lookup but is not kept:
|
|
132
212
|
// caching it would evict everything else.
|
|
133
213
|
if (weight > this.maxBytes)
|
|
134
214
|
return;
|
|
135
215
|
this.remove(id);
|
|
136
|
-
this.entries.set(id, { map, weight, expiresAt: this.now() + ttlMs });
|
|
216
|
+
this.entries.set(id, { map, weight, expiresAt: this.now() + ttlMs, ...(contentKeys?.length ? { contentKeys } : {}) });
|
|
137
217
|
this.weight += weight;
|
|
138
218
|
while ((this.weight > this.maxBytes || this.entries.size > this.maxEntries) &&
|
|
139
219
|
this.entries.size > 1) {
|
|
@@ -147,6 +227,8 @@ export class SourceMapCache {
|
|
|
147
227
|
return;
|
|
148
228
|
this.entries.delete(id);
|
|
149
229
|
this.weight -= entry.weight;
|
|
230
|
+
for (const key of entry.contentKeys ?? [])
|
|
231
|
+
this.removeContent(key);
|
|
150
232
|
}
|
|
151
233
|
}
|
|
152
234
|
/** Rough heap of a list of strings (V8 strings plus array slots). */
|
|
@@ -165,15 +247,24 @@ function stringsBytes(list) {
|
|
|
165
247
|
* once into a compact table; otherwise they are left encoded and each
|
|
166
248
|
* lookup reads them in one pass (`cacheable: false`). Throws when the map
|
|
167
249
|
* itself is unusable.
|
|
250
|
+
*
|
|
251
|
+
* `sourcesContent` (RF-05) is taken out before anything else: the TraceMap
|
|
252
|
+
* never holds it and `bytes` never counts it, so whether a map is decoded
|
|
253
|
+
* and cached is decided exactly as for a map uploaded without contents.
|
|
254
|
+
* They are returned beside the map; SourceMapCache keeps them in their own
|
|
255
|
+
* store.
|
|
168
256
|
*/
|
|
169
|
-
export function
|
|
257
|
+
export function parseSourceMapWithContents(json, maxDecodedBytes = Number.POSITIVE_INFINITY) {
|
|
170
258
|
const raw = JSON.parse(json);
|
|
171
259
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
172
260
|
throw new Error("not a source map");
|
|
173
|
-
const fields = raw;
|
|
261
|
+
const { sourcesContent, ...fields } = raw;
|
|
174
262
|
const { mappings } = fields;
|
|
175
263
|
if (typeof mappings !== "string")
|
|
176
264
|
throw new Error("no mappings");
|
|
265
|
+
const contents = Array.isArray(sourcesContent)
|
|
266
|
+
? sourcesContent.map((content) => (typeof content === "string" ? content : null))
|
|
267
|
+
: null;
|
|
177
268
|
// TraceMap copies only the standard fields, but given an object that has
|
|
178
269
|
// its private `_decodedMemo` it would return that object as is. It gets
|
|
179
270
|
// no mappings: decoding them is this module's job.
|
|
@@ -190,19 +281,41 @@ export function parseSourceMap(json, maxDecodedBytes = Number.POSITIVE_INFINITY)
|
|
|
190
281
|
stringsBytes(map.names) +
|
|
191
282
|
stringsBytes(map.sources) +
|
|
192
283
|
stringsBytes(map.resolvedSources) +
|
|
193
|
-
stringsBytes(map.sourcesContent) +
|
|
194
284
|
(scopes?.bytes ?? 0);
|
|
285
|
+
// Standalone use (and a map too big to cache): the contents travel with
|
|
286
|
+
// this parse; SourceMapCache replaces this for the maps it keeps. Built
|
|
287
|
+
// outside this scope so `lookup`'s closure never holds the contents.
|
|
288
|
+
const contentOf = contentsReader(contents);
|
|
195
289
|
if (bytes <= maxDecodedBytes) {
|
|
196
290
|
const decoded = DecodedMappings.decode(mappings);
|
|
197
291
|
return {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
292
|
+
loaded: {
|
|
293
|
+
map,
|
|
294
|
+
scopes,
|
|
295
|
+
bytes,
|
|
296
|
+
cacheable: true,
|
|
297
|
+
lookup: (positions) => positions.map((position) => decoded.find(position.line, position.column)),
|
|
298
|
+
contentOf,
|
|
299
|
+
},
|
|
300
|
+
contents,
|
|
203
301
|
};
|
|
204
302
|
}
|
|
205
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
loaded: { map, scopes, bytes, cacheable: false, lookup: (positions) => findInMappings(mappings, positions), contentOf },
|
|
305
|
+
contents,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* `contentOf` over a parse's own contents. A separate function on purpose:
|
|
310
|
+
* closures made in one scope share it, so a reader made next to `lookup`
|
|
311
|
+
* would keep every source content alive for as long as the cached map.
|
|
312
|
+
*/
|
|
313
|
+
function contentsReader(contents) {
|
|
314
|
+
return (index) => contents?.[index] ?? null;
|
|
315
|
+
}
|
|
316
|
+
/** parseSourceMapWithContents, the map alone (its contents through `contentOf`). */
|
|
317
|
+
export function parseSourceMap(json, maxDecodedBytes = Number.POSITIVE_INFINITY) {
|
|
318
|
+
return parseSourceMapWithContents(json, maxDecodedBytes).loaded;
|
|
206
319
|
}
|
|
207
320
|
// ── Resolution ────────────────────────────────────────────────────────────
|
|
208
321
|
/** A name a minifier evidently produced ("a", "Xe", "Object.t"). */
|
|
@@ -211,25 +324,56 @@ function looksMangled(name) {
|
|
|
211
324
|
const last = bare.split(/[.\s]/).filter(Boolean).pop() ?? "";
|
|
212
325
|
return last.length <= 2;
|
|
213
326
|
}
|
|
214
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Lines `index - radius` … `index + radius` (0-based) of `content`, found
|
|
329
|
+
* without splitting the whole file (a busy frame in a big file would split
|
|
330
|
+
* it on every event); `at` is where line `index` sits in `lines`. Null when
|
|
331
|
+
* the file is shorter than `index + 1` lines.
|
|
332
|
+
*/
|
|
333
|
+
function linesAround(content, index, radius) {
|
|
334
|
+
if (index < 0)
|
|
335
|
+
return null;
|
|
336
|
+
const first = Math.max(0, index - radius);
|
|
337
|
+
let start = 0;
|
|
338
|
+
for (let line = 0; line < first; line++) {
|
|
339
|
+
const newline = content.indexOf("\n", start);
|
|
340
|
+
if (newline < 0)
|
|
341
|
+
return null;
|
|
342
|
+
start = newline + 1;
|
|
343
|
+
}
|
|
344
|
+
const lines = [];
|
|
345
|
+
for (let line = first; line <= index + radius; line++) {
|
|
346
|
+
const newline = content.indexOf("\n", start);
|
|
347
|
+
const end = newline < 0 ? content.length : newline;
|
|
348
|
+
lines.push(content.slice(start, end).replace(/\r$/, ""));
|
|
349
|
+
if (newline < 0)
|
|
350
|
+
break;
|
|
351
|
+
start = newline + 1;
|
|
352
|
+
}
|
|
353
|
+
const at = index - first;
|
|
354
|
+
return at < lines.length ? { lines, at } : null;
|
|
355
|
+
}
|
|
356
|
+
function contextFor(loaded, sourceIndex, line) {
|
|
215
357
|
let content = null;
|
|
216
358
|
try {
|
|
217
|
-
content =
|
|
359
|
+
content = loaded.contentOf(sourceIndex);
|
|
218
360
|
}
|
|
219
361
|
catch {
|
|
220
362
|
content = null;
|
|
221
363
|
}
|
|
222
364
|
if (!content)
|
|
223
365
|
return {};
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
if (index < 0 || index >= lines.length)
|
|
366
|
+
const window = linesAround(content, line - 1, CONTEXT_LINES);
|
|
367
|
+
if (!window)
|
|
227
368
|
return {};
|
|
228
|
-
|
|
369
|
+
// Source code can hold secrets: scrubbed like the stack. Cut first so the
|
|
370
|
+
// scrub work stays bounded, with slack so a secret straddling the cut is
|
|
371
|
+
// still seen whole.
|
|
372
|
+
const clean = (value) => scrubText(value.slice(0, MAX_CONTEXT_CHARS + 512)).slice(0, MAX_CONTEXT_CHARS);
|
|
229
373
|
return {
|
|
230
|
-
contextLine:
|
|
231
|
-
preContext: lines.slice(
|
|
232
|
-
postContext: lines.slice(
|
|
374
|
+
contextLine: clean(window.lines[window.at]),
|
|
375
|
+
preContext: window.lines.slice(0, window.at).map(clean),
|
|
376
|
+
postContext: window.lines.slice(window.at + 1).map(clean),
|
|
233
377
|
};
|
|
234
378
|
}
|
|
235
379
|
export class Symbolicator {
|
|
@@ -271,7 +415,12 @@ export class Symbolicator {
|
|
|
271
415
|
return null;
|
|
272
416
|
if (!JS_FILE_RE.test(frame.filename))
|
|
273
417
|
return null;
|
|
274
|
-
|
|
418
|
+
// A server's dependency and runtime frames (node_modules/, node:) run
|
|
419
|
+
// from files no upload covers; looking them up by suffix could even
|
|
420
|
+
// pick an app bundle that happens to share the file name.
|
|
421
|
+
if (scope.server && !frame.inApp)
|
|
422
|
+
return null;
|
|
423
|
+
const path = scope.server ? serverScriptPath(frame.filename) : scriptPath(frame.filename);
|
|
275
424
|
if (!path)
|
|
276
425
|
return null;
|
|
277
426
|
const debugId = this.debugIdFor(frame.filename, scope.debugIds);
|
|
@@ -283,6 +432,7 @@ export class Symbolicator {
|
|
|
283
432
|
release: scope.release,
|
|
284
433
|
filename: path,
|
|
285
434
|
...(debugId ? { debugId } : {}),
|
|
435
|
+
...(scope.server ? { suffix: true } : {}),
|
|
286
436
|
};
|
|
287
437
|
}
|
|
288
438
|
async resolveAll(frames, scope) {
|
|
@@ -366,15 +516,17 @@ function resolveFrame(loaded, frame, position, segment) {
|
|
|
366
516
|
}
|
|
367
517
|
})();
|
|
368
518
|
const line = segment.line + 1;
|
|
519
|
+
const inApp = !ignored && isInAppPath(filename);
|
|
369
520
|
const resolved = {
|
|
370
521
|
function: frame.function,
|
|
371
522
|
module: null,
|
|
372
523
|
filename,
|
|
373
524
|
lineno: line,
|
|
374
525
|
colno: segment.column + 1,
|
|
375
|
-
inApp
|
|
526
|
+
inApp,
|
|
376
527
|
resolved: true,
|
|
377
|
-
|
|
528
|
+
// Context lines are the evidence for the app's own code (RF-05).
|
|
529
|
+
...(inApp ? contextFor(loaded, segment.source, line) : {}),
|
|
378
530
|
};
|
|
379
531
|
const name = segment.name >= 0 ? map.names[segment.name] : undefined;
|
|
380
532
|
return {
|
package/dist/trace-dedupe.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ import type { RuntimeOccurrenceInput } from "./types.js";
|
|
|
43
43
|
* message, severity, time and batch position (so its deterministic id), and
|
|
44
44
|
* takes from the others what it lacks: attributes (its own win), stack,
|
|
45
45
|
* route, method, status, request id, declared error fields, fingerprint
|
|
46
|
-
* override and
|
|
46
|
+
* override, breadcrumbs and debug ids. `autter.merged_recordings` counts the merged
|
|
47
47
|
* recordings.
|
|
48
48
|
*
|
|
49
49
|
* Rule 2, redundant status-only occurrences. A status-only `HttpServerError`
|
package/dist/trace-dedupe.js
CHANGED
|
@@ -46,7 +46,7 @@ import { parseStackFrames } from "./stack-frames.js";
|
|
|
46
46
|
* message, severity, time and batch position (so its deterministic id), and
|
|
47
47
|
* takes from the others what it lacks: attributes (its own win), stack,
|
|
48
48
|
* route, method, status, request id, declared error fields, fingerprint
|
|
49
|
-
* override and
|
|
49
|
+
* override, breadcrumbs and debug ids. `autter.merged_recordings` counts the merged
|
|
50
50
|
* recordings.
|
|
51
51
|
*
|
|
52
52
|
* Rule 2, redundant status-only occurrences. A status-only `HttpServerError`
|
|
@@ -245,6 +245,8 @@ function mergeInto(kept, others) {
|
|
|
245
245
|
}
|
|
246
246
|
if (!merged.breadcrumbs?.length && other.breadcrumbs?.length)
|
|
247
247
|
merged.breadcrumbs = other.breadcrumbs;
|
|
248
|
+
if (!merged.debugIds && other.debugIds)
|
|
249
|
+
merged.debugIds = other.debugIds;
|
|
248
250
|
}
|
|
249
251
|
attributes["autter.merged_recordings"] = others.length + 1;
|
|
250
252
|
// Same bounds as every other occurrence (128 keys, kept's own first).
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Breadcrumb } from "./breadcrumbs.js";
|
|
2
|
+
import type { LatencySamples } from "./latency.js";
|
|
2
3
|
import type { SuppressionRule } from "./suppression.js";
|
|
3
4
|
export type RuntimeSource = "browser" | "server";
|
|
4
5
|
/**
|
|
@@ -56,9 +57,10 @@ export interface RuntimeOccurrenceInput {
|
|
|
56
57
|
/** Steps before the error, oldest first, ≤ 30 (validated by breadcrumbs.ts). */
|
|
57
58
|
breadcrumbs?: Breadcrumb[];
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Script URL (browser) or script path (server, the SDK's
|
|
61
|
+
* `autter.debug_ids`) → debug id for the bundles in the stack, used to
|
|
62
|
+
* find their source maps (EI-13, RF-05). Input-only: never stored or
|
|
63
|
+
* sent to the sink.
|
|
62
64
|
*/
|
|
63
65
|
debugIds?: Record<string, string>;
|
|
64
66
|
}
|
|
@@ -79,18 +81,23 @@ export interface StackFrame {
|
|
|
79
81
|
inApp: boolean;
|
|
80
82
|
/** True when a source map resolved the frame. */
|
|
81
83
|
resolved: boolean;
|
|
82
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The frame's own source line, on in-app resolved frames whose map
|
|
86
|
+
* carried the source's `sourcesContent` (≤ 300 chars, scrubbed).
|
|
87
|
+
*/
|
|
83
88
|
contextLine?: string;
|
|
84
|
-
/**
|
|
89
|
+
/** Up to 5 lines before the frame's line, in file order (RF-05; 3 before), ≤ 300 chars each. */
|
|
85
90
|
preContext?: string[];
|
|
91
|
+
/** Up to 5 lines after it, in file order, ≤ 300 chars each. */
|
|
86
92
|
postContext?: string[];
|
|
87
93
|
}
|
|
88
94
|
/**
|
|
89
95
|
* Every grouping scheme an issue identity can come from (contract §1).
|
|
90
|
-
* The ingester computes `manual-v1`, `code-v1`, `frames-v3`
|
|
91
|
-
* `
|
|
96
|
+
* The ingester computes `manual-v1`, `code-v1`, `frames-v3`, `message-v1`
|
|
97
|
+
* and `cause-v1` (RF-06); `rule-v1` and `external-v1` exist only on the
|
|
98
|
+
* platform.
|
|
92
99
|
*/
|
|
93
|
-
export type FingerprintSchemeV2 = "manual-v1" | "rule-v1" | "code-v1" | "frames-v3" | "message-v1" | "external-v1";
|
|
100
|
+
export type FingerprintSchemeV2 = "manual-v1" | "rule-v1" | "code-v1" | "frames-v3" | "message-v1" | "cause-v1" | "external-v1";
|
|
94
101
|
export interface FingerprintCandidate {
|
|
95
102
|
scheme: FingerprintSchemeV2;
|
|
96
103
|
/** 32 lowercase hex characters. */
|
|
@@ -115,11 +122,12 @@ export interface RuntimeOccurrence extends RuntimeOccurrenceInput {
|
|
|
115
122
|
fingerprint: string;
|
|
116
123
|
/** "code-v1" when grouped by a valid error code, else "message-v1". */
|
|
117
124
|
fingerprintScheme: "message-v1" | "code-v1";
|
|
118
|
-
/** Parsed (and, for
|
|
125
|
+
/** Parsed (and, for bundled JS, symbolicated) frames, innermost first, ≤ 50. */
|
|
119
126
|
frames?: StackFrame[];
|
|
120
127
|
/**
|
|
121
|
-
* Every applicable identity,
|
|
122
|
-
*
|
|
128
|
+
* Every applicable identity, in precedence order (manual-v1, code-v1,
|
|
129
|
+
* frames-v3, message-v1, cause-v1); always includes the legacy one,
|
|
130
|
+
* which is last unless a cause-v1 candidate follows it.
|
|
123
131
|
*/
|
|
124
132
|
fingerprints?: FingerprintCandidate[];
|
|
125
133
|
/** What went into the frames-v3 fingerprint, when one was computed. */
|
|
@@ -200,6 +208,13 @@ export interface RuntimeMetricPoint {
|
|
|
200
208
|
errorCount: number;
|
|
201
209
|
durationSumMs: number;
|
|
202
210
|
sessionCount: number;
|
|
211
|
+
/**
|
|
212
|
+
* Latency samples behind `durationSumMs` (IP-02, latency.ts), written to
|
|
213
|
+
* `runtime_route_latency_1m`. Absent when the feed has no durations
|
|
214
|
+
* (errors outside requests, browser usage counters). Not forwarded to
|
|
215
|
+
* the sink.
|
|
216
|
+
*/
|
|
217
|
+
latency?: LatencySamples;
|
|
203
218
|
}
|
|
204
219
|
/**
|
|
205
220
|
* The tenant a validated ingest key resolves to.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autter/otlp-ingester",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Self-hostable OTLP + browser-error ingest service for Autter Runtime: normalises telemetry into a per-repo ClickHouse data model",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|