@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.
@@ -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 };
@@ -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 };
@@ -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 };
@@ -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 };