@flareapp/electron 0.1.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 +173 -0
- package/dist/main.cjs +486 -0
- package/dist/main.d.cts +77 -0
- package/dist/main.d.mts +77 -0
- package/dist/main.mjs +406 -0
- package/dist/preload.cjs +21 -0
- package/dist/preload.d.cts +8 -0
- package/dist/preload.d.mts +8 -0
- package/dist/preload.mjs +20 -0
- package/dist/renderer.cjs +84 -0
- package/dist/renderer.d.cts +30 -0
- package/dist/renderer.d.mts +30 -0
- package/dist/renderer.mjs +81 -0
- package/package.json +80 -0
package/dist/main.d.cts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AttributeValue, Attributes, Config, Config as Config$1, DEFAULT_URL_DENYLIST, Flare, GlobalScopeProvider, Glow, Logger, MessageLevel, NullFileReader, Report, Scope, SdkInfo, StackFrame, convertToError, redactUrlQuery, resolveDenylist } from "@flareapp/core";
|
|
2
|
+
import { App, IpcMain } from "electron";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
type ElectronFatalMode = 'off' | 'report' | 'report-and-exit';
|
|
6
|
+
type ElectronUser = {
|
|
7
|
+
id?: string | number;
|
|
8
|
+
email?: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
ipAddress?: string;
|
|
11
|
+
};
|
|
12
|
+
/** A frame the IPC receiver evaluates for trust. Mirrors Electron's WebFrameMain shape we use. */
|
|
13
|
+
type SenderFrame = {
|
|
14
|
+
url: string;
|
|
15
|
+
};
|
|
16
|
+
type ElectronOptions = {
|
|
17
|
+
uncaughtExceptionMode?: ElectronFatalMode;
|
|
18
|
+
unhandledRejectionMode?: ElectronFatalMode;
|
|
19
|
+
shutdownTimeoutMs?: number; /** Listen to app 'render-process-gone' / 'child-process-gone' and report them. */
|
|
20
|
+
captureRenderProcessGone?: boolean; /** Extra URL scheme names (no colon), e.g. 'app', trusted as report senders. */
|
|
21
|
+
trustedProtocols?: string[]; /** Full override of the sender-trust check. When set, replaces the default policy. */
|
|
22
|
+
trustSender?: (frame: SenderFrame) => boolean; /** Max serialized report size in bytes accepted over IPC. */
|
|
23
|
+
maxReportBytes?: number;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/main/ElectronFlare.d.ts
|
|
27
|
+
type AppLike = Pick<App, 'getName' | 'getVersion' | 'getLocale' | 'isReady' | 'exit' | 'on' | 'off'> & {
|
|
28
|
+
isPackaged: boolean;
|
|
29
|
+
};
|
|
30
|
+
type ElectronFlareDeps = {
|
|
31
|
+
app: AppLike;
|
|
32
|
+
ipcMain: IpcMain;
|
|
33
|
+
};
|
|
34
|
+
declare class ElectronFlare extends Flare {
|
|
35
|
+
private app;
|
|
36
|
+
private ipcMain;
|
|
37
|
+
private options;
|
|
38
|
+
private user;
|
|
39
|
+
private isLit;
|
|
40
|
+
private handlerManager;
|
|
41
|
+
private renderGoneHandler;
|
|
42
|
+
private childGoneHandler;
|
|
43
|
+
private forwardedInFlight;
|
|
44
|
+
private flushScheduler;
|
|
45
|
+
private mainStage;
|
|
46
|
+
private mainVersion;
|
|
47
|
+
private mainSourcemapVersionId;
|
|
48
|
+
constructor(deps: ElectronFlareDeps);
|
|
49
|
+
configure(config: Partial<Config$1>): this;
|
|
50
|
+
light(key?: string, debug?: boolean): this;
|
|
51
|
+
configureElectron(partial: ElectronOptions): this;
|
|
52
|
+
setUser(user: ElectronUser | null): void;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
/** Attach or detach the process-gone listeners to match options.captureRenderProcessGone. Idempotent. */
|
|
55
|
+
private reconcileCrashListeners;
|
|
56
|
+
private detachCrashListeners;
|
|
57
|
+
private reportProcessGone;
|
|
58
|
+
/**
|
|
59
|
+
* Wait for both core's tracked reports AND forwarded renderer reports (which bypass core's
|
|
60
|
+
* private track()), bounded by timeoutMs. The timeout is cleared when all reports settle
|
|
61
|
+
* first, so the event loop is not kept alive unnecessarily.
|
|
62
|
+
*/
|
|
63
|
+
flush(timeoutMs?: number): Promise<void>;
|
|
64
|
+
/** Overlay main-authoritative config + Electron metadata + user onto a forwarded report, then send. */
|
|
65
|
+
private receiveRendererReport;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/constants.d.ts
|
|
69
|
+
/** IPC channel renderer reports travel on. Shared by preload, renderer, and main. */
|
|
70
|
+
declare const FLARE_IPC_CHANNEL = "flare:report";
|
|
71
|
+
/** Global key the preload bridge exposes in the renderer's main world. */
|
|
72
|
+
declare const FLARE_BRIDGE_KEY = "__flare";
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/main.d.ts
|
|
75
|
+
declare const flare: ElectronFlare;
|
|
76
|
+
//#endregion
|
|
77
|
+
export { type AttributeValue, type Attributes, type Config, DEFAULT_URL_DENYLIST, type ElectronFatalMode, ElectronFlare, type ElectronOptions, type ElectronUser, FLARE_BRIDGE_KEY, FLARE_IPC_CHANNEL, GlobalScopeProvider, type Glow, Logger, type MessageLevel, NullFileReader, type Report, Scope, type SdkInfo, type SenderFrame, type StackFrame, convertToError, flare, redactUrlQuery, resolveDenylist };
|
package/dist/main.d.mts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { App, IpcMain } from "electron";
|
|
2
|
+
import { AttributeValue, Attributes, Config, Config as Config$1, DEFAULT_URL_DENYLIST, Flare, GlobalScopeProvider, Glow, Logger, MessageLevel, NullFileReader, Report, Scope, SdkInfo, StackFrame, convertToError, redactUrlQuery, resolveDenylist } from "@flareapp/core";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
type ElectronFatalMode = 'off' | 'report' | 'report-and-exit';
|
|
6
|
+
type ElectronUser = {
|
|
7
|
+
id?: string | number;
|
|
8
|
+
email?: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
ipAddress?: string;
|
|
11
|
+
};
|
|
12
|
+
/** A frame the IPC receiver evaluates for trust. Mirrors Electron's WebFrameMain shape we use. */
|
|
13
|
+
type SenderFrame = {
|
|
14
|
+
url: string;
|
|
15
|
+
};
|
|
16
|
+
type ElectronOptions = {
|
|
17
|
+
uncaughtExceptionMode?: ElectronFatalMode;
|
|
18
|
+
unhandledRejectionMode?: ElectronFatalMode;
|
|
19
|
+
shutdownTimeoutMs?: number; /** Listen to app 'render-process-gone' / 'child-process-gone' and report them. */
|
|
20
|
+
captureRenderProcessGone?: boolean; /** Extra URL scheme names (no colon), e.g. 'app', trusted as report senders. */
|
|
21
|
+
trustedProtocols?: string[]; /** Full override of the sender-trust check. When set, replaces the default policy. */
|
|
22
|
+
trustSender?: (frame: SenderFrame) => boolean; /** Max serialized report size in bytes accepted over IPC. */
|
|
23
|
+
maxReportBytes?: number;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/main/ElectronFlare.d.ts
|
|
27
|
+
type AppLike = Pick<App, 'getName' | 'getVersion' | 'getLocale' | 'isReady' | 'exit' | 'on' | 'off'> & {
|
|
28
|
+
isPackaged: boolean;
|
|
29
|
+
};
|
|
30
|
+
type ElectronFlareDeps = {
|
|
31
|
+
app: AppLike;
|
|
32
|
+
ipcMain: IpcMain;
|
|
33
|
+
};
|
|
34
|
+
declare class ElectronFlare extends Flare {
|
|
35
|
+
private app;
|
|
36
|
+
private ipcMain;
|
|
37
|
+
private options;
|
|
38
|
+
private user;
|
|
39
|
+
private isLit;
|
|
40
|
+
private handlerManager;
|
|
41
|
+
private renderGoneHandler;
|
|
42
|
+
private childGoneHandler;
|
|
43
|
+
private forwardedInFlight;
|
|
44
|
+
private flushScheduler;
|
|
45
|
+
private mainStage;
|
|
46
|
+
private mainVersion;
|
|
47
|
+
private mainSourcemapVersionId;
|
|
48
|
+
constructor(deps: ElectronFlareDeps);
|
|
49
|
+
configure(config: Partial<Config$1>): this;
|
|
50
|
+
light(key?: string, debug?: boolean): this;
|
|
51
|
+
configureElectron(partial: ElectronOptions): this;
|
|
52
|
+
setUser(user: ElectronUser | null): void;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
/** Attach or detach the process-gone listeners to match options.captureRenderProcessGone. Idempotent. */
|
|
55
|
+
private reconcileCrashListeners;
|
|
56
|
+
private detachCrashListeners;
|
|
57
|
+
private reportProcessGone;
|
|
58
|
+
/**
|
|
59
|
+
* Wait for both core's tracked reports AND forwarded renderer reports (which bypass core's
|
|
60
|
+
* private track()), bounded by timeoutMs. The timeout is cleared when all reports settle
|
|
61
|
+
* first, so the event loop is not kept alive unnecessarily.
|
|
62
|
+
*/
|
|
63
|
+
flush(timeoutMs?: number): Promise<void>;
|
|
64
|
+
/** Overlay main-authoritative config + Electron metadata + user onto a forwarded report, then send. */
|
|
65
|
+
private receiveRendererReport;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/constants.d.ts
|
|
69
|
+
/** IPC channel renderer reports travel on. Shared by preload, renderer, and main. */
|
|
70
|
+
declare const FLARE_IPC_CHANNEL = "flare:report";
|
|
71
|
+
/** Global key the preload bridge exposes in the renderer's main world. */
|
|
72
|
+
declare const FLARE_BRIDGE_KEY = "__flare";
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/main.d.ts
|
|
75
|
+
declare const flare: ElectronFlare;
|
|
76
|
+
//#endregion
|
|
77
|
+
export { type AttributeValue, type Attributes, type Config, DEFAULT_URL_DENYLIST, type ElectronFatalMode, ElectronFlare, type ElectronOptions, type ElectronUser, FLARE_BRIDGE_KEY, FLARE_IPC_CHANNEL, GlobalScopeProvider, type Glow, Logger, type MessageLevel, NullFileReader, type Report, Scope, type SdkInfo, type SenderFrame, type StackFrame, convertToError, flare, redactUrlQuery, resolveDenylist };
|
package/dist/main.mjs
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { app, ipcMain } from "electron";
|
|
2
|
+
import { Api, DEFAULT_URL_DENYLIST, Flare, GlobalScopeProvider, GlobalScopeProvider as GlobalScopeProvider$1, Logger, NullFileReader, Scope, convertToError, redactUrlQuery, resolveDenylist } from "@flareapp/core";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
//#region src/env.ts
|
|
8
|
+
const CLIENT_VERSION = typeof process !== "undefined" && true ? "0.1.0" : "?";
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/types.ts
|
|
12
|
+
const DEFAULT_ELECTRON_OPTIONS = {
|
|
13
|
+
uncaughtExceptionMode: "report-and-exit",
|
|
14
|
+
unhandledRejectionMode: "report-and-exit",
|
|
15
|
+
shutdownTimeoutMs: 2e3,
|
|
16
|
+
captureRenderProcessGone: true,
|
|
17
|
+
trustedProtocols: [],
|
|
18
|
+
trustSender: null,
|
|
19
|
+
maxReportBytes: 1e6
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/main/collectElectron.ts
|
|
24
|
+
/**
|
|
25
|
+
* App + runtime attributes that are safe to overlay onto ANY report regardless of origin.
|
|
26
|
+
* Reused by both the collector (main errors) and the IPC receiver (forwarded renderer reports).
|
|
27
|
+
*
|
|
28
|
+
* Intentionally does NOT include per-process fields like `flare.entry_point.type` or `process.type`
|
|
29
|
+
* so that forwarded renderer reports keep their own `flare.entry_point.type: 'web'` intact.
|
|
30
|
+
*/
|
|
31
|
+
function collectElectronAppAttributes(app) {
|
|
32
|
+
const versions = process.versions;
|
|
33
|
+
const attrs = {
|
|
34
|
+
"service.name": app.getName(),
|
|
35
|
+
"app.version": app.getVersion(),
|
|
36
|
+
"app.packaged": app.isPackaged,
|
|
37
|
+
"process.runtime.name": "electron",
|
|
38
|
+
"process.runtime.version": versions.electron ?? "",
|
|
39
|
+
"process.versions.electron": versions.electron ?? "",
|
|
40
|
+
"process.versions.chrome": versions.chrome ?? "",
|
|
41
|
+
"process.versions.node": versions.node ?? process.version,
|
|
42
|
+
"host.arch": process.arch,
|
|
43
|
+
"os.type": os.type()
|
|
44
|
+
};
|
|
45
|
+
if (app.isReady()) try {
|
|
46
|
+
attrs["app.locale"] = app.getLocale();
|
|
47
|
+
} catch {}
|
|
48
|
+
return attrs;
|
|
49
|
+
}
|
|
50
|
+
/** Project a user into OTel enduser.* / client.address keys. */
|
|
51
|
+
function projectUser(user) {
|
|
52
|
+
const attrs = {};
|
|
53
|
+
if (!user) return attrs;
|
|
54
|
+
if (user.id !== void 0) attrs["enduser.id"] = String(user.id);
|
|
55
|
+
if (user.email !== void 0) attrs["enduser.email"] = user.email;
|
|
56
|
+
if (user.username !== void 0) attrs["enduser.username"] = user.username;
|
|
57
|
+
if (user.ipAddress !== void 0) attrs["client.address"] = user.ipAddress;
|
|
58
|
+
return attrs;
|
|
59
|
+
}
|
|
60
|
+
/** Build the ContextCollector core calls on every main-process report. */
|
|
61
|
+
function makeElectronContextCollector(app, getUser) {
|
|
62
|
+
return (_config) => ({
|
|
63
|
+
"flare.entry_point.type": "server",
|
|
64
|
+
"process.type": process.type ?? "browser",
|
|
65
|
+
...collectElectronAppAttributes(app),
|
|
66
|
+
...projectUser(getUser())
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/main/ElectronDiskFileReader.ts
|
|
72
|
+
/** Reads source snippets for main-process stack frames from disk. Mirrors @flareapp/node's DiskFileReader. */
|
|
73
|
+
var ElectronDiskFileReader = class {
|
|
74
|
+
async read(url) {
|
|
75
|
+
if (!isLocalFileUrl(url)) return null;
|
|
76
|
+
try {
|
|
77
|
+
return await readFile(/^file:\/\//i.test(url) ? fileURLToPath(url) : url, "utf-8");
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function isLocalFileUrl(url) {
|
|
84
|
+
return /^file:\/\//i.test(url) || url.startsWith("/") || /^[a-z]:[\\/]/i.test(url) || url.startsWith("\\\\");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/main/ElectronFlushScheduler.ts
|
|
89
|
+
/** Flushes pending logs/reports when the Electron app is quitting. App is injectable for tests. */
|
|
90
|
+
var ElectronFlushScheduler = class {
|
|
91
|
+
listener = null;
|
|
92
|
+
constructor(app) {
|
|
93
|
+
this.app = app;
|
|
94
|
+
}
|
|
95
|
+
register(flush) {
|
|
96
|
+
if (this.listener) return;
|
|
97
|
+
this.listener = () => {
|
|
98
|
+
flush();
|
|
99
|
+
};
|
|
100
|
+
this.app.on("before-quit", this.listener);
|
|
101
|
+
}
|
|
102
|
+
/** Detach the before-quit listener so a disposed ElectronFlare leaves no flush handler on the shared app. */
|
|
103
|
+
dispose() {
|
|
104
|
+
if (this.listener) {
|
|
105
|
+
this.app.off("before-quit", this.listener);
|
|
106
|
+
this.listener = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/constants.ts
|
|
113
|
+
/** IPC channel renderer reports travel on. Shared by preload, renderer, and main. */
|
|
114
|
+
const FLARE_IPC_CHANNEL = "flare:report";
|
|
115
|
+
/** Global key the preload bridge exposes in the renderer's main world. */
|
|
116
|
+
const FLARE_BRIDGE_KEY = "__flare";
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/main/ipcReceiver.ts
|
|
120
|
+
/** Module-level ownership token for the single flare:report channel. */
|
|
121
|
+
let currentOwner = null;
|
|
122
|
+
/** Default sender-trust check: accept file: and localhost/127.0.0.1 only, plus configured custom protocols. */
|
|
123
|
+
function defaultTrustPolicy(frame, opts) {
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = new URL(frame.url);
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const scheme = parsed.protocol.replace(/:$/, "");
|
|
131
|
+
if (scheme === "file") return parsed.hostname === "" || parsed.hostname === "localhost";
|
|
132
|
+
const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
|
|
133
|
+
if ((scheme === "http" || scheme === "https") && isLoopback) return true;
|
|
134
|
+
if (opts.trustedProtocols.includes(scheme)) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
function isTrusted(frame, opts) {
|
|
138
|
+
if (!frame || typeof frame.url !== "string") return false;
|
|
139
|
+
if (opts.trustSender) return opts.trustSender(frame);
|
|
140
|
+
return defaultTrustPolicy(frame, opts);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Minimal top-level structural guard for a parsed report. This is intentionally NOT an exhaustive
|
|
144
|
+
* validator of every StackFrame / SpanEvent: the renderer builds the report with our own Flare code,
|
|
145
|
+
* and a compromised renderer could forge any valid-looking shape anyway. The real security boundary
|
|
146
|
+
* is the sender-trust check plus the byte-size cap; this guard only rejects obviously-wrong payloads.
|
|
147
|
+
*/
|
|
148
|
+
function isReportShape(value) {
|
|
149
|
+
if (typeof value !== "object" || value === null) return false;
|
|
150
|
+
const r = value;
|
|
151
|
+
return typeof r.seenAtUnixNano === "number" && Array.isArray(r.stacktrace) && Array.isArray(r.events) && typeof r.attributes === "object" && r.attributes !== null && !Array.isArray(r.attributes);
|
|
152
|
+
}
|
|
153
|
+
function registerIpcReceiver(ipcMain, owner, deps) {
|
|
154
|
+
if (currentOwner === owner) return;
|
|
155
|
+
if (currentOwner !== null) ipcMain.removeHandler(FLARE_IPC_CHANNEL);
|
|
156
|
+
currentOwner = owner;
|
|
157
|
+
ipcMain.handle(FLARE_IPC_CHANNEL, (async (event, payload) => {
|
|
158
|
+
const opts = deps.getOptions();
|
|
159
|
+
if (!isTrusted(event.senderFrame, opts)) return;
|
|
160
|
+
if (typeof payload !== "string") return;
|
|
161
|
+
if (Buffer.byteLength(payload, "utf8") > opts.maxReportBytes) return;
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(payload);
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!isReportShape(parsed)) return;
|
|
169
|
+
await deps.onReport(parsed);
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
function disposeIpcReceiver(ipcMain, owner) {
|
|
173
|
+
if (currentOwner !== owner) return;
|
|
174
|
+
ipcMain.removeHandler(FLARE_IPC_CHANNEL);
|
|
175
|
+
currentOwner = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/main/processHandlers.ts
|
|
180
|
+
/**
|
|
181
|
+
* Fatal callbacks for Electron main. Mirrors @flareapp/node's buildFatalCallbacks but exits via
|
|
182
|
+
* the injected `exit` (defaulting to app.exit at the call site), which is immediate and skips
|
|
183
|
+
* before-quit/will-quit, correct after an uncaught exception.
|
|
184
|
+
*/
|
|
185
|
+
function buildFatalCallbacks(flare, getOpts, exit) {
|
|
186
|
+
return {
|
|
187
|
+
async onUncaught(err, origin) {
|
|
188
|
+
const opts = getOpts();
|
|
189
|
+
if (opts.uncaughtExceptionMode === "report-and-exit") process.exitCode = 1;
|
|
190
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
191
|
+
try {
|
|
192
|
+
await flare.report(error, { "process.uncaught_exception.origin": origin });
|
|
193
|
+
} catch {}
|
|
194
|
+
if (opts.uncaughtExceptionMode === "report-and-exit") {
|
|
195
|
+
await flare.flush(opts.shutdownTimeoutMs);
|
|
196
|
+
exit(1);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
async onRejection(reason) {
|
|
200
|
+
const opts = getOpts();
|
|
201
|
+
if (opts.unhandledRejectionMode === "report-and-exit") process.exitCode = 1;
|
|
202
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
203
|
+
try {
|
|
204
|
+
await flare.report(error);
|
|
205
|
+
} catch {}
|
|
206
|
+
if (opts.unhandledRejectionMode === "report-and-exit") {
|
|
207
|
+
await flare.flush(opts.shutdownTimeoutMs);
|
|
208
|
+
exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Owns the lifecycle of the two process-level error listeners for the Electron main process.
|
|
215
|
+
* Mirrors node's ProcessHandlerManager. Attach/detach the two process listeners, reconciling
|
|
216
|
+
* against the desired modes.
|
|
217
|
+
*/
|
|
218
|
+
var ProcessHandlerManager = class {
|
|
219
|
+
uncaughtHandler = null;
|
|
220
|
+
rejectionHandler = null;
|
|
221
|
+
constructor(cbs) {
|
|
222
|
+
this.cbs = cbs;
|
|
223
|
+
}
|
|
224
|
+
reconcile(opts) {
|
|
225
|
+
this.reconcileOne("uncaughtException", opts.uncaughtExceptionMode, () => this.uncaughtHandler, (h) => {
|
|
226
|
+
this.uncaughtHandler = h;
|
|
227
|
+
}, (err, origin) => this.cbs.onUncaught(err, origin));
|
|
228
|
+
this.reconcileOne("unhandledRejection", opts.unhandledRejectionMode, () => this.rejectionHandler, (h) => {
|
|
229
|
+
this.rejectionHandler = h;
|
|
230
|
+
}, (reason) => this.cbs.onRejection(reason));
|
|
231
|
+
}
|
|
232
|
+
detach() {
|
|
233
|
+
if (this.uncaughtHandler) {
|
|
234
|
+
process.off("uncaughtException", this.uncaughtHandler);
|
|
235
|
+
this.uncaughtHandler = null;
|
|
236
|
+
}
|
|
237
|
+
if (this.rejectionHandler) {
|
|
238
|
+
process.off("unhandledRejection", this.rejectionHandler);
|
|
239
|
+
this.rejectionHandler = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
reconcileOne(event, mode, get, set, impl) {
|
|
243
|
+
const current = get();
|
|
244
|
+
const wants = mode !== "off";
|
|
245
|
+
if (wants && !current) {
|
|
246
|
+
set(impl);
|
|
247
|
+
process.on(event, impl);
|
|
248
|
+
} else if (!wants && current) {
|
|
249
|
+
process.off(event, current);
|
|
250
|
+
set(null);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/main/ElectronFlare.ts
|
|
257
|
+
const SDK_NAME = "@flareapp/electron";
|
|
258
|
+
/** Write `value` under `key` when non-empty, otherwise remove any renderer-supplied value. */
|
|
259
|
+
function overlayOrDelete(attributes, key, value) {
|
|
260
|
+
if (value) attributes[key] = value;
|
|
261
|
+
else delete attributes[key];
|
|
262
|
+
}
|
|
263
|
+
var ElectronFlare = class extends Flare {
|
|
264
|
+
app;
|
|
265
|
+
ipcMain;
|
|
266
|
+
options = { ...DEFAULT_ELECTRON_OPTIONS };
|
|
267
|
+
user = null;
|
|
268
|
+
isLit = false;
|
|
269
|
+
handlerManager;
|
|
270
|
+
renderGoneHandler = null;
|
|
271
|
+
childGoneHandler = null;
|
|
272
|
+
forwardedInFlight = /* @__PURE__ */ new Set();
|
|
273
|
+
flushScheduler;
|
|
274
|
+
mainStage = "";
|
|
275
|
+
mainVersion = "";
|
|
276
|
+
mainSourcemapVersionId = "";
|
|
277
|
+
constructor(deps) {
|
|
278
|
+
const app = deps.app;
|
|
279
|
+
const collector = makeElectronContextCollector(app, () => this.user);
|
|
280
|
+
const flushScheduler = new ElectronFlushScheduler(app);
|
|
281
|
+
super(new Api(), collector, new ElectronDiskFileReader(), new GlobalScopeProvider$1(), flushScheduler);
|
|
282
|
+
this.app = app;
|
|
283
|
+
this.ipcMain = deps.ipcMain;
|
|
284
|
+
this.flushScheduler = flushScheduler;
|
|
285
|
+
this.setSdkInfo({
|
|
286
|
+
name: SDK_NAME,
|
|
287
|
+
version: CLIENT_VERSION
|
|
288
|
+
});
|
|
289
|
+
this.handlerManager = new ProcessHandlerManager(buildFatalCallbacks(this, () => this.options, (code) => this.app.exit(code)));
|
|
290
|
+
registerIpcReceiver(this.ipcMain, this, {
|
|
291
|
+
getOptions: () => this.options,
|
|
292
|
+
onReport: (report) => this.receiveRendererReport(report)
|
|
293
|
+
});
|
|
294
|
+
this.reconcileCrashListeners();
|
|
295
|
+
}
|
|
296
|
+
configure(config) {
|
|
297
|
+
if (config.stage !== void 0) this.mainStage = config.stage;
|
|
298
|
+
if (config.version !== void 0) this.mainVersion = config.version;
|
|
299
|
+
if (config.sourcemapVersionId !== void 0) this.mainSourcemapVersionId = config.sourcemapVersionId;
|
|
300
|
+
return super.configure(config);
|
|
301
|
+
}
|
|
302
|
+
light(key, debug) {
|
|
303
|
+
super.light(key, debug);
|
|
304
|
+
this.isLit = true;
|
|
305
|
+
this.handlerManager.reconcile(this.options);
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
configureElectron(partial) {
|
|
309
|
+
if (partial.uncaughtExceptionMode !== void 0) this.options.uncaughtExceptionMode = partial.uncaughtExceptionMode;
|
|
310
|
+
if (partial.unhandledRejectionMode !== void 0) this.options.unhandledRejectionMode = partial.unhandledRejectionMode;
|
|
311
|
+
if (partial.shutdownTimeoutMs !== void 0) this.options.shutdownTimeoutMs = partial.shutdownTimeoutMs;
|
|
312
|
+
if (partial.captureRenderProcessGone !== void 0) this.options.captureRenderProcessGone = partial.captureRenderProcessGone;
|
|
313
|
+
if (partial.trustedProtocols !== void 0) this.options.trustedProtocols = Array.isArray(partial.trustedProtocols) ? partial.trustedProtocols : [];
|
|
314
|
+
if (partial.trustSender !== void 0) this.options.trustSender = partial.trustSender;
|
|
315
|
+
if (partial.maxReportBytes !== void 0 && Number.isFinite(partial.maxReportBytes)) this.options.maxReportBytes = partial.maxReportBytes;
|
|
316
|
+
if (this.isLit) this.handlerManager.reconcile(this.options);
|
|
317
|
+
this.reconcileCrashListeners();
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
setUser(user) {
|
|
321
|
+
this.user = user;
|
|
322
|
+
}
|
|
323
|
+
dispose() {
|
|
324
|
+
this.handlerManager.detach();
|
|
325
|
+
this.detachCrashListeners();
|
|
326
|
+
disposeIpcReceiver(this.ipcMain, this);
|
|
327
|
+
this.flushScheduler.dispose();
|
|
328
|
+
}
|
|
329
|
+
/** Attach or detach the process-gone listeners to match options.captureRenderProcessGone. Idempotent. */
|
|
330
|
+
reconcileCrashListeners() {
|
|
331
|
+
const want = this.options.captureRenderProcessGone;
|
|
332
|
+
const attached = this.renderGoneHandler !== null;
|
|
333
|
+
if (want && !attached) {
|
|
334
|
+
this.renderGoneHandler = (_event, webContents, details) => {
|
|
335
|
+
return this.reportProcessGone("renderer", details, webContents?.id);
|
|
336
|
+
};
|
|
337
|
+
this.childGoneHandler = (_event, details) => {
|
|
338
|
+
return this.reportProcessGone("child", details);
|
|
339
|
+
};
|
|
340
|
+
this.app.on("render-process-gone", this.renderGoneHandler);
|
|
341
|
+
this.app.on("child-process-gone", this.childGoneHandler);
|
|
342
|
+
} else if (!want && attached) this.detachCrashListeners();
|
|
343
|
+
}
|
|
344
|
+
detachCrashListeners() {
|
|
345
|
+
if (this.renderGoneHandler) {
|
|
346
|
+
this.app.off("render-process-gone", this.renderGoneHandler);
|
|
347
|
+
this.renderGoneHandler = null;
|
|
348
|
+
}
|
|
349
|
+
if (this.childGoneHandler) {
|
|
350
|
+
this.app.off("child-process-gone", this.childGoneHandler);
|
|
351
|
+
this.childGoneHandler = null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
reportProcessGone(kind, details, webContentsId) {
|
|
355
|
+
const reason = details.reason ?? "unknown";
|
|
356
|
+
const label = kind === "renderer" ? "Renderer process gone" : "Child process gone";
|
|
357
|
+
const error = /* @__PURE__ */ new Error(`${label}: ${reason}`);
|
|
358
|
+
const attrs = {
|
|
359
|
+
"electron.process_gone.kind": kind,
|
|
360
|
+
"electron.process_gone.reason": reason
|
|
361
|
+
};
|
|
362
|
+
if (details.exitCode !== void 0) attrs["electron.process_gone.exit_code"] = details.exitCode;
|
|
363
|
+
if (details.type !== void 0) attrs["electron.process_gone.type"] = details.type;
|
|
364
|
+
if (details.serviceName !== void 0) attrs["electron.process_gone.service_name"] = details.serviceName;
|
|
365
|
+
if (webContentsId !== void 0) attrs["electron.process_gone.web_contents_id"] = webContentsId;
|
|
366
|
+
return this.report(error, attrs);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Wait for both core's tracked reports AND forwarded renderer reports (which bypass core's
|
|
370
|
+
* private track()), bounded by timeoutMs. The timeout is cleared when all reports settle
|
|
371
|
+
* first, so the event loop is not kept alive unnecessarily.
|
|
372
|
+
*/
|
|
373
|
+
flush(timeoutMs = 2e3) {
|
|
374
|
+
const settled = Promise.allSettled([super.flush(timeoutMs), ...this.forwardedInFlight]);
|
|
375
|
+
return new Promise((resolve) => {
|
|
376
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
377
|
+
settled.then(() => {
|
|
378
|
+
clearTimeout(timer);
|
|
379
|
+
resolve();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/** Overlay main-authoritative config + Electron metadata + user onto a forwarded report, then send. */
|
|
384
|
+
receiveRendererReport(report) {
|
|
385
|
+
Object.assign(report.attributes, collectElectronAppAttributes(this.app), projectUser(this.user));
|
|
386
|
+
overlayOrDelete(report.attributes, "service.stage", this.mainStage);
|
|
387
|
+
overlayOrDelete(report.attributes, "service.version", this.mainVersion);
|
|
388
|
+
if (this.mainSourcemapVersionId) report.sourcemapVersionId = this.mainSourcemapVersionId;
|
|
389
|
+
else delete report.sourcemapVersionId;
|
|
390
|
+
const sent = this.sendReport(report).finally(() => {
|
|
391
|
+
this.forwardedInFlight.delete(sent);
|
|
392
|
+
});
|
|
393
|
+
this.forwardedInFlight.add(sent);
|
|
394
|
+
return sent;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/main.ts
|
|
400
|
+
const flare = new ElectronFlare({
|
|
401
|
+
app,
|
|
402
|
+
ipcMain
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
//#endregion
|
|
406
|
+
export { DEFAULT_URL_DENYLIST, ElectronFlare, FLARE_BRIDGE_KEY, FLARE_IPC_CHANNEL, GlobalScopeProvider, Logger, NullFileReader, Scope, convertToError, flare, redactUrlQuery, resolveDenylist };
|
package/dist/preload.cjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let electron = require("electron");
|
|
3
|
+
|
|
4
|
+
//#region src/constants.ts
|
|
5
|
+
/** IPC channel renderer reports travel on. Shared by preload, renderer, and main. */
|
|
6
|
+
const FLARE_IPC_CHANNEL = "flare:report";
|
|
7
|
+
/** Global key the preload bridge exposes in the renderer's main world. */
|
|
8
|
+
const FLARE_BRIDGE_KEY = "__flare";
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/preload.ts
|
|
12
|
+
/**
|
|
13
|
+
* Call once in your preload script. Bridges renderer error reports to the main process over a
|
|
14
|
+
* single contextBridge method. Respects contextIsolation; does not require nodeIntegration.
|
|
15
|
+
*/
|
|
16
|
+
function exposeFlare() {
|
|
17
|
+
electron.contextBridge.exposeInMainWorld(FLARE_BRIDGE_KEY, { report: (payload) => electron.ipcRenderer.invoke(FLARE_IPC_CHANNEL, payload) });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
exports.exposeFlare = exposeFlare;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/preload.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Call once in your preload script. Bridges renderer error reports to the main process over a
|
|
4
|
+
* single contextBridge method. Respects contextIsolation; does not require nodeIntegration.
|
|
5
|
+
*/
|
|
6
|
+
declare function exposeFlare(): void;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { exposeFlare };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/preload.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Call once in your preload script. Bridges renderer error reports to the main process over a
|
|
4
|
+
* single contextBridge method. Respects contextIsolation; does not require nodeIntegration.
|
|
5
|
+
*/
|
|
6
|
+
declare function exposeFlare(): void;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { exposeFlare };
|
package/dist/preload.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from "electron";
|
|
2
|
+
|
|
3
|
+
//#region src/constants.ts
|
|
4
|
+
/** IPC channel renderer reports travel on. Shared by preload, renderer, and main. */
|
|
5
|
+
const FLARE_IPC_CHANNEL = "flare:report";
|
|
6
|
+
/** Global key the preload bridge exposes in the renderer's main world. */
|
|
7
|
+
const FLARE_BRIDGE_KEY = "__flare";
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/preload.ts
|
|
11
|
+
/**
|
|
12
|
+
* Call once in your preload script. Bridges renderer error reports to the main process over a
|
|
13
|
+
* single contextBridge method. Respects contextIsolation; does not require nodeIntegration.
|
|
14
|
+
*/
|
|
15
|
+
function exposeFlare() {
|
|
16
|
+
contextBridge.exposeInMainWorld(FLARE_BRIDGE_KEY, { report: (payload) => ipcRenderer.invoke(FLARE_IPC_CHANNEL, payload) });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { exposeFlare };
|