@reliantlabs/forge-web-runtime 0.9.2 → 0.10.1

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 CHANGED
@@ -157,6 +157,70 @@ object, embed` are blocked, images are not inlined and fonts are not collected.
157
157
  fetch/XHR spans have their `http.url` / `url.full` stripped of query string and
158
158
  fragment; request and response bodies and headers are not captured.
159
159
 
160
+ ## Node and Electron-main telemetry (`/node`)
161
+
162
+ `initNodeTelemetry` is the browser entry's twin for a Node process — an
163
+ Electron app's main process, a CLI. It ships logs, errors and spans as
164
+ OTLP/JSON to the same ingest, normally the keyed one
165
+ (`https://<ingest host>/k/<key>`), using Node built-ins only: no
166
+ OpenTelemetry SDK packages to install beside `@opentelemetry/api`.
167
+
168
+ ```js
169
+ import { app } from "electron";
170
+ import {
171
+ flushOnQuit,
172
+ frontendServiceName,
173
+ initNodeTelemetry,
174
+ } from "@reliantlabs/forge-web-runtime/node";
175
+
176
+ const telemetry = initNodeTelemetry({
177
+ serviceName: frontendServiceName("shop", "desktop"), // "<project>-<frontend>", as forge derives it
178
+ serviceVersion: app.getVersion(),
179
+ environment: "prod",
180
+ appClient: "electron",
181
+ endpoint: "https://ingest.example.dev/k/pk_live_…", // unset, "" or "off": telemetry is off
182
+ origin: "app://bundle", // the keyed ingest admits only an allowlisted Origin
183
+ appRoot: app.getAppPath(), // stack paths are reported relative to it
184
+ });
185
+ flushOnQuit(app); // hold before-quit until the queue is out (2s at most)
186
+
187
+ telemetry?.logger.warn("renderer gone", { reason: "oom" });
188
+ telemetry?.reportError(err, { "settings.section": "privacy" });
189
+ telemetry?.tracer.startActiveSpan("backend.start", (span) => {
190
+ /* … */ span.end();
191
+ });
192
+ ```
193
+
194
+ - **Uncaught exceptions and unhandled rejections** become ERROR records with
195
+ `exception.type`, `exception.message`, `exception.stacktrace` and
196
+ `code.function.name` — the keys forge/pkg/observe writes for a server error —
197
+ and no `rum.sessionId`, so `forge obs issues` groups them with sfp1.
198
+ `code.function.name` is the innermost application frame (not Node, Electron or
199
+ `node_modules`), written `fn (src/file.js)`.
200
+ - **The process's fate is unchanged.** Exceptions are watched with
201
+ `uncaughtExceptionMonitor`. When the runtime's `unhandledRejection` listener is
202
+ the only one, it flushes, then raises the rejection as Node would have
203
+ (`--unhandled-rejections` is honoured). In plain Node with no
204
+ `uncaughtException` handler, it flushes and then exits as Node does; under
205
+ Electron nothing is installed there. `captureProcessErrors: false` turns all
206
+ of this off.
207
+ - **Batching:** records wait up to `batchDelayMs` (5s) or `maxBatchSize` (256);
208
+ at most `maxQueueSize` (2048) per signal are held. Timers are unref'd. A fatal
209
+ record is sent at once, and an idle process flushes on `beforeExit`.
210
+ `flush(ms)` / `shutdown(ms)` resolve by their timeout whatever the network
211
+ does. An app that already defers `before-quit` for its own shutdown awaits
212
+ `telemetry.flush()` as the last step of it instead of calling `flushOnQuit`.
213
+ - **Privacy:** an exception keeps its message's first line, with credential
214
+ shapes redacted (the patterns `track()` uses) and at most 1024 chars; the
215
+ stack is rebuilt from that line and its frames, with the home directory, the
216
+ install path and `app.asar` removed from every path. String attributes and
217
+ bodies are redacted and bounded, and object-valued attributes are dropped.
218
+ `setUser(id)` stamps the opaque `userId` the browser entry stamps. Nothing
219
+ else about the machine is sent: no host name, IP, argv or environment.
220
+ - The global `@opentelemetry/api` tracer, AsyncLocalStorage context manager and
221
+ W3C `traceparent` propagator are this runtime's unless `registerGlobal: false`.
222
+ `reportException` from the barrel reports through it once it is running.
223
+
160
224
  ## The DOM-free interceptors entry point
161
225
 
162
226
  ```ts
@@ -0,0 +1,73 @@
1
+ /** Log attribute keys an ERROR record carries; the same keys observe writes. */
2
+ export declare const EXCEPTION_TYPE = "exception.type";
3
+ export declare const EXCEPTION_MESSAGE = "exception.message";
4
+ export declare const EXCEPTION_STACKTRACE = "exception.stacktrace";
5
+ export declare const CODE_FUNCTION = "code.function.name";
6
+ export declare const CODE_FILE = "code.file.path";
7
+ export declare const CODE_LINE = "code.line.number";
8
+ /** Pins a record to a named issue whatever its call site (observe.ErrorGroup). */
9
+ export declare const ERROR_GROUP = "error.group";
10
+ /** Longest exception.message, in characters: observe's MaxExceptionMessage. */
11
+ export declare const MAX_EXCEPTION_MESSAGE_CHARS = 1024;
12
+ /** Longest exception.stacktrace, in characters: observe's maxStacktrace. */
13
+ export declare const MAX_STACKTRACE_CHARS: number;
14
+ /** Most frames kept from a stack. V8 records 10 by default. */
15
+ export declare const MAX_FRAMES = 64;
16
+ /** How paths in a stack are rewritten. */
17
+ export interface PathScrub {
18
+ /** Files below it are reported relative to it (Electron: app.getAppPath()). */
19
+ appRoot?: string;
20
+ /** Files below it that are not below appRoot start with `~`. */
21
+ homeDir?: string;
22
+ /** Compare paths case-insensitively (win32). */
23
+ caseInsensitive?: boolean;
24
+ }
25
+ /** One parsed V8 stack frame. */
26
+ export interface StackFrame {
27
+ /** The function as V8 names it, without `async `, `new ` or `[as alias]`; "" when anonymous. */
28
+ fn: string;
29
+ /** The file, already rewritten by the PathScrub. */
30
+ file: string;
31
+ line: number;
32
+ column: number;
33
+ }
34
+ /**
35
+ * The path a record may carry for `file`: relative below the app root, from
36
+ * inside an Electron archive, `~`-rooted below the home directory, otherwise
37
+ * unchanged. Pure; exported for tests.
38
+ */
39
+ export declare function scrubPath(file: string, scrub: PathScrub): string;
40
+ /** Parse a V8 stack's frame lines. Lines that are not frames are skipped. */
41
+ export declare function parseStack(stack: string, scrub: PathScrub): StackFrame[];
42
+ /**
43
+ * Whether a frame is the application's own code: not Node or Electron
44
+ * internals, not a dependency, not eval. The innermost such frame is where a
45
+ * failure "began" for grouping, as observe takes the innermost application
46
+ * frame of a Go stack.
47
+ */
48
+ export declare function isApplicationFrame(frame: StackFrame): boolean;
49
+ /**
50
+ * code.function.name for a frame: V8's own frame shape without line and
51
+ * column, `createWindow (src/main.js)`. The file is part of it because sfp1
52
+ * hashes this attribute and not code.file.path, and two `handle` functions in
53
+ * two files are two places.
54
+ */
55
+ export declare function frameFunction(frame: StackFrame): string;
56
+ /** Cut to `max` characters, ellipsis included. */
57
+ export declare function bound(value: string, max: number): string;
58
+ /** First line, credential shapes redacted, bounded: what a message may say. */
59
+ export declare function boundMessage(message: string): string;
60
+ /**
61
+ * exception.type for a thrown value: the class name for an Error (a subclass
62
+ * that never set `name` still reports its class), `typeof` for anything else.
63
+ */
64
+ export declare function exceptionType(value: unknown): string;
65
+ /** Attribute values an ERROR record carries. */
66
+ export type ExceptionRecordAttributes = Record<string, string | number>;
67
+ /**
68
+ * The attributes an ERROR record about `value` carries: exception.type,
69
+ * exception.message, and — when the value has a stack — exception.stacktrace
70
+ * and the code.* location of its innermost application frame. Pure given
71
+ * `scrub`; never throws.
72
+ */
73
+ export declare function exceptionAttributes(value: unknown, scrub: PathScrub): ExceptionRecordAttributes;
@@ -0,0 +1,278 @@
1
+ // Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
2
+ //
3
+ // What a Node / Electron-main ERROR record carries so the issues pipeline
4
+ // groups it: the Node twin of forge/pkg/observe's error_inputs.go.
5
+ //
6
+ // A record from `/node` has no `rum.sessionId`, so the control plane's issue
7
+ // view treats it as a SERVER error and fingerprints it with sfp1
8
+ // (forge/pkg/obsquery/fingerprint.go). sfp1 hashes the service, then
9
+ //
10
+ // code.function.name where the failure began (never a line number)
11
+ // norm(Body) the message template ("uncaught exception", ...)
12
+ // exception.type the kind of failure
13
+ //
14
+ // so this module derives exactly those keys, under the same names observe
15
+ // writes, plus exception.message, exception.stacktrace, code.file.path and
16
+ // code.line.number for whoever reads the issue.
17
+ //
18
+ // ── Two installs must group as one ───────────────────────────────────────
19
+ //
20
+ // A stack frame names a file on the user's disk: /Users/<name>/..., or
21
+ // C:\Users\<name>\AppData\... on Windows. That is a username, and it would
22
+ // also split one bug into an issue per machine, because code.function.name
23
+ // carries the file. So every path is rewritten before it leaves: below the
24
+ // app root it becomes relative (`src/main.js`), inside an Electron archive it
25
+ // starts after `app.asar/`, and anything else under the home directory starts
26
+ // with `~`. The same rewrite runs over the whole stack text.
27
+ //
28
+ // ── What leaves, and what does not ───────────────────────────────────────
29
+ //
30
+ // The message is cut to its first line, has credential shapes redacted (the
31
+ // same patterns `track()` uses, redactSecrets in track.ts) and is bounded.
32
+ // The stack is REBUILT as "<type>: <that message>" plus the frame lines, so a
33
+ // multi-line message (stderr, tool output, a file's contents) cannot ride in
34
+ // through the stack's first line. A thrown non-Error object contributes its
35
+ // type and a string `message` property only: an object literal is where a
36
+ // payload hides, and it is never serialized.
37
+ import { redactSecrets } from "./track.js";
38
+ /** Log attribute keys an ERROR record carries; the same keys observe writes. */
39
+ export const EXCEPTION_TYPE = "exception.type";
40
+ export const EXCEPTION_MESSAGE = "exception.message";
41
+ export const EXCEPTION_STACKTRACE = "exception.stacktrace";
42
+ export const CODE_FUNCTION = "code.function.name";
43
+ export const CODE_FILE = "code.file.path";
44
+ export const CODE_LINE = "code.line.number";
45
+ /** Pins a record to a named issue whatever its call site (observe.ErrorGroup). */
46
+ export const ERROR_GROUP = "error.group";
47
+ /** Longest exception.message, in characters: observe's MaxExceptionMessage. */
48
+ export const MAX_EXCEPTION_MESSAGE_CHARS = 1024;
49
+ /** Longest exception.stacktrace, in characters: observe's maxStacktrace. */
50
+ export const MAX_STACKTRACE_CHARS = 16 * 1024;
51
+ /** Most frames kept from a stack. V8 records 10 by default. */
52
+ export const MAX_FRAMES = 64;
53
+ const ELLIPSIS = "…";
54
+ // ` at fn (file:line:col)`, ` at file:line:col`, ` at async fn (...)`.
55
+ // The file part is greedy up to the LAST :line:col, so a Windows drive letter
56
+ // (C:\...) or a URL scheme (file:///...) stays in it.
57
+ const FRAME_RE = /^\s*at (?:(.*?) \()?(.+):(\d+):(\d+)\)?\s*$/;
58
+ /** Normalize separators so one comparison serves POSIX and Windows paths. */
59
+ function slashes(p) {
60
+ return p.replace(/\\/g, "/");
61
+ }
62
+ function stripFileUrl(p) {
63
+ if (!p.startsWith("file://")) {
64
+ return p;
65
+ }
66
+ let rest = p.slice("file://".length);
67
+ // file:///C:/x -> C:/x ; file:///home/x -> /home/x
68
+ if (/^\/[A-Za-z]:\//.test(rest)) {
69
+ rest = rest.slice(1);
70
+ }
71
+ try {
72
+ return decodeURIComponent(rest);
73
+ }
74
+ catch {
75
+ return rest;
76
+ }
77
+ }
78
+ function under(path, root, ci) {
79
+ if (!root) {
80
+ return null;
81
+ }
82
+ let r = slashes(root).replace(/\/+$/, "");
83
+ if (r === "") {
84
+ return null;
85
+ }
86
+ let p = path;
87
+ if (ci) {
88
+ r = r.toLowerCase();
89
+ p = path.toLowerCase();
90
+ }
91
+ if (p === r) {
92
+ return "";
93
+ }
94
+ if (p.startsWith(r + "/")) {
95
+ return path.slice(r.length + 1);
96
+ }
97
+ return null;
98
+ }
99
+ /**
100
+ * The path a record may carry for `file`: relative below the app root, from
101
+ * inside an Electron archive, `~`-rooted below the home directory, otherwise
102
+ * unchanged. Pure; exported for tests.
103
+ */
104
+ export function scrubPath(file, scrub) {
105
+ const ci = scrub.caseInsensitive ?? false;
106
+ const p = slashes(stripFileUrl(file));
107
+ const rel = under(p, scrub.appRoot, ci);
108
+ if (rel !== null) {
109
+ return rel;
110
+ }
111
+ const asar = p.search(/\/app\.asar(?:\.unpacked)?\//);
112
+ if (asar !== -1) {
113
+ return p.slice(p.indexOf("/", asar + 1) + 1);
114
+ }
115
+ const home = under(p, scrub.homeDir, ci);
116
+ if (home !== null) {
117
+ return home === "" ? "~" : `~/${home}`;
118
+ }
119
+ return p;
120
+ }
121
+ /** Parse a V8 stack's frame lines. Lines that are not frames are skipped. */
122
+ export function parseStack(stack, scrub) {
123
+ const frames = [];
124
+ for (const line of stack.split("\n")) {
125
+ if (frames.length >= MAX_FRAMES) {
126
+ break;
127
+ }
128
+ const m = FRAME_RE.exec(line);
129
+ if (!m) {
130
+ continue;
131
+ }
132
+ // `Timeout.explode [as _onTimeout]`: the alias is the property the
133
+ // function was reached through, which changes with the caller.
134
+ const fn = (m[1] ?? "")
135
+ .replace(/^(?:async |new )/, "")
136
+ .replace(/ \[as [^\]]*\]$/, "")
137
+ .trim();
138
+ frames.push({
139
+ fn,
140
+ file: scrubPath(m[2] ?? "", scrub),
141
+ line: Number(m[3]),
142
+ column: Number(m[4]),
143
+ });
144
+ }
145
+ return frames;
146
+ }
147
+ /**
148
+ * Whether a frame is the application's own code: not Node or Electron
149
+ * internals, not a dependency, not eval. The innermost such frame is where a
150
+ * failure "began" for grouping, as observe takes the innermost application
151
+ * frame of a Go stack.
152
+ */
153
+ export function isApplicationFrame(frame) {
154
+ const f = frame.file;
155
+ if (f === "" || f === "<anonymous>" || f === "native") {
156
+ return false;
157
+ }
158
+ if (f.startsWith("node:") ||
159
+ f.startsWith("internal/") ||
160
+ f.startsWith("electron/")) {
161
+ return false;
162
+ }
163
+ if (f.includes("node_modules/") || f.includes("node_modules.asar")) {
164
+ return false;
165
+ }
166
+ if (f.includes("electron.asar") || f.includes("electron/js2c")) {
167
+ return false;
168
+ }
169
+ return true;
170
+ }
171
+ /**
172
+ * code.function.name for a frame: V8's own frame shape without line and
173
+ * column, `createWindow (src/main.js)`. The file is part of it because sfp1
174
+ * hashes this attribute and not code.file.path, and two `handle` functions in
175
+ * two files are two places.
176
+ */
177
+ export function frameFunction(frame) {
178
+ return `${frame.fn || "<anonymous>"} (${frame.file})`;
179
+ }
180
+ /** Cut to `max` characters, ellipsis included. */
181
+ export function bound(value, max) {
182
+ if (value.length <= max) {
183
+ return value;
184
+ }
185
+ return value.slice(0, Math.max(0, max - ELLIPSIS.length)) + ELLIPSIS;
186
+ }
187
+ /** First line, credential shapes redacted, bounded: what a message may say. */
188
+ export function boundMessage(message) {
189
+ const nl = message.indexOf("\n");
190
+ const first = (nl === -1 ? message : message.slice(0, nl)).replace(/\s+$/, "");
191
+ // Cut before redacting so the scan is bounded, with headroom so a secret
192
+ // straddling the final cut is still seen whole.
193
+ return bound(redactSecrets(first.slice(0, MAX_EXCEPTION_MESSAGE_CHARS * 2)), MAX_EXCEPTION_MESSAGE_CHARS);
194
+ }
195
+ /**
196
+ * exception.type for a thrown value: the class name for an Error (a subclass
197
+ * that never set `name` still reports its class), `typeof` for anything else.
198
+ */
199
+ export function exceptionType(value) {
200
+ if (value instanceof Error) {
201
+ const ctor = value.constructor
202
+ ?.name;
203
+ if (typeof ctor === "string" &&
204
+ ctor !== "" &&
205
+ ctor !== "Error" &&
206
+ ctor !== "Object") {
207
+ return ctor;
208
+ }
209
+ return typeof value.name === "string" && value.name !== ""
210
+ ? value.name
211
+ : "Error";
212
+ }
213
+ if (value === null) {
214
+ return "null";
215
+ }
216
+ return typeof value;
217
+ }
218
+ function rawMessage(value) {
219
+ if (value instanceof Error) {
220
+ return typeof value.message === "string" ? value.message : "";
221
+ }
222
+ switch (typeof value) {
223
+ case "string":
224
+ return value;
225
+ case "number":
226
+ case "boolean":
227
+ case "bigint":
228
+ case "symbol":
229
+ return String(value);
230
+ case "object": {
231
+ // Never serialized: an object literal is where a payload hides.
232
+ const m = value?.message;
233
+ return typeof m === "string" ? m : "";
234
+ }
235
+ default:
236
+ return "";
237
+ }
238
+ }
239
+ /**
240
+ * The attributes an ERROR record about `value` carries: exception.type,
241
+ * exception.message, and — when the value has a stack — exception.stacktrace
242
+ * and the code.* location of its innermost application frame. Pure given
243
+ * `scrub`; never throws.
244
+ */
245
+ export function exceptionAttributes(value, scrub) {
246
+ const type = exceptionType(value);
247
+ const message = boundMessage(rawMessage(value));
248
+ const out = {
249
+ [EXCEPTION_TYPE]: type,
250
+ [EXCEPTION_MESSAGE]: message,
251
+ };
252
+ let stack;
253
+ try {
254
+ stack = value?.stack;
255
+ }
256
+ catch {
257
+ stack = undefined;
258
+ }
259
+ if (typeof stack !== "string" || stack === "") {
260
+ return out;
261
+ }
262
+ const frames = parseStack(stack, scrub);
263
+ if (frames.length === 0) {
264
+ return out;
265
+ }
266
+ const header = message === "" ? type : `${type}: ${message}`;
267
+ const lines = frames.map((f) => ` at ${f.fn ? `${f.fn} (${f.file}:${f.line}:${f.column})` : `${f.file}:${f.line}:${f.column}`}`);
268
+ // The top of the stack — where it failed — is the part kept.
269
+ out[EXCEPTION_STACKTRACE] = bound(redactSecrets([header, ...lines].join("\n")), MAX_STACKTRACE_CHARS);
270
+ const origin = frames.find(isApplicationFrame) ?? frames[0];
271
+ if (origin) {
272
+ out[CODE_FUNCTION] = frameFunction(origin);
273
+ out[CODE_FILE] = origin.file;
274
+ out[CODE_LINE] = origin.line;
275
+ }
276
+ return out;
277
+ }
278
+ //# sourceMappingURL=node-exception.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-exception.js","sourceRoot":"","sources":["../src/node-exception.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// What a Node / Electron-main ERROR record carries so the issues pipeline\n// groups it: the Node twin of forge/pkg/observe's error_inputs.go.\n//\n// A record from `/node` has no `rum.sessionId`, so the control plane's issue\n// view treats it as a SERVER error and fingerprints it with sfp1\n// (forge/pkg/obsquery/fingerprint.go). sfp1 hashes the service, then\n//\n//\tcode.function.name where the failure began (never a line number)\n//\tnorm(Body) the message template (\"uncaught exception\", ...)\n//\texception.type the kind of failure\n//\n// so this module derives exactly those keys, under the same names observe\n// writes, plus exception.message, exception.stacktrace, code.file.path and\n// code.line.number for whoever reads the issue.\n//\n// ── Two installs must group as one ───────────────────────────────────────\n//\n// A stack frame names a file on the user's disk: /Users/<name>/..., or\n// C:\\Users\\<name>\\AppData\\... on Windows. That is a username, and it would\n// also split one bug into an issue per machine, because code.function.name\n// carries the file. So every path is rewritten before it leaves: below the\n// app root it becomes relative (`src/main.js`), inside an Electron archive it\n// starts after `app.asar/`, and anything else under the home directory starts\n// with `~`. The same rewrite runs over the whole stack text.\n//\n// ── What leaves, and what does not ───────────────────────────────────────\n//\n// The message is cut to its first line, has credential shapes redacted (the\n// same patterns `track()` uses, redactSecrets in track.ts) and is bounded.\n// The stack is REBUILT as \"<type>: <that message>\" plus the frame lines, so a\n// multi-line message (stderr, tool output, a file's contents) cannot ride in\n// through the stack's first line. A thrown non-Error object contributes its\n// type and a string `message` property only: an object literal is where a\n// payload hides, and it is never serialized.\nimport { redactSecrets } from \"./track.js\";\n\n/** Log attribute keys an ERROR record carries; the same keys observe writes. */\nexport const EXCEPTION_TYPE = \"exception.type\";\nexport const EXCEPTION_MESSAGE = \"exception.message\";\nexport const EXCEPTION_STACKTRACE = \"exception.stacktrace\";\nexport const CODE_FUNCTION = \"code.function.name\";\nexport const CODE_FILE = \"code.file.path\";\nexport const CODE_LINE = \"code.line.number\";\n/** Pins a record to a named issue whatever its call site (observe.ErrorGroup). */\nexport const ERROR_GROUP = \"error.group\";\n\n/** Longest exception.message, in characters: observe's MaxExceptionMessage. */\nexport const MAX_EXCEPTION_MESSAGE_CHARS = 1024;\n/** Longest exception.stacktrace, in characters: observe's maxStacktrace. */\nexport const MAX_STACKTRACE_CHARS = 16 * 1024;\n/** Most frames kept from a stack. V8 records 10 by default. */\nexport const MAX_FRAMES = 64;\n\nconst ELLIPSIS = \"…\";\n\n/** How paths in a stack are rewritten. */\nexport interface PathScrub {\n /** Files below it are reported relative to it (Electron: app.getAppPath()). */\n appRoot?: string;\n /** Files below it that are not below appRoot start with `~`. */\n homeDir?: string;\n /** Compare paths case-insensitively (win32). */\n caseInsensitive?: boolean;\n}\n\n/** One parsed V8 stack frame. */\nexport interface StackFrame {\n /** The function as V8 names it, without `async `, `new ` or `[as alias]`; \"\" when anonymous. */\n fn: string;\n /** The file, already rewritten by the PathScrub. */\n file: string;\n line: number;\n column: number;\n}\n\n// ` at fn (file:line:col)`, ` at file:line:col`, ` at async fn (...)`.\n// The file part is greedy up to the LAST :line:col, so a Windows drive letter\n// (C:\\...) or a URL scheme (file:///...) stays in it.\nconst FRAME_RE = /^\\s*at (?:(.*?) \\()?(.+):(\\d+):(\\d+)\\)?\\s*$/;\n\n/** Normalize separators so one comparison serves POSIX and Windows paths. */\nfunction slashes(p: string): string {\n return p.replace(/\\\\/g, \"/\");\n}\n\nfunction stripFileUrl(p: string): string {\n if (!p.startsWith(\"file://\")) {\n return p;\n }\n let rest = p.slice(\"file://\".length);\n // file:///C:/x -> C:/x ; file:///home/x -> /home/x\n if (/^\\/[A-Za-z]:\\//.test(rest)) {\n rest = rest.slice(1);\n }\n try {\n return decodeURIComponent(rest);\n } catch {\n return rest;\n }\n}\n\nfunction under(\n path: string,\n root: string | undefined,\n ci: boolean,\n): string | null {\n if (!root) {\n return null;\n }\n let r = slashes(root).replace(/\\/+$/, \"\");\n if (r === \"\") {\n return null;\n }\n let p = path;\n if (ci) {\n r = r.toLowerCase();\n p = path.toLowerCase();\n }\n if (p === r) {\n return \"\";\n }\n if (p.startsWith(r + \"/\")) {\n return path.slice(r.length + 1);\n }\n return null;\n}\n\n/**\n * The path a record may carry for `file`: relative below the app root, from\n * inside an Electron archive, `~`-rooted below the home directory, otherwise\n * unchanged. Pure; exported for tests.\n */\nexport function scrubPath(file: string, scrub: PathScrub): string {\n const ci = scrub.caseInsensitive ?? false;\n const p = slashes(stripFileUrl(file));\n const rel = under(p, scrub.appRoot, ci);\n if (rel !== null) {\n return rel;\n }\n const asar = p.search(/\\/app\\.asar(?:\\.unpacked)?\\//);\n if (asar !== -1) {\n return p.slice(p.indexOf(\"/\", asar + 1) + 1);\n }\n const home = under(p, scrub.homeDir, ci);\n if (home !== null) {\n return home === \"\" ? \"~\" : `~/${home}`;\n }\n return p;\n}\n\n/** Parse a V8 stack's frame lines. Lines that are not frames are skipped. */\nexport function parseStack(stack: string, scrub: PathScrub): StackFrame[] {\n const frames: StackFrame[] = [];\n for (const line of stack.split(\"\\n\")) {\n if (frames.length >= MAX_FRAMES) {\n break;\n }\n const m = FRAME_RE.exec(line);\n if (!m) {\n continue;\n }\n // `Timeout.explode [as _onTimeout]`: the alias is the property the\n // function was reached through, which changes with the caller.\n const fn = (m[1] ?? \"\")\n .replace(/^(?:async |new )/, \"\")\n .replace(/ \\[as [^\\]]*\\]$/, \"\")\n .trim();\n frames.push({\n fn,\n file: scrubPath(m[2] ?? \"\", scrub),\n line: Number(m[3]),\n column: Number(m[4]),\n });\n }\n return frames;\n}\n\n/**\n * Whether a frame is the application's own code: not Node or Electron\n * internals, not a dependency, not eval. The innermost such frame is where a\n * failure \"began\" for grouping, as observe takes the innermost application\n * frame of a Go stack.\n */\nexport function isApplicationFrame(frame: StackFrame): boolean {\n const f = frame.file;\n if (f === \"\" || f === \"<anonymous>\" || f === \"native\") {\n return false;\n }\n if (\n f.startsWith(\"node:\") ||\n f.startsWith(\"internal/\") ||\n f.startsWith(\"electron/\")\n ) {\n return false;\n }\n if (f.includes(\"node_modules/\") || f.includes(\"node_modules.asar\")) {\n return false;\n }\n if (f.includes(\"electron.asar\") || f.includes(\"electron/js2c\")) {\n return false;\n }\n return true;\n}\n\n/**\n * code.function.name for a frame: V8's own frame shape without line and\n * column, `createWindow (src/main.js)`. The file is part of it because sfp1\n * hashes this attribute and not code.file.path, and two `handle` functions in\n * two files are two places.\n */\nexport function frameFunction(frame: StackFrame): string {\n return `${frame.fn || \"<anonymous>\"} (${frame.file})`;\n}\n\n/** Cut to `max` characters, ellipsis included. */\nexport function bound(value: string, max: number): string {\n if (value.length <= max) {\n return value;\n }\n return value.slice(0, Math.max(0, max - ELLIPSIS.length)) + ELLIPSIS;\n}\n\n/** First line, credential shapes redacted, bounded: what a message may say. */\nexport function boundMessage(message: string): string {\n const nl = message.indexOf(\"\\n\");\n const first = (nl === -1 ? message : message.slice(0, nl)).replace(\n /\\s+$/,\n \"\",\n );\n // Cut before redacting so the scan is bounded, with headroom so a secret\n // straddling the final cut is still seen whole.\n return bound(\n redactSecrets(first.slice(0, MAX_EXCEPTION_MESSAGE_CHARS * 2)),\n MAX_EXCEPTION_MESSAGE_CHARS,\n );\n}\n\n/**\n * exception.type for a thrown value: the class name for an Error (a subclass\n * that never set `name` still reports its class), `typeof` for anything else.\n */\nexport function exceptionType(value: unknown): string {\n if (value instanceof Error) {\n const ctor = (value as { constructor?: { name?: unknown } }).constructor\n ?.name;\n if (\n typeof ctor === \"string\" &&\n ctor !== \"\" &&\n ctor !== \"Error\" &&\n ctor !== \"Object\"\n ) {\n return ctor;\n }\n return typeof value.name === \"string\" && value.name !== \"\"\n ? value.name\n : \"Error\";\n }\n if (value === null) {\n return \"null\";\n }\n return typeof value;\n}\n\nfunction rawMessage(value: unknown): string {\n if (value instanceof Error) {\n return typeof value.message === \"string\" ? value.message : \"\";\n }\n switch (typeof value) {\n case \"string\":\n return value;\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n case \"symbol\":\n return String(value);\n case \"object\": {\n // Never serialized: an object literal is where a payload hides.\n const m = (value as { message?: unknown } | null)?.message;\n return typeof m === \"string\" ? m : \"\";\n }\n default:\n return \"\";\n }\n}\n\n/** Attribute values an ERROR record carries. */\nexport type ExceptionRecordAttributes = Record<string, string | number>;\n\n/**\n * The attributes an ERROR record about `value` carries: exception.type,\n * exception.message, and — when the value has a stack — exception.stacktrace\n * and the code.* location of its innermost application frame. Pure given\n * `scrub`; never throws.\n */\nexport function exceptionAttributes(\n value: unknown,\n scrub: PathScrub,\n): ExceptionRecordAttributes {\n const type = exceptionType(value);\n const message = boundMessage(rawMessage(value));\n const out: ExceptionRecordAttributes = {\n [EXCEPTION_TYPE]: type,\n [EXCEPTION_MESSAGE]: message,\n };\n let stack: unknown;\n try {\n stack = (value as { stack?: unknown } | null)?.stack;\n } catch {\n stack = undefined;\n }\n if (typeof stack !== \"string\" || stack === \"\") {\n return out;\n }\n const frames = parseStack(stack, scrub);\n if (frames.length === 0) {\n return out;\n }\n const header = message === \"\" ? type : `${type}: ${message}`;\n const lines = frames.map(\n (f) =>\n ` at ${f.fn ? `${f.fn} (${f.file}:${f.line}:${f.column})` : `${f.file}:${f.line}:${f.column}`}`,\n );\n // The top of the stack — where it failed — is the part kept.\n out[EXCEPTION_STACKTRACE] = bound(\n redactSecrets([header, ...lines].join(\"\\n\")),\n MAX_STACKTRACE_CHARS,\n );\n const origin = frames.find(isApplicationFrame) ?? frames[0];\n if (origin) {\n out[CODE_FUNCTION] = frameFunction(origin);\n out[CODE_FILE] = origin.file;\n out[CODE_LINE] = origin.line;\n }\n return out;\n}\n"]}
@@ -0,0 +1,168 @@
1
+ /** An attribute value a record or span may carry. */
2
+ export type AttributeValue = string | number | boolean | ReadonlyArray<string> | ReadonlyArray<number> | ReadonlyArray<boolean>;
3
+ export type Attributes = Record<string, AttributeValue | undefined>;
4
+ /** Most attributes on one record or span; the rest are dropped. */
5
+ export declare const MAX_ATTRIBUTES = 128;
6
+ /** Longest string attribute value. */
7
+ export declare const MAX_ATTRIBUTE_CHARS = 4096;
8
+ interface AnyValue {
9
+ stringValue?: string;
10
+ boolValue?: boolean;
11
+ intValue?: string;
12
+ doubleValue?: number;
13
+ arrayValue?: {
14
+ values: AnyValue[];
15
+ };
16
+ }
17
+ export interface KeyValue {
18
+ key: string;
19
+ value: AnyValue;
20
+ }
21
+ /** OTLP KeyValues for `attrs`: bounded, unencodable values dropped. */
22
+ export declare function encodeAttributes(attrs: Attributes | undefined): KeyValue[];
23
+ /** Nanoseconds since the epoch for `ms` (fractional ms allowed), as OTLP's decimal string. */
24
+ export declare function unixNanos(ms: number): string;
25
+ /** Now, in epoch milliseconds with sub-millisecond precision. */
26
+ export declare function nowMs(): number;
27
+ /** OTel severity numbers (logs data model). */
28
+ export declare const SEVERITY: {
29
+ readonly DEBUG: 5;
30
+ readonly INFO: 9;
31
+ readonly WARN: 13;
32
+ readonly ERROR: 17;
33
+ };
34
+ export type SeverityText = keyof typeof SEVERITY;
35
+ /** One log record, as OTLP/JSON. */
36
+ export interface LogRecord {
37
+ timeUnixNano: string;
38
+ observedTimeUnixNano: string;
39
+ severityNumber: number;
40
+ severityText: SeverityText;
41
+ body: {
42
+ stringValue: string;
43
+ };
44
+ attributes: KeyValue[];
45
+ traceId?: string;
46
+ spanId?: string;
47
+ flags?: number;
48
+ }
49
+ /** One finished span, as OTLP/JSON. */
50
+ export interface SpanRecord {
51
+ traceId: string;
52
+ spanId: string;
53
+ parentSpanId?: string;
54
+ name: string;
55
+ kind: number;
56
+ startTimeUnixNano: string;
57
+ endTimeUnixNano: string;
58
+ attributes: KeyValue[];
59
+ droppedAttributesCount?: number;
60
+ events: Array<{
61
+ timeUnixNano: string;
62
+ name: string;
63
+ attributes: KeyValue[];
64
+ }>;
65
+ links: Array<{
66
+ traceId: string;
67
+ spanId: string;
68
+ attributes: KeyValue[];
69
+ }>;
70
+ status: {
71
+ code: number;
72
+ message?: string;
73
+ };
74
+ /** Grouping key for scopeSpans; not on the wire. */
75
+ scope: Scope;
76
+ }
77
+ export interface Scope {
78
+ name: string;
79
+ version?: string;
80
+ }
81
+ /** The ExportLogsServiceRequest body for one batch. */
82
+ export declare function logsPayload(resource: KeyValue[], scope: Scope, records: LogRecord[]): {
83
+ resourceLogs: {
84
+ resource: {
85
+ attributes: KeyValue[];
86
+ };
87
+ scopeLogs: {
88
+ scope: Scope;
89
+ logRecords: LogRecord[];
90
+ }[];
91
+ }[];
92
+ };
93
+ /** The ExportTraceServiceRequest body for one batch, spans grouped by scope. */
94
+ export declare function tracesPayload(resource: KeyValue[], spans: SpanRecord[]): {
95
+ resourceSpans: {
96
+ resource: {
97
+ attributes: KeyValue[];
98
+ };
99
+ scopeSpans: {
100
+ scope: Scope;
101
+ spans: Omit<SpanRecord, "scope">[];
102
+ }[];
103
+ }[];
104
+ };
105
+ export type FetchLike = (url: string, init: {
106
+ method: string;
107
+ headers: Record<string, string>;
108
+ body: string;
109
+ signal?: AbortSignal;
110
+ }) => Promise<{
111
+ status: number;
112
+ headers: {
113
+ get(name: string): string | null;
114
+ };
115
+ }>;
116
+ export interface ExporterOptions {
117
+ /** The OTLP base: `${endpoint}/v1/logs`, `${endpoint}/v1/traces`. */
118
+ endpoint: string;
119
+ /** Sent on every request. The keyed ingest requires an allowlisted `Origin`. */
120
+ headers: Record<string, string>;
121
+ fetch: FetchLike;
122
+ /** One request's budget. */
123
+ timeoutMs: number;
124
+ /** Retries after the first attempt, on a network error, 429 or 5xx gateway answer. */
125
+ retries: number;
126
+ /** Told about a batch that was dropped; never about one that landed. */
127
+ onError?: (error: Error) => void;
128
+ /** Test seam for the backoff wait. */
129
+ sleep?: (ms: number) => Promise<void>;
130
+ }
131
+ /** Longest backoff between attempts, whatever Retry-After asks for. */
132
+ export declare const MAX_RETRY_DELAY_MS = 5000;
133
+ /**
134
+ * Posts one OTLP/JSON body to `${endpoint}/v1/<signal>`. Resolves true when
135
+ * the collector accepted it, false when it was dropped (after the retries);
136
+ * never rejects.
137
+ */
138
+ export declare function exportSignal(opts: ExporterOptions, signal: "logs" | "traces", body: unknown): Promise<boolean>;
139
+ export interface BatchOptions<T> {
140
+ /** Most items held; a push beyond it is dropped and counted. */
141
+ maxQueue: number;
142
+ /** Most items in one export. */
143
+ maxBatch: number;
144
+ /** How long an item may wait for a batch to fill. */
145
+ delayMs: number;
146
+ send(items: T[]): Promise<unknown>;
147
+ }
148
+ /**
149
+ * A bounded queue that exports in batches: when a batch fills, when the delay
150
+ * after the first queued item elapses, or on flush(). Its timer is unref'd, so
151
+ * a pending batch never keeps a process alive by itself.
152
+ */
153
+ export declare class BatchQueue<T> {
154
+ private readonly opts;
155
+ private items;
156
+ private timer;
157
+ private inflight;
158
+ /** Items refused because the queue was full. */
159
+ dropped: number;
160
+ constructor(opts: BatchOptions<T>);
161
+ get size(): number;
162
+ push(item: T): void;
163
+ /** Start exporting everything queued now, without waiting for it. */
164
+ drain(): void;
165
+ /** Export everything queued and wait for every export in flight. */
166
+ flush(): Promise<void>;
167
+ }
168
+ export {};