@tillstack/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 ADDED
@@ -0,0 +1,48 @@
1
+ # @tillstack/electron
2
+
3
+ TillPulse observability for Electron desktop apps. Covers the surfaces a web
4
+ error tracker can't reach: main-process crashes, native C++/V8 minidumps,
5
+ renderer/GPU/child-process failures, and app hangs.
6
+
7
+ ```bash
8
+ npm install @tillstack/electron
9
+ ```
10
+
11
+ In your **main process**:
12
+
13
+ ```ts
14
+ import { TillPulse } from '@tillstack/electron'
15
+
16
+ TillPulse.init({
17
+ dsn: 'https://<public-key>@ingest.tilldev.dev/<project-id>',
18
+ release: app.getVersion(),
19
+ environment: 'production',
20
+ })
21
+ ```
22
+
23
+ In each **renderer**, use [`@tillstack/browser`](../sdk-browser) — the renderer
24
+ is a Chromium page, so it gets the full web treatment (errors, Web Vitals,
25
+ network, breadcrumbs) with the same DSN.
26
+
27
+ The main-process SDK captures:
28
+
29
+ | Signal | Source |
30
+ |---|---|
31
+ | Main-process crashes | `uncaughtException`, `unhandledRejection` (stack-parsed) |
32
+ | Native crashes | Electron `crashReporter` minidumps (C++/V8 faults the JS layer can't catch) |
33
+ | Renderer/GPU/child gone | `render-process-gone`, `child-process-gone` (reason → crashed/oom) |
34
+ | App hangs | renderer `unresponsive`/`responsive` → duration-tagged hang events |
35
+ | Crash-free sessions | `session` on ready, `crashed` on an uncaught exception, `exited` on quit |
36
+ | Desktop context | OS + arch + RAM, Electron/Chrome/Node/V8 versions |
37
+
38
+ Events are **PII-scrubbed on-device** and buffered in a **file-backed queue** in
39
+ `userData` — so the report explaining a crash survives the restart after it.
40
+ Native minidump upload is opt-in (`uploadMinidumps: true`); by default dumps are
41
+ kept locally and a metadata event is sent.
42
+
43
+ ## Options
44
+
45
+ `dsn`, `release` required. Toggles (default on): `enableNativeCrashReporter`,
46
+ `enableProcessMonitoring`, `enableHangDetection`, `enableSessionTracking`. Also
47
+ `uploadMinidumps`, `environment`, `dist`, `disableDefaultPiiScrubbing`,
48
+ `beforeSend`, `initialTags`, `flushIntervalMs`, `debug`.
@@ -0,0 +1,77 @@
1
+ import { TillPulseEvent, Severity, BreadcrumbType, Breadcrumb } from '@tillstack/types';
2
+ export { TillPulseEvent } from '@tillstack/types';
3
+
4
+ interface TillPulseElectronOptions {
5
+ dsn: string;
6
+ release: string;
7
+ environment?: string;
8
+ dist?: string;
9
+ /** Start Electron's native crashReporter (writes minidumps for C++/V8 crashes the JS layer can't catch). Default true. */
10
+ enableNativeCrashReporter?: boolean;
11
+ /** Upload native minidumps to `<dsn-host>/api/v1/minidump`. Default false (dumps kept locally + a metadata event is sent). */
12
+ uploadMinidumps?: boolean;
13
+ /** Capture renderer/GPU/child-process-gone as crashes. Default true. */
14
+ enableProcessMonitoring?: boolean;
15
+ /** Treat renderer `unresponsive` as an app-hang event. Default true. */
16
+ enableHangDetection?: boolean;
17
+ /** Emit session events for crash-free sessions. Default true. */
18
+ enableSessionTracking?: boolean;
19
+ /** Opt in to TillShield desktop threat detection (DevTools-open on renderers). Off by default. */
20
+ enableThreatDetection?: boolean;
21
+ disableDefaultPiiScrubbing?: boolean;
22
+ beforeSend?: (e: TillPulseEvent) => TillPulseEvent | null | Promise<TillPulseEvent | null>;
23
+ initialTags?: Record<string, string>;
24
+ flushIntervalMs?: number;
25
+ debug?: boolean;
26
+ }
27
+ declare class TillPulseElectronSDK {
28
+ private opts;
29
+ private queue;
30
+ private endpoint;
31
+ private sessionId;
32
+ private timer;
33
+ private initialized;
34
+ private crumbs;
35
+ private tags;
36
+ private user;
37
+ private sessionStartedAt;
38
+ private sessionEnded;
39
+ init(options: TillPulseElectronOptions): void;
40
+ captureException(error: unknown): void;
41
+ captureMessage(message: string, level?: Severity): void;
42
+ captureSecurityEvent(input: {
43
+ securityType: string;
44
+ severity?: Severity;
45
+ confidence?: number;
46
+ evidence?: Record<string, unknown>;
47
+ message?: string;
48
+ }): void;
49
+ addBreadcrumb(input: {
50
+ type: BreadcrumbType;
51
+ category: string;
52
+ message: string;
53
+ level?: Breadcrumb['level'];
54
+ data?: Record<string, unknown>;
55
+ }): void;
56
+ setUser(user: {
57
+ id?: string;
58
+ segment?: string;
59
+ } | null): void;
60
+ setTag(key: string, value: string): void;
61
+ getQueueStats(): {
62
+ size: number;
63
+ };
64
+ flush(useKeepalive?: boolean): Promise<boolean>;
65
+ close(): void;
66
+ private _device;
67
+ private _base;
68
+ private _captureError;
69
+ private _captureSynthetic;
70
+ private _startSession;
71
+ private _markSession;
72
+ private _emitSession;
73
+ private _enqueue;
74
+ }
75
+ declare const TillPulse: TillPulseElectronSDK;
76
+
77
+ export { TillPulse, type TillPulseElectronOptions, TillPulse as default };
@@ -0,0 +1,77 @@
1
+ import { TillPulseEvent, Severity, BreadcrumbType, Breadcrumb } from '@tillstack/types';
2
+ export { TillPulseEvent } from '@tillstack/types';
3
+
4
+ interface TillPulseElectronOptions {
5
+ dsn: string;
6
+ release: string;
7
+ environment?: string;
8
+ dist?: string;
9
+ /** Start Electron's native crashReporter (writes minidumps for C++/V8 crashes the JS layer can't catch). Default true. */
10
+ enableNativeCrashReporter?: boolean;
11
+ /** Upload native minidumps to `<dsn-host>/api/v1/minidump`. Default false (dumps kept locally + a metadata event is sent). */
12
+ uploadMinidumps?: boolean;
13
+ /** Capture renderer/GPU/child-process-gone as crashes. Default true. */
14
+ enableProcessMonitoring?: boolean;
15
+ /** Treat renderer `unresponsive` as an app-hang event. Default true. */
16
+ enableHangDetection?: boolean;
17
+ /** Emit session events for crash-free sessions. Default true. */
18
+ enableSessionTracking?: boolean;
19
+ /** Opt in to TillShield desktop threat detection (DevTools-open on renderers). Off by default. */
20
+ enableThreatDetection?: boolean;
21
+ disableDefaultPiiScrubbing?: boolean;
22
+ beforeSend?: (e: TillPulseEvent) => TillPulseEvent | null | Promise<TillPulseEvent | null>;
23
+ initialTags?: Record<string, string>;
24
+ flushIntervalMs?: number;
25
+ debug?: boolean;
26
+ }
27
+ declare class TillPulseElectronSDK {
28
+ private opts;
29
+ private queue;
30
+ private endpoint;
31
+ private sessionId;
32
+ private timer;
33
+ private initialized;
34
+ private crumbs;
35
+ private tags;
36
+ private user;
37
+ private sessionStartedAt;
38
+ private sessionEnded;
39
+ init(options: TillPulseElectronOptions): void;
40
+ captureException(error: unknown): void;
41
+ captureMessage(message: string, level?: Severity): void;
42
+ captureSecurityEvent(input: {
43
+ securityType: string;
44
+ severity?: Severity;
45
+ confidence?: number;
46
+ evidence?: Record<string, unknown>;
47
+ message?: string;
48
+ }): void;
49
+ addBreadcrumb(input: {
50
+ type: BreadcrumbType;
51
+ category: string;
52
+ message: string;
53
+ level?: Breadcrumb['level'];
54
+ data?: Record<string, unknown>;
55
+ }): void;
56
+ setUser(user: {
57
+ id?: string;
58
+ segment?: string;
59
+ } | null): void;
60
+ setTag(key: string, value: string): void;
61
+ getQueueStats(): {
62
+ size: number;
63
+ };
64
+ flush(useKeepalive?: boolean): Promise<boolean>;
65
+ close(): void;
66
+ private _device;
67
+ private _base;
68
+ private _captureError;
69
+ private _captureSynthetic;
70
+ private _startSession;
71
+ private _markSession;
72
+ private _emitSession;
73
+ private _enqueue;
74
+ }
75
+ declare const TillPulse: TillPulseElectronSDK;
76
+
77
+ export { TillPulse, type TillPulseElectronOptions, TillPulse as default };
package/dist/index.js ADDED
@@ -0,0 +1,492 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var electron = require('electron');
6
+ var os = require('os');
7
+ var fs = require('fs');
8
+ var path = require('path');
9
+
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var os__namespace = /*#__PURE__*/_interopNamespace(os);
29
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
30
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
31
+
32
+ // src/index.ts
33
+
34
+ // src/scrub.ts
35
+ var SCRUB_RULES = [
36
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[aws_key]" },
37
+ { pattern: /(?:aws_secret|secret_access_key|AWS_SECRET)[^A-Za-z0-9+/]*([A-Za-z0-9+/]{40})\b/gi, replacement: "[aws_secret]" },
38
+ { pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: "[gcp_key]" },
39
+ { pattern: /\b(?:ghp|gho|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[github_token]" },
40
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g, replacement: "[github_token]" },
41
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
42
+ { pattern: /\bxapp-\d-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
43
+ { pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, replacement: "[jwt]" },
44
+ { pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: "Bearer [token]" },
45
+ { pattern: /\b(?:sk|pk|ak|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, replacement: "[apikey]" },
46
+ { pattern: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g, replacement: "[email]" },
47
+ { pattern: /\b[A-Z0-9]{10}\b(?=\s*confirmed|\s*transaction|\s*M-?Pesa)/gi, replacement: "[mpesa_tx]" },
48
+ { pattern: /\b(?:\d[ -]*?){13,19}\b/g, replacement: "[card]" },
49
+ { pattern: /\b\d{13}\b/g, replacement: "[id_number]" },
50
+ { pattern: /\b\d{11}\b/g, replacement: "[id_number]" },
51
+ { pattern: /\+?\d{1,3}[\s.-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}\b/g, replacement: "[phone]" },
52
+ { pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[ip]" }
53
+ ];
54
+ var SENSITIVE_KEY = /^(?:password|passwd|pwd|secret|client[_-]?secret|token|access[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|apikey|authorization|auth|session|cookie|set[_-]?cookie|private[_-]?key|credit[_-]?card|card[_-]?number|cvv|cvc|ssn|pin|otp|mfa|totp)$/i;
55
+ var MAX_REDACT_DEPTH = 24;
56
+ function redactByKey(value, depth = 0) {
57
+ if (depth > MAX_REDACT_DEPTH || value === null || typeof value !== "object") return value;
58
+ if (Array.isArray(value)) return value.map((v) => redactByKey(v, depth + 1));
59
+ const out = {};
60
+ for (const [k, v] of Object.entries(value)) {
61
+ out[k] = SENSITIVE_KEY.test(k) ? "[redacted]" : redactByKey(v, depth + 1);
62
+ }
63
+ return out;
64
+ }
65
+ function scrubEvent(event) {
66
+ try {
67
+ let json = JSON.stringify(redactByKey(event));
68
+ for (const { pattern, replacement } of SCRUB_RULES) json = json.replace(pattern, replacement);
69
+ return JSON.parse(json);
70
+ } catch {
71
+ return event;
72
+ }
73
+ }
74
+
75
+ // src/index.ts
76
+ var SDK_NAME = "@tillstack/electron";
77
+ var SDK_VERSION = "0.1.0";
78
+ function parseDsn(dsn) {
79
+ const m = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);
80
+ if (!m) throw new Error(`TillPulse: invalid DSN: ${dsn}`);
81
+ const scheme = m[1];
82
+ const host = m[3];
83
+ const local = /^(localhost|127\.0\.0\.1|::1)(:\d+)?$/i.test(host);
84
+ if (scheme !== "https" && !local) {
85
+ throw new Error("TillPulse: DSN must use https:// (refusing to send telemetry over cleartext http)");
86
+ }
87
+ return { scheme, host };
88
+ }
89
+ function generateId() {
90
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
91
+ }
92
+ var FileQueue = class {
93
+ constructor(dir) {
94
+ this.items = [];
95
+ this.max = 500;
96
+ this.file = path__namespace.join(dir, "tillpulse-queue.json");
97
+ try {
98
+ if (fs__namespace.existsSync(this.file)) {
99
+ const parsed = JSON.parse(fs__namespace.readFileSync(this.file, "utf8"));
100
+ if (Array.isArray(parsed)) this.items = parsed;
101
+ }
102
+ } catch {
103
+ }
104
+ }
105
+ persist() {
106
+ try {
107
+ fs__namespace.writeFileSync(this.file, JSON.stringify(this.items));
108
+ } catch {
109
+ }
110
+ }
111
+ enqueue(id, eventType, obj) {
112
+ let payload;
113
+ try {
114
+ payload = JSON.stringify(obj);
115
+ } catch {
116
+ return;
117
+ }
118
+ this.items.push({ id, event_type: eventType, payload, attempts: 0, next: 0 });
119
+ if (this.items.length > this.max) this.items = this.items.slice(this.items.length - this.max);
120
+ this.persist();
121
+ }
122
+ due(limit) {
123
+ const now = Date.now();
124
+ return this.items.filter((i) => i.next <= now).slice(0, limit);
125
+ }
126
+ ackOk(ids) {
127
+ const s = new Set(ids);
128
+ this.items = this.items.filter((i) => !s.has(i.id));
129
+ this.persist();
130
+ }
131
+ ackFail(ids) {
132
+ const s = new Set(ids);
133
+ const now = Date.now();
134
+ for (const i of this.items) {
135
+ if (!s.has(i.id)) continue;
136
+ i.attempts += 1;
137
+ i.next = now + Math.min(2e3 * 2 ** (i.attempts - 1), 5 * 6e4);
138
+ }
139
+ this.items = this.items.filter((i) => i.attempts <= 10);
140
+ this.persist();
141
+ }
142
+ size() {
143
+ return this.items.length;
144
+ }
145
+ };
146
+ function osName() {
147
+ if (process.platform === "darwin") return "macOS";
148
+ if (process.platform === "win32") return "Windows";
149
+ return "Linux";
150
+ }
151
+ var TillPulseElectronSDK = class {
152
+ constructor() {
153
+ this.opts = null;
154
+ this.queue = null;
155
+ this.endpoint = "";
156
+ this.sessionId = generateId();
157
+ this.timer = null;
158
+ this.initialized = false;
159
+ this.crumbs = [];
160
+ this.tags = {};
161
+ this.user = null;
162
+ this.sessionStartedAt = 0;
163
+ this.sessionEnded = false;
164
+ }
165
+ init(options) {
166
+ if (this.initialized) return;
167
+ let host;
168
+ let scheme;
169
+ try {
170
+ ;
171
+ ({ host, scheme } = parseDsn(options.dsn));
172
+ } catch (err) {
173
+ if (options.debug) console.error("[TillPulse]", err);
174
+ return;
175
+ }
176
+ this.initialized = true;
177
+ this.opts = options;
178
+ this.endpoint = `${scheme}://${host}/api/v1/envelope`;
179
+ this.tags = { ...options.initialTags };
180
+ let dir = ".";
181
+ try {
182
+ dir = electron.app.getPath("userData");
183
+ } catch {
184
+ }
185
+ this.queue = new FileQueue(dir);
186
+ if (options.enableNativeCrashReporter !== false) {
187
+ try {
188
+ electron.crashReporter.start({
189
+ productName: (() => {
190
+ try {
191
+ return electron.app.getName();
192
+ } catch {
193
+ return "app";
194
+ }
195
+ })(),
196
+ submitURL: options.uploadMinidumps ? `${scheme}://${host}/api/v1/minidump` : void 0,
197
+ uploadToServer: options.uploadMinidumps === true,
198
+ compress: true,
199
+ globalExtra: { tp_release: options.release, tp_env: options.environment ?? "production" }
200
+ });
201
+ } catch (err) {
202
+ if (options.debug) console.warn("[TillPulse] crashReporter.start failed:", err);
203
+ }
204
+ }
205
+ process.on("uncaughtException", (...args) => {
206
+ const err = args[0];
207
+ this._captureError(err, "critical", "uncaughtException");
208
+ this._markSession("crashed");
209
+ void this.flush(true);
210
+ });
211
+ process.on("unhandledRejection", (...args) => {
212
+ this._captureError(args[0], "high", "unhandledRejection");
213
+ });
214
+ if (options.enableProcessMonitoring !== false) {
215
+ electron.app.on("render-process-gone", (...a) => {
216
+ const details = a[2] ?? a[1];
217
+ const reason = details?.reason ?? "unknown";
218
+ const crashed = reason === "crashed" || reason === "oom" || reason === "abnormal-exit";
219
+ this._captureSynthetic("RenderProcessGone", `Renderer process gone: ${reason}`, crashed ? "critical" : "medium", {
220
+ reason,
221
+ exit_code: details?.exitCode
222
+ });
223
+ if (crashed) this._markSession("crashed");
224
+ });
225
+ electron.app.on("child-process-gone", (...a) => {
226
+ const d = a[1];
227
+ this._captureSynthetic("ChildProcessGone", `Child process (${d?.type}) gone: ${d?.reason}`, "medium", {
228
+ type: d?.type,
229
+ reason: d?.reason,
230
+ exit_code: d?.exitCode
231
+ });
232
+ });
233
+ }
234
+ if (options.enableHangDetection !== false) {
235
+ electron.app.on("web-contents-created", (...a) => {
236
+ const contents = a[1];
237
+ if (!contents || typeof contents.on !== "function") return;
238
+ let hangStart = 0;
239
+ contents.on("unresponsive", () => {
240
+ hangStart = Date.now();
241
+ this.addBreadcrumb({ type: "device", category: "app.hang", level: "warning", message: "renderer unresponsive" });
242
+ });
243
+ contents.on("responsive", () => {
244
+ const dur = hangStart ? Date.now() - hangStart : 0;
245
+ if (dur > 0) {
246
+ this._captureSynthetic("AppHang", `Renderer was unresponsive for ${dur}ms`, "medium", { duration_ms: dur });
247
+ }
248
+ hangStart = 0;
249
+ });
250
+ });
251
+ }
252
+ if (options.enableThreatDetection) {
253
+ electron.app.on("web-contents-created", (...a) => {
254
+ const contents = a[1];
255
+ if (!contents || typeof contents.on !== "function") return;
256
+ contents.on("devtools-opened", () => {
257
+ this.captureSecurityEvent({
258
+ securityType: "devtools_open",
259
+ severity: "low",
260
+ confidence: 0.7,
261
+ message: "DevTools opened on a renderer",
262
+ evidence: { url: typeof contents.getURL === "function" ? contents.getURL() : void 0 }
263
+ });
264
+ });
265
+ });
266
+ }
267
+ if (options.enableSessionTracking !== false) {
268
+ const start = () => this._startSession();
269
+ try {
270
+ if (electron.app.isReady()) start();
271
+ else void electron.app.whenReady().then(start);
272
+ } catch {
273
+ start();
274
+ }
275
+ electron.app.on("before-quit", () => {
276
+ this._markSession("exited");
277
+ void this.flush(true);
278
+ });
279
+ electron.app.on("window-all-closed", () => this._markSession("exited"));
280
+ }
281
+ const flushMs = options.flushIntervalMs ?? 1e4;
282
+ this.timer = setInterval(() => void this.flush(), flushMs);
283
+ if (this.timer.unref) this.timer.unref();
284
+ if (options.debug) console.log("[TillPulse] electron SDK initialized", { release: options.release });
285
+ }
286
+ // ── Public API ──
287
+ captureException(error) {
288
+ this._captureError(error, "high", "captureException");
289
+ }
290
+ captureMessage(message, level = "info") {
291
+ this._enqueue(this._base("error", level, { exception: { type: "Message", value: message } }));
292
+ }
293
+ // Report a security signal to TillShield (same shape as the other SDKs).
294
+ captureSecurityEvent(input) {
295
+ this._enqueue(
296
+ this._base("security", input.severity ?? "high", {
297
+ security_type: input.securityType,
298
+ security_confidence: input.confidence ?? 0.8,
299
+ security_evidence: input.evidence ?? {},
300
+ exception: { type: `security:${input.securityType}`, value: input.message ?? input.securityType }
301
+ })
302
+ );
303
+ }
304
+ addBreadcrumb(input) {
305
+ this.crumbs.push({
306
+ type: input.type,
307
+ category: input.category,
308
+ message: input.message,
309
+ level: input.level ?? "info",
310
+ data: input.data,
311
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
312
+ });
313
+ if (this.crumbs.length > 100) this.crumbs = this.crumbs.slice(this.crumbs.length - 100);
314
+ }
315
+ setUser(user) {
316
+ this.user = user;
317
+ }
318
+ setTag(key, value) {
319
+ this.tags[key] = value;
320
+ }
321
+ getQueueStats() {
322
+ return { size: this.queue?.size() ?? 0 };
323
+ }
324
+ async flush(useKeepalive = false) {
325
+ if (!this.queue || !this.opts) return false;
326
+ const items = this.queue.due(50);
327
+ if (items.length === 0) return true;
328
+ const lines = [
329
+ JSON.stringify({ dsn: this.opts.dsn, sdk: { name: SDK_NAME, version: SDK_VERSION }, sent_at: (/* @__PURE__ */ new Date()).toISOString() })
330
+ ];
331
+ const ids = [];
332
+ for (const it of items) {
333
+ lines.push(JSON.stringify({ type: it.event_type, content_type: "application/json", length: it.payload.length }));
334
+ lines.push(it.payload);
335
+ ids.push(it.id);
336
+ }
337
+ try {
338
+ const res = await fetch(this.endpoint, {
339
+ method: "POST",
340
+ headers: { "Content-Type": "application/x-sentry-envelope", Authorization: `DSN ${this.opts.dsn}` },
341
+ body: lines.join("\n")
342
+ });
343
+ if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) {
344
+ this.queue.ackOk(ids);
345
+ return true;
346
+ }
347
+ this.queue.ackFail(ids);
348
+ return false;
349
+ } catch {
350
+ this.queue.ackFail(ids);
351
+ return false;
352
+ }
353
+ }
354
+ close() {
355
+ this._markSession("exited");
356
+ if (this.timer) {
357
+ clearInterval(this.timer);
358
+ this.timer = null;
359
+ }
360
+ void this.flush(true);
361
+ this.initialized = false;
362
+ }
363
+ // ── Internals ──
364
+ _device() {
365
+ let total = 0;
366
+ let free = 0;
367
+ let release2 = "";
368
+ try {
369
+ total = os__namespace.totalmem();
370
+ free = os__namespace.freemem();
371
+ release2 = os__namespace.release();
372
+ } catch {
373
+ }
374
+ return {
375
+ device_model: `Electron ${process.versions["electron"] ?? ""}`.trim(),
376
+ device_manufacturer: process.platform,
377
+ device_family: "desktop",
378
+ os_name: osName(),
379
+ os_version: release2,
380
+ cpu_architecture: process.arch,
381
+ total_ram_mb: total ? Math.round(total / (1024 * 1024)) : void 0,
382
+ available_ram_mb: free ? Math.round(free / (1024 * 1024)) : void 0,
383
+ locale: process.env["LANG"]
384
+ };
385
+ }
386
+ _base(eventType, severity, extra) {
387
+ const o = this.opts;
388
+ const now = (/* @__PURE__ */ new Date()).toISOString();
389
+ const event = {
390
+ event_id: generateId(),
391
+ project_id: "",
392
+ event_type: eventType,
393
+ severity,
394
+ environment: o.environment ?? "production",
395
+ release_version: o.release,
396
+ dist: o.dist,
397
+ timestamp: now,
398
+ client_timestamp: now,
399
+ breadcrumbs: this.crumbs.slice(),
400
+ device: this._device(),
401
+ network: { network_type: "unknown" },
402
+ app_state: { app_foreground: true },
403
+ user: { user_id: this.user?.id, user_segment: this.user?.segment, session_id: this.sessionId },
404
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
405
+ ...extra
406
+ };
407
+ if (Object.keys(this.tags).length) {
408
+ event.tags = { ...this.tags };
409
+ }
410
+ event.extra = {
411
+ chrome: process.versions["chrome"],
412
+ node: process.versions["node"],
413
+ v8: process.versions["v8"]
414
+ };
415
+ return event;
416
+ }
417
+ _captureError(thrown, severity, mechanism) {
418
+ if (!this.opts) return;
419
+ const err = thrown instanceof Error ? thrown : null;
420
+ this._enqueue(
421
+ this._base("error", severity, {
422
+ exception: {
423
+ type: err?.name ?? "Error",
424
+ value: err?.message ?? String(thrown),
425
+ stacktrace: err?.stack ? { frames: parseNodeStack(err.stack) } : void 0
426
+ },
427
+ fingerprint: `electron:${mechanism}`
428
+ })
429
+ );
430
+ }
431
+ _captureSynthetic(type, message, severity, data) {
432
+ if (!this.opts) return;
433
+ const ev = this._base("error", severity, { exception: { type, value: message } });
434
+ ev.extra = {
435
+ ...ev.extra,
436
+ ...data
437
+ };
438
+ this._enqueue(ev);
439
+ }
440
+ _startSession() {
441
+ this.sessionStartedAt = Date.now();
442
+ this.sessionEnded = false;
443
+ this._emitSession("ok");
444
+ }
445
+ _markSession(status) {
446
+ if (this.sessionEnded || this.sessionStartedAt === 0) return;
447
+ this.sessionEnded = true;
448
+ this._emitSession(status, Date.now() - this.sessionStartedAt);
449
+ }
450
+ _emitSession(status, durationMs) {
451
+ if (!this.opts) return;
452
+ this._enqueue(this._base("session", "info", { session_status: status, ...durationMs !== void 0 ? { session_duration_ms: durationMs } : {} }));
453
+ }
454
+ async _enqueue(event) {
455
+ const o = this.opts;
456
+ if (!o || !this.queue) return;
457
+ let final = event;
458
+ if (o.beforeSend) {
459
+ try {
460
+ final = await o.beforeSend(event);
461
+ } catch {
462
+ }
463
+ }
464
+ if (!final) return;
465
+ if (!o.disableDefaultPiiScrubbing) final = scrubEvent(final);
466
+ this.queue.enqueue(final.event_id, final.event_type, final);
467
+ }
468
+ };
469
+ function parseNodeStack(stack) {
470
+ const frames = [];
471
+ for (const raw of stack.split("\n")) {
472
+ const line = raw.trim();
473
+ const m = line.match(/^at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)$/) || line.match(/^at\s+()(.+?):(\d+):(\d+)$/);
474
+ if (!m) continue;
475
+ const file = m[2];
476
+ frames.push({
477
+ filename: file,
478
+ function: m[1] || "<anonymous>",
479
+ lineno: Number(m[3]),
480
+ colno: Number(m[4]),
481
+ in_app: !file.includes("node_modules") && !file.startsWith("node:")
482
+ });
483
+ }
484
+ return frames;
485
+ }
486
+ var TillPulse = new TillPulseElectronSDK();
487
+ var src_default = TillPulse;
488
+
489
+ exports.TillPulse = TillPulse;
490
+ exports.default = src_default;
491
+ //# sourceMappingURL=index.js.map
492
+ //# sourceMappingURL=index.js.map