@greenhat-services/magpie-browser 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Magpie contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # @greenhat-services/magpie-browser
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Magpie error tracking and performance monitoring for the browser.
4
+
5
+ ```sh
6
+ npm install @greenhat-services/magpie-browser
7
+ ```
8
+
9
+ Reports uncaught errors and unhandled rejections, records console, click, navigation and request breadcrumbs, and traces page loads. About 11 KB gzipped, no runtime dependencies.
10
+
11
+ ```ts
12
+ import * as Magpie from '@greenhat-services/magpie-browser';
13
+
14
+ Magpie.init({ dsn: 'https://<key>@<host>/<project>', release: '...', tracesSampleRate: 0.2 });
15
+ Magpie.captureException(error);
16
+ ```
17
+
18
+ Full documentation, options and what is (and is not) sent: https://github.com/Greenhat-Services/magpie-js#readme
19
+
20
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,615 @@
1
+ 'use strict';
2
+
3
+ var magpieCore = require('@greenhat-services/magpie-core');
4
+
5
+ // src/index.ts
6
+ var CONSOLE_LEVELS = [
7
+ ["debug", "debug"],
8
+ ["log", "info"],
9
+ ["info", "info"],
10
+ ["warn", "warning"],
11
+ ["error", "error"]
12
+ ];
13
+ function consoleBreadcrumbs() {
14
+ return {
15
+ name: "ConsoleBreadcrumbs",
16
+ setup(client) {
17
+ if (typeof console === "undefined") return;
18
+ const originals = /* @__PURE__ */ new Map();
19
+ let inside = false;
20
+ for (const [method, level] of CONSOLE_LEVELS) {
21
+ const original = console[method];
22
+ if (typeof original !== "function") continue;
23
+ originals.set(method, original);
24
+ console[method] = function(...args) {
25
+ if (!inside) {
26
+ inside = true;
27
+ try {
28
+ client.addBreadcrumb({ category: "log", level, message: args.map(format).join(" ") });
29
+ } finally {
30
+ inside = false;
31
+ }
32
+ }
33
+ return original.apply(this, args);
34
+ };
35
+ }
36
+ return () => {
37
+ for (const [method, original] of originals) console[method] = original;
38
+ };
39
+ }
40
+ };
41
+ }
42
+ function format(value) {
43
+ if (typeof value === "string") return magpieCore.truncate(value, 300);
44
+ if (value instanceof Error) return magpieCore.truncate(`${value.name}: ${value.message}`, 300);
45
+ try {
46
+ return magpieCore.truncate(JSON.stringify(magpieCore.normalize(value, 2, 100, 10)) ?? String(value), 300);
47
+ } catch {
48
+ return String(value);
49
+ }
50
+ }
51
+ function clickBreadcrumbs() {
52
+ return {
53
+ name: "ClickBreadcrumbs",
54
+ setup(client) {
55
+ if (typeof document === "undefined") return;
56
+ const onClick = (event) => {
57
+ const el = event.target;
58
+ if (!el || !("tagName" in el)) return;
59
+ client.addBreadcrumb({ category: "ui.click", message: describeElement(el) });
60
+ };
61
+ document.addEventListener("click", onClick, { capture: true, passive: true });
62
+ return () => document.removeEventListener("click", onClick, { capture: true });
63
+ }
64
+ };
65
+ }
66
+ function describeElement(el) {
67
+ const parts = [];
68
+ for (let node = el, depth = 0; node && depth < 3; node = node.parentElement, depth++) {
69
+ const tag = node.tagName.toLowerCase();
70
+ if (tag === "html" || tag === "body") break;
71
+ let part = tag;
72
+ if (node.id) part += `#${node.id}`;
73
+ const classes = (typeof node.className === "string" ? node.className : "").split(/\s+/).filter(Boolean).slice(0, 3);
74
+ if (classes.length) part += `.${classes.join(".")}`;
75
+ const testId = node.getAttribute("data-testid");
76
+ if (testId) part += `[data-testid=${testId}]`;
77
+ parts.unshift(part);
78
+ if (node.id) break;
79
+ }
80
+ return magpieCore.truncate(parts.join(" > ") || el.tagName.toLowerCase(), 200);
81
+ }
82
+ function historyBreadcrumbs(onNavigate) {
83
+ return {
84
+ name: "HistoryBreadcrumbs",
85
+ setup(client) {
86
+ if (typeof window === "undefined" || !window.history) return;
87
+ let from = location.href;
88
+ const record = () => {
89
+ const to = location.href;
90
+ if (to === from) return;
91
+ client.addBreadcrumb({ category: "navigation", data: { from: magpieCore.scrubUrl(from), to: magpieCore.scrubUrl(to) } });
92
+ from = to;
93
+ onNavigate?.(to);
94
+ };
95
+ const restore = [];
96
+ for (const method of ["pushState", "replaceState"]) {
97
+ const original = history[method];
98
+ history[method] = function(...args) {
99
+ const result = original.apply(this, args);
100
+ record();
101
+ return result;
102
+ };
103
+ restore.push(() => {
104
+ history[method] = original;
105
+ });
106
+ }
107
+ window.addEventListener("popstate", record);
108
+ restore.push(() => window.removeEventListener("popstate", record));
109
+ return () => restore.forEach((r) => r());
110
+ }
111
+ };
112
+ }
113
+
114
+ // src/globalHandlers.ts
115
+ function globalHandlers() {
116
+ return {
117
+ name: "GlobalHandlers",
118
+ setup(client) {
119
+ if (typeof window === "undefined") return;
120
+ const onError = (event) => {
121
+ const target = event.target;
122
+ if (target && target !== window && "tagName" in target) {
123
+ const src = target.src || target.href || "";
124
+ client.addBreadcrumb({ category: "resource", level: "warning", message: `Failed to load <${target.tagName.toLowerCase()}> ${src}`.trim() });
125
+ return;
126
+ }
127
+ const e = event;
128
+ const mechanism = { type: "onerror", handled: false };
129
+ if (e.error !== void 0 && e.error !== null) {
130
+ client.captureException(e.error, { mechanism });
131
+ return;
132
+ }
133
+ if (!e.filename && /^Script error\.?$/i.test(e.message)) return;
134
+ const error = new Error(e.message);
135
+ error.name = /^(\w+Error):/.exec(e.message)?.[1] ?? "Error";
136
+ error.stack = e.filename ? `${error.name}: ${e.message}
137
+ at ${e.filename}:${e.lineno || 0}:${e.colno || 0}` : void 0;
138
+ client.captureException(error, { mechanism });
139
+ };
140
+ const onRejection = (event) => {
141
+ client.captureException(event.reason, { mechanism: { type: "onunhandledrejection", handled: false } });
142
+ };
143
+ window.addEventListener("error", onError, true);
144
+ window.addEventListener("unhandledrejection", onRejection);
145
+ return () => {
146
+ window.removeEventListener("error", onError, true);
147
+ window.removeEventListener("unhandledrejection", onRejection);
148
+ };
149
+ }
150
+ };
151
+ }
152
+
153
+ // src/idle.ts
154
+ var trackers = /* @__PURE__ */ new WeakMap();
155
+ function idleFor(tx) {
156
+ return trackers.get(tx);
157
+ }
158
+ function trackIdle(tx, options) {
159
+ let pending = 0;
160
+ let loaded = !options.waitForLoad || typeof document !== "undefined" && document.readyState === "complete";
161
+ let lastActivity = Date.now();
162
+ let timer;
163
+ let ended = false;
164
+ const cleanup = [];
165
+ const end = (backdate) => {
166
+ if (ended) return;
167
+ ended = true;
168
+ if (timer) clearTimeout(timer);
169
+ cleanup.forEach((c) => c());
170
+ const endAt = Math.max(tx.startTimestamp, (backdate ? lastActivity : Date.now()) / 1e3);
171
+ options.beforeEnd?.(tx, endAt);
172
+ tx.end(endAt);
173
+ options.onEnd?.(tx);
174
+ };
175
+ const arm = () => {
176
+ if (timer) clearTimeout(timer);
177
+ if (ended || pending > 0 || !loaded) return;
178
+ timer = setTimeout(() => end(true), options.idleTimeout);
179
+ };
180
+ const idle = {
181
+ requestStarted() {
182
+ pending++;
183
+ lastActivity = Date.now();
184
+ if (timer) clearTimeout(timer);
185
+ },
186
+ requestFinished() {
187
+ pending = Math.max(0, pending - 1);
188
+ lastActivity = Date.now();
189
+ arm();
190
+ },
191
+ finish: () => end(false)
192
+ };
193
+ trackers.set(tx, idle);
194
+ const max = setTimeout(() => end(false), options.maxDuration);
195
+ cleanup.push(() => clearTimeout(max));
196
+ if (typeof window !== "undefined") {
197
+ if (!loaded) {
198
+ const onLoad = () => {
199
+ loaded = true;
200
+ lastActivity = Date.now();
201
+ arm();
202
+ };
203
+ window.addEventListener("load", onLoad, { once: true });
204
+ cleanup.push(() => window.removeEventListener("load", onLoad));
205
+ }
206
+ const onHide = () => {
207
+ if (document.visibilityState === "hidden") end(true);
208
+ };
209
+ document.addEventListener("visibilitychange", onHide);
210
+ cleanup.push(() => document.removeEventListener("visibilitychange", onHide));
211
+ }
212
+ arm();
213
+ return idle;
214
+ }
215
+
216
+ // src/http.ts
217
+ var refusedHosts = /* @__PURE__ */ new Set();
218
+ function httpIntegration() {
219
+ return {
220
+ name: "Http",
221
+ setup(client) {
222
+ if (typeof window === "undefined") return;
223
+ const restore = [];
224
+ const skip = (url) => !!client.dsn && absolute(url).startsWith(client.dsn.origin);
225
+ if (typeof window.fetch === "function") {
226
+ const original = window.fetch;
227
+ window.fetch = function patchedFetch(input, init2) {
228
+ const info = describe(input, init2);
229
+ if (skip(info.url)) return original.call(this, input, init2);
230
+ const crossOrigin = !sameOrigin(absolute(info.url));
231
+ const host = hostOf(info.url);
232
+ const call = begin(client, info, crossOrigin && refusedHosts.has(host));
233
+ let args = [input, init2];
234
+ if (call.header) {
235
+ const headers = new Headers(init2?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0));
236
+ if (!headers.has(magpieCore.TRACE_HEADER)) headers.set(magpieCore.TRACE_HEADER, call.header);
237
+ args = [input, { ...init2, headers }];
238
+ }
239
+ const done = (response) => {
240
+ call.finish(response.status);
241
+ return response;
242
+ };
243
+ const fail = (error) => {
244
+ call.finish(0);
245
+ throw error;
246
+ };
247
+ return original.apply(this, args).then(done, (error) => {
248
+ if (call.header && crossOrigin && error instanceof TypeError) {
249
+ refusedHosts.add(host);
250
+ if (client.options.debug) console.warn(`[magpie] ${host} rejected the ${magpieCore.TRACE_HEADER} header (its CORS policy does not allow it), so it will not be sent there again. Remove it from tracePropagationTargets, or allow the header on that API.`);
251
+ if (info.method === "GET" || info.method === "HEAD") return original.call(this, input, init2).then(done, fail);
252
+ }
253
+ return fail(error);
254
+ });
255
+ };
256
+ restore.push(() => {
257
+ window.fetch = original;
258
+ });
259
+ }
260
+ if (typeof XMLHttpRequest !== "undefined") {
261
+ const proto = XMLHttpRequest.prototype;
262
+ const open = proto.open;
263
+ const send = proto.send;
264
+ const state = /* @__PURE__ */ new WeakMap();
265
+ proto.open = function(method, url, ...rest) {
266
+ state.set(this, { method: String(method).toUpperCase(), url: String(url) });
267
+ return open.call(this, method, url, ...rest);
268
+ };
269
+ proto.send = function(body) {
270
+ const info = state.get(this);
271
+ if (info && !skip(info.url)) {
272
+ const call = begin(client, info);
273
+ if (call.header) {
274
+ try {
275
+ this.setRequestHeader(magpieCore.TRACE_HEADER, call.header);
276
+ } catch {
277
+ }
278
+ }
279
+ this.addEventListener("loadend", () => call.finish(this.status));
280
+ }
281
+ return send.call(this, body);
282
+ };
283
+ restore.push(() => {
284
+ proto.open = open;
285
+ proto.send = send;
286
+ });
287
+ }
288
+ return () => restore.forEach((r) => r());
289
+ }
290
+ };
291
+ }
292
+ function begin(client, info, withholdHeader = false) {
293
+ const started = performance.now();
294
+ const parent = client.activeTransaction;
295
+ const idle = parent ? idleFor(parent) : void 0;
296
+ const tracing = (client.options.tracesSampleRate ?? 0) > 0 && parent !== void 0;
297
+ let span;
298
+ if (parent?.sampled) {
299
+ span = parent.startChild({ op: "http.client", description: `${info.method} ${stripQuery(info.url)}`, data: { method: info.method } });
300
+ idle?.requestStarted();
301
+ } else if (parent) {
302
+ idle?.requestStarted();
303
+ }
304
+ const target = absolute(info.url);
305
+ const header = !withholdHeader && tracing && parent && (sameOrigin(target) || client.shouldPropagate(target)) ? magpieCore.formatTraceHeader(parent.traceId, span?.spanId ?? parent.spanId, parent.sampled) : void 0;
306
+ return {
307
+ header,
308
+ finish(status) {
309
+ const duration = Math.round(performance.now() - started);
310
+ const failed = status === 0 || status >= 400;
311
+ if (span) {
312
+ if (status) span.setData("status_code", status);
313
+ span.setStatus(status === 0 ? "unknown_error" : status >= 500 ? "internal_error" : "ok");
314
+ span.end();
315
+ }
316
+ if (parent) idle?.requestFinished();
317
+ client.addBreadcrumb({
318
+ category: "http",
319
+ type: "http",
320
+ level: status >= 500 || status === 0 ? "error" : failed ? "warning" : "info",
321
+ message: `${info.method} ${magpieCore.scrubUrl(info.url)}`,
322
+ data: { method: info.method, ...status ? { status_code: status } : { status_code: "failed" }, duration_ms: duration }
323
+ });
324
+ }
325
+ };
326
+ }
327
+ function describe(input, init2) {
328
+ const isRequest = typeof Request !== "undefined" && input instanceof Request;
329
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : isRequest ? input.url : String(input);
330
+ return { method: (init2?.method ?? (isRequest ? input.method : "GET")).toUpperCase(), url };
331
+ }
332
+ function absolute(url) {
333
+ try {
334
+ return new URL(url, location.href).href;
335
+ } catch {
336
+ return url;
337
+ }
338
+ }
339
+ function hostOf(url) {
340
+ try {
341
+ return new URL(url, location.href).host;
342
+ } catch {
343
+ return url;
344
+ }
345
+ }
346
+ function sameOrigin(absoluteUrl) {
347
+ try {
348
+ return new URL(absoluteUrl).origin === location.origin;
349
+ } catch {
350
+ return false;
351
+ }
352
+ }
353
+ function stripQuery(url) {
354
+ return magpieCore.scrubUrl(url).split(/[?#]/)[0];
355
+ }
356
+ var KEEPALIVE_LIMIT = 6e4;
357
+ function fetchTransport(dsn, fetchImpl = (...args) => globalThis.fetch(...args)) {
358
+ const url = (payload) => `${magpieCore.endpoint(dsn, payload.type === "transaction" ? "transactions" : "events")}?magpie_key=${encodeURIComponent(dsn.publicKey)}`;
359
+ return {
360
+ async send(payload) {
361
+ try {
362
+ const body = JSON.stringify(payload);
363
+ const response = await fetchImpl(url(payload), {
364
+ method: "POST",
365
+ body,
366
+ headers: { "Content-Type": "text/plain" },
367
+ keepalive: body.length < KEEPALIVE_LIMIT,
368
+ mode: "cors",
369
+ credentials: "omit",
370
+ referrerPolicy: "no-referrer"
371
+ });
372
+ return { ok: response.ok, status: response.status, retryAfter: parseRetryAfter(response.headers.get("retry-after")) };
373
+ } catch {
374
+ return { ok: false };
375
+ }
376
+ }
377
+ };
378
+ }
379
+ function parseRetryAfter(value) {
380
+ const seconds = Number(value);
381
+ return value !== null && Number.isFinite(seconds) && seconds > 0 ? Math.min(seconds, 3600) : void 0;
382
+ }
383
+
384
+ // src/uaparse.ts
385
+ function parseUserAgent(ua) {
386
+ const first = (re) => re.exec(ua)?.[1];
387
+ const major = (v) => v ? v.split(".")[0] : "";
388
+ let browser = { name: "Unknown", version: "" };
389
+ const rules = [
390
+ ["Edge", /\bEdgA?i?O?S?\/([\d.]+)/],
391
+ ["Opera", /\bOPR\/([\d.]+)/],
392
+ ["Samsung Internet", /\bSamsungBrowser\/([\d.]+)/],
393
+ ["Firefox", /\b(?:Firefox|FxiOS)\/([\d.]+)/],
394
+ ["Chrome", /\b(?:Headless)?(?:Chrome|Chromium|CriOS)\/([\d.]+)/],
395
+ ["Safari", /\bVersion\/([\d.]+).*\bSafari\//]
396
+ ];
397
+ for (const [name, re] of rules) {
398
+ const v = first(re);
399
+ if (v) {
400
+ browser = { name, version: major(v) };
401
+ break;
402
+ }
403
+ }
404
+ let os = { name: "Unknown" };
405
+ if (/\bWindows NT/.test(ua)) {
406
+ const nt = first(/\bWindows NT ([\d.]+)/);
407
+ os = { name: "Windows", version: nt === "10.0" ? "10" : nt === "6.3" ? "8.1" : nt === "6.2" ? "8" : nt === "6.1" ? "7" : nt };
408
+ } else if (/\b(iPhone|iPad|iPod)\b/.test(ua)) {
409
+ os = { name: "iOS", version: first(/\bOS ([\d_]+)/)?.replace(/_/g, ".") };
410
+ } else if (/\bAndroid\b/.test(ua)) {
411
+ os = { name: "Android", version: first(/\bAndroid ([\d.]+)/) };
412
+ } else if (/\bMac OS X\b/.test(ua)) {
413
+ os = { name: "macOS", version: first(/\bMac OS X ([\d_.]+)/)?.replace(/_/g, ".") };
414
+ } else if (/\bCrOS\b/.test(ua)) {
415
+ os = { name: "Chrome OS" };
416
+ } else if (/\bLinux\b/.test(ua)) {
417
+ os = { name: "Linux" };
418
+ }
419
+ const device = /\b(iPad|Tablet)\b/.test(ua) || /\bAndroid\b/.test(ua) && !/\bMobile\b/.test(ua) ? "tablet" : /\b(iPhone|iPod|Mobile)\b/.test(ua) ? "mobile" : "desktop";
420
+ return { browser, os, device };
421
+ }
422
+
423
+ // src/runtime.ts
424
+ var SDK_NAME = "magpie.javascript.browser";
425
+ function browserRuntime(options = {}) {
426
+ let agent;
427
+ const ua = () => typeof navigator === "undefined" ? "" : navigator.userAgent;
428
+ const parsed = () => agent ??= parseUserAgent(ua());
429
+ return {
430
+ sdk: { name: options.sdkName ?? SDK_NAME, version: magpieCore.SDK_VERSION },
431
+ platform: "javascript",
432
+ // The caller passes the original fetch, taken before it is patched, so delivering an event is never itself traced.
433
+ createTransport: (dsn) => fetchTransport(dsn, options.fetch),
434
+ contexts() {
435
+ const a = parsed();
436
+ return {
437
+ browser: { name: a.browser.name, version: a.browser.version },
438
+ os: { name: a.os.name, ...a.os.version ? { version: a.os.version } : {} },
439
+ device: {
440
+ type: a.device,
441
+ ...typeof screen !== "undefined" ? { screen_resolution: `${screen.width}x${screen.height}` } : {},
442
+ ...typeof navigator !== "undefined" && navigator.language ? { language: navigator.language } : {}
443
+ }
444
+ };
445
+ },
446
+ request() {
447
+ if (typeof location === "undefined") return void 0;
448
+ return {
449
+ url: location.href,
450
+ headers: {
451
+ "User-Agent": ua(),
452
+ ...options.sendDefaultPii && typeof document !== "undefined" && document.referrer ? { Referer: document.referrer } : {}
453
+ }
454
+ };
455
+ }
456
+ };
457
+ }
458
+ var DEFAULTS = { pageload: true, history: false, idleTimeout: 1e3, maxDuration: 15e3 };
459
+ var settings = /* @__PURE__ */ new WeakMap();
460
+ function tracingIntegration(options = {}) {
461
+ return {
462
+ name: "Tracing",
463
+ setup(client) {
464
+ if (typeof window === "undefined" || !((client.options.tracesSampleRate ?? 0) > 0)) return;
465
+ const config = { ...DEFAULTS, ...options };
466
+ settings.set(client, config);
467
+ const teardown = [];
468
+ if (config.pageload) startPageload(client, config);
469
+ if (config.history) {
470
+ const onChange = () => startNavigation(client, location.pathname);
471
+ window.addEventListener("popstate", onChange);
472
+ teardown.push(() => window.removeEventListener("popstate", onChange));
473
+ }
474
+ return () => {
475
+ idleFor(client.activeTransaction)?.finish();
476
+ teardown.forEach((t) => t());
477
+ };
478
+ }
479
+ };
480
+ }
481
+ function startNavigation(client, name) {
482
+ const config = settings.get(client);
483
+ if (!config) return void 0;
484
+ if (client.activeTransaction) idleFor(client.activeTransaction)?.finish();
485
+ const tx = client.startTransaction({ name, op: "navigation" });
486
+ client.activeTransaction = tx;
487
+ client.scope.setTransactionName(name);
488
+ trackIdle(tx, {
489
+ idleTimeout: config.idleTimeout,
490
+ maxDuration: config.maxDuration,
491
+ onEnd: (ended) => {
492
+ if (client.activeTransaction === ended) client.activeTransaction = void 0;
493
+ }
494
+ });
495
+ return tx;
496
+ }
497
+ function renameActiveTransaction(client, name) {
498
+ client.activeTransaction?.setName(name);
499
+ client.scope.setTransactionName(name);
500
+ }
501
+ function startPageload(client, config) {
502
+ const meta = document.querySelector('meta[name="magpie-trace"]')?.getAttribute("content");
503
+ const parent = magpieCore.parseTraceHeader(meta);
504
+ const tx = client.startTransaction({
505
+ name: location.pathname,
506
+ op: "pageload",
507
+ startTimestamp: performance.timeOrigin / 1e3,
508
+ ...parent ? { traceId: parent.traceId, parentSpanId: parent.spanId, sampled: parent.sampled } : {}
509
+ });
510
+ client.activeTransaction = tx;
511
+ client.scope.setTransactionName(tx.name);
512
+ const lcp = observeLargestPaint();
513
+ trackIdle(tx, {
514
+ idleTimeout: config.idleTimeout,
515
+ maxDuration: config.maxDuration,
516
+ waitForLoad: true,
517
+ beforeEnd: (t) => addPageloadSpans(client, t, lcp()),
518
+ onEnd: (ended) => {
519
+ if (client.activeTransaction === ended) client.activeTransaction = void 0;
520
+ }
521
+ });
522
+ }
523
+ function observeLargestPaint() {
524
+ let value;
525
+ try {
526
+ const observer = new PerformanceObserver((list) => {
527
+ const last = list.getEntries().at(-1);
528
+ if (last) value = last.startTime;
529
+ });
530
+ observer.observe({ type: "largest-contentful-paint", buffered: true });
531
+ } catch {
532
+ }
533
+ return () => value;
534
+ }
535
+ function addPageloadSpans(client, tx, lcp) {
536
+ if (!tx.sampled) return;
537
+ try {
538
+ const origin = performance.timeOrigin;
539
+ const at = (ms) => (origin + ms) / 1e3;
540
+ const add = (op, description, from, to) => {
541
+ if (!(to > from) && !(op === "ui.paint" && to === from)) return;
542
+ const span = tx.startChild({ op, description, startTimestamp: at(from) });
543
+ span.end(at(to));
544
+ };
545
+ const nav = performance.getEntriesByType("navigation")[0];
546
+ if (nav) {
547
+ add("browser.dns", "DNS lookup", nav.domainLookupStart, nav.domainLookupEnd);
548
+ add("browser.connect", "Connection", nav.connectStart, nav.connectEnd);
549
+ add("browser.request", "Waiting for the first byte", nav.requestStart, nav.responseStart);
550
+ add("browser.response", "Downloading the document", nav.responseStart, nav.responseEnd);
551
+ add("browser.dom", "Building the page", nav.responseEnd, nav.domContentLoadedEventEnd);
552
+ add("browser.load", "Loading everything else", nav.domContentLoadedEventEnd, nav.loadEventEnd);
553
+ }
554
+ const fcp = performance.getEntriesByName("first-contentful-paint")[0];
555
+ if (fcp) add("ui.paint", "first-contentful-paint", fcp.startTime, fcp.startTime);
556
+ if (lcp !== void 0) add("ui.paint", "largest-contentful-paint", lcp, lcp);
557
+ const ingest = client.dsn?.origin;
558
+ performance.getEntriesByType("resource").filter((r) => {
559
+ const entry = r;
560
+ return ["script", "link", "css", "img"].includes(entry.initiatorType) && entry.responseEnd > 0 && !(ingest && entry.name.startsWith(ingest));
561
+ }).slice(0, 25).forEach((r) => {
562
+ const entry = r;
563
+ add(`resource.${entry.initiatorType}`, entry.name.split(/[?#]/)[0], entry.startTime, entry.responseEnd);
564
+ });
565
+ } catch {
566
+ }
567
+ }
568
+ var injectedRelease = () => globalThis.__MAGPIE_RELEASE__;
569
+ function init(options = {}) {
570
+ if (typeof window === "undefined") return void 0;
571
+ const previous = magpieCore.getClient();
572
+ if (previous) void previous.close(0);
573
+ const { captureUnhandled = true, breadcrumbs = true, tracing, sdkName, integrations = [], ...rest } = options;
574
+ const crumbs = breadcrumbs === false ? {} : { console: true, clicks: true, history: true, http: true, ...breadcrumbs === true ? {} : breadcrumbs };
575
+ const tracesOn = (rest.tracesSampleRate ?? 0) > 0 && tracing !== false;
576
+ const nativeFetch = typeof window.fetch === "function" ? window.fetch.bind(window) : void 0;
577
+ const client = new magpieCore.Client(
578
+ { ...rest, release: rest.release ?? injectedRelease() },
579
+ browserRuntime({ sendDefaultPii: rest.sendDefaultPii, sdkName, fetch: nativeFetch })
580
+ );
581
+ const defaults = [
582
+ ...captureUnhandled ? [globalHandlers()] : [],
583
+ ...crumbs.console ? [consoleBreadcrumbs()] : [],
584
+ ...crumbs.clicks ? [clickBreadcrumbs()] : [],
585
+ ...crumbs.history ? [historyBreadcrumbs()] : [],
586
+ // Request breadcrumbs and spans share one patch, and tracing needs it, so it runs when either is wanted.
587
+ ...crumbs.http || tracesOn ? [httpIntegration()] : [],
588
+ ...tracesOn ? [tracingIntegration(typeof tracing === "object" ? tracing : {})] : []
589
+ ];
590
+ magpieCore.setClient(client);
591
+ client.scope.setTransactionName(location.pathname);
592
+ client.setupIntegrations([...defaults.filter((i) => i.name !== "Tracing"), ...defaults.filter((i) => i.name === "Tracing"), ...integrations]);
593
+ return client;
594
+ }
595
+
596
+ exports.clickBreadcrumbs = clickBreadcrumbs;
597
+ exports.consoleBreadcrumbs = consoleBreadcrumbs;
598
+ exports.describeElement = describeElement;
599
+ exports.fetchTransport = fetchTransport;
600
+ exports.globalHandlers = globalHandlers;
601
+ exports.historyBreadcrumbs = historyBreadcrumbs;
602
+ exports.httpIntegration = httpIntegration;
603
+ exports.init = init;
604
+ exports.parseUserAgent = parseUserAgent;
605
+ exports.renameActiveTransaction = renameActiveTransaction;
606
+ exports.startNavigation = startNavigation;
607
+ exports.tracingIntegration = tracingIntegration;
608
+ Object.keys(magpieCore).forEach(function (k) {
609
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
610
+ enumerable: true,
611
+ get: function () { return magpieCore[k]; }
612
+ });
613
+ });
614
+ //# sourceMappingURL=index.cjs.map
615
+ //# sourceMappingURL=index.cjs.map