@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 +64 -0
- package/dist/node-exception.d.ts +73 -0
- package/dist/node-exception.js +278 -0
- package/dist/node-exception.js.map +1 -0
- package/dist/node-otlp.d.ts +168 -0
- package/dist/node-otlp.js +235 -0
- package/dist/node-otlp.js.map +1 -0
- package/dist/node-trace.d.ts +41 -0
- package/dist/node-trace.js +331 -0
- package/dist/node-trace.js.map +1 -0
- package/dist/node.d.ts +153 -0
- package/dist/node.js +576 -0
- package/dist/node.js.map +1 -0
- package/dist/service-hooks.d.ts +88 -4
- package/dist/service-hooks.js +60 -2
- package/dist/service-hooks.js.map +1 -1
- package/dist/service-name.d.ts +2 -0
- package/dist/service-name.js +14 -0
- package/dist/service-name.js.map +1 -0
- package/package.json +5 -1
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 {};
|