@maple-dev/browser 0.11.1 → 0.13.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 +23 -9
- package/dist/console-iNhlufZV.mjs +183 -0
- package/dist/{deferred-B8pFPgZ_.mjs → deferred-Dr2AmxSU.mjs} +32 -5
- package/dist/index.d.mts +12 -5
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +7 -4
- package/dist/react.mjs +43 -5
- package/dist/replay-session-BuUOwHnI.mjs +906 -0
- package/dist/{sink-DbEuphC-.mjs → sink-Ji0J42ul.mjs} +848 -188
- package/dist/{src-CCfykEMo.mjs → src-JUynR8Jc.mjs} +535 -169
- package/package.json +9 -6
- package/dist/console-Bkd3hBGp.mjs +0 -73
- package/dist/replay-session-D0zjvZIV.mjs +0 -596
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ MapleBrowser.init({
|
|
|
28
28
|
|
|
29
29
|
That single call:
|
|
30
30
|
|
|
31
|
-
- starts OTel browser tracing, auto-instrumenting `fetch
|
|
32
|
-
ingest (`POST /v1/traces`);
|
|
31
|
+
- starts OTel browser tracing, auto-instrumenting `fetch` and `XMLHttpRequest`,
|
|
32
|
+
exporting to Maple's ingest (`POST /v1/traces`);
|
|
33
33
|
- records the session with rrweb, chunking events (~5s / 100KB windows),
|
|
34
34
|
gzipping them with the native `CompressionStream`, and uploading to
|
|
35
35
|
`POST /v1/sessionReplays/blob`. rrweb ships in a lazy code-split chunk loaded
|
|
@@ -57,6 +57,11 @@ try {
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
`captureException` is safe before `init()`: calls are held (up to 30) and recorded when it runs.
|
|
61
|
+
A thrown value that is not an `Error` (`throw "boom"`, `throw { code: 1 }`) is recorded with its
|
|
62
|
+
fields as the message, and an uncaught one gets a single frame at the throw site rather than a
|
|
63
|
+
stack from inside the SDK.
|
|
64
|
+
|
|
60
65
|
Opt out of the global handlers with `tracing: { captureErrors: false }` — worth
|
|
61
66
|
doing only when another tracker already owns them, or the same crash is recorded
|
|
62
67
|
twice.
|
|
@@ -70,7 +75,10 @@ fingerprint to one contentless issue that buries the real ones. Add
|
|
|
70
75
|
|
|
71
76
|
`fetch`/XHR responses with a 4xx or 5xx status are errors, typed by status, as the HTTP semantic
|
|
72
77
|
conventions say for client spans. Narrow that with `errors: { captureHttpStatus: [[500, 599]] }`.
|
|
73
|
-
A
|
|
78
|
+
A request that never got a response is an error too, unless the connection went away while it was
|
|
79
|
+
in flight: the page unloaded or was hidden, or the browser went offline (or was offline when it
|
|
80
|
+
failed). Those stay `Unset` with `maple.connection_lost` set to `page_unload`, `page_hidden` or
|
|
81
|
+
`offline`, since the browser cancelled them, not your API.
|
|
74
82
|
|
|
75
83
|
### Breadcrumbs
|
|
76
84
|
|
|
@@ -99,9 +107,10 @@ in production.
|
|
|
99
107
|
|
|
100
108
|
## Identifying users
|
|
101
109
|
|
|
102
|
-
Pass `
|
|
103
|
-
`MapleBrowser.identify(user.id)` later.
|
|
104
|
-
metadata rows and stamped as `user.id` on future
|
|
110
|
+
Pass `user` to `init()` when you already know the signed-in user
|
|
111
|
+
(`user: { id: user.id, email: user.email }`), or call `MapleBrowser.identify(user.id)` later.
|
|
112
|
+
The id is attached to future session metadata rows and stamped as `user.id` on future
|
|
113
|
+
browser-created spans.
|
|
105
114
|
|
|
106
115
|
```ts
|
|
107
116
|
MapleBrowser.identify(user.id)
|
|
@@ -182,7 +191,7 @@ MapleBrowser.init({
|
|
|
182
191
|
|
|
183
192
|
Call these from your router's hooks to make one click one trace: a navigation
|
|
184
193
|
span, the data-loading spans under it, and the `fetch` spans they start.
|
|
185
|
-
[maple.dev/docs/
|
|
194
|
+
[maple.dev/docs/session-replay/browser-sdk](https://maple.dev/docs/session-replay/browser-sdk#framework-examples) shows where for each
|
|
186
195
|
framework.
|
|
187
196
|
|
|
188
197
|
```ts
|
|
@@ -196,12 +205,12 @@ MapleBrowser.endNavigation("/projects/:id") // the route is ready: its template
|
|
|
196
205
|
- The first `startNavigation` opens a `pageload` span, joined to the server
|
|
197
206
|
render's trace from a `Server-Timing: traceparent;desc="…"` entry or a
|
|
198
207
|
`<meta name="traceparent">` tag; later calls open `navigate` spans, and end
|
|
199
|
-
one still open
|
|
208
|
+
one still open, marked `maple.navigation.interrupted`.
|
|
200
209
|
- The document's `pageload` starts at navigation start, and gets child spans
|
|
201
210
|
from the Navigation Timing entry once the page has loaded: `documentFetch`
|
|
202
211
|
(with `dns`, `connect`, `request` and `response` under it), `domProcessing`
|
|
203
212
|
and `loadEvent`.
|
|
204
|
-
- `traced`
|
|
213
|
+
- `traced` takes a sync or async `fn`, returns its result and rethrows its error unchanged. Only requests
|
|
205
214
|
started before `fn`'s first `await` nest under its span. An error it recorded
|
|
206
215
|
isn't reported again by `captureException` or the global handlers.
|
|
207
216
|
- All three are no-ops on the server, before `init()`, with tracing disabled or
|
|
@@ -220,6 +229,11 @@ instrumentTanStackRouter(router) // navigate /projects/$projectId
|
|
|
220
229
|
```
|
|
221
230
|
|
|
222
231
|
Attach them after `MapleBrowser.init`: a navigation reported earlier uses up the page load.
|
|
232
|
+
Attaching the same router twice (React StrictMode, HMR) keeps one instrumentation, and the
|
|
233
|
+
unsubscribe they return ends a navigation still open as interrupted.
|
|
234
|
+
|
|
235
|
+
`mapleReactErrorHandler(source, next)` replaces React's own console logging for that callback, so
|
|
236
|
+
it hands the error on to `next` afterwards, `console.error` by default.
|
|
223
237
|
|
|
224
238
|
## Linking a marketing site to your app
|
|
225
239
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { I as safeEmit, L as truncateText, Y as isJsonObject, b as patchMethod } from "./sink-Ji0J42ul.mjs";
|
|
2
|
+
//#region ../browser-session/src/replay/capture/console.ts
|
|
3
|
+
const LEVELS = [
|
|
4
|
+
"log",
|
|
5
|
+
"info",
|
|
6
|
+
"warn",
|
|
7
|
+
"error",
|
|
8
|
+
"debug"
|
|
9
|
+
];
|
|
10
|
+
const MAX_MESSAGE = 2e3;
|
|
11
|
+
/** No array logged into a 2,000-character message needs more elements than this. */
|
|
12
|
+
const MAX_ARRAY_ELEMENTS = 100;
|
|
13
|
+
/** Nor more keys of one object, or more levels of nesting. */
|
|
14
|
+
const MAX_OBJECT_KEYS = 50;
|
|
15
|
+
const MAX_DEPTH = 6;
|
|
16
|
+
/**
|
|
17
|
+
* Everyone capturing this module's console wrappers: breadcrumbs and replay can
|
|
18
|
+
* both listen. One wrapper per level per page, toggled by whether anyone
|
|
19
|
+
* listens, so starting and stopping capture across visibility changes does not
|
|
20
|
+
* stack wrappers.
|
|
21
|
+
*/
|
|
22
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
23
|
+
let unpatch = [];
|
|
24
|
+
/** Set while listeners run: a listener that logs must not be captured again. */
|
|
25
|
+
let dispatching = false;
|
|
26
|
+
function wrapLevel(level) {
|
|
27
|
+
return (original, hooks) => (...args) => {
|
|
28
|
+
const subscribers = hooks();
|
|
29
|
+
if (subscribers && !dispatching) {
|
|
30
|
+
dispatching = true;
|
|
31
|
+
try {
|
|
32
|
+
for (const listener of subscribers) listener(level, args);
|
|
33
|
+
} catch {} finally {
|
|
34
|
+
dispatching = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
original.apply(console, args);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Capture `console.*` calls as session events, then forward to the original so
|
|
42
|
+
* the host app's console behaves normally. Never throws into the call site.
|
|
43
|
+
* Teardown leaves a wrapper that something else wrapped in place, inert.
|
|
44
|
+
*/
|
|
45
|
+
function installConsoleCapture(emit, options = {}) {
|
|
46
|
+
const listener = (level, args) => {
|
|
47
|
+
safeEmit(emit, {
|
|
48
|
+
type: "console",
|
|
49
|
+
level,
|
|
50
|
+
...options.maskText ? void 0 : { message: formatArgs(args) }
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
if (listeners.size === 0) unpatch = LEVELS.map((level) => patchMethod(console, level, listeners, wrapLevel(level)));
|
|
54
|
+
listeners.add(listener);
|
|
55
|
+
let removed = false;
|
|
56
|
+
return () => {
|
|
57
|
+
if (removed) return;
|
|
58
|
+
removed = true;
|
|
59
|
+
listeners.delete(listener);
|
|
60
|
+
if (listeners.size > 0) return;
|
|
61
|
+
for (const off of unpatch) off();
|
|
62
|
+
unpatch = [];
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** One console line as text, at most `MAX_MESSAGE` characters. Never throws. */
|
|
66
|
+
function formatArgs(args) {
|
|
67
|
+
let text = "";
|
|
68
|
+
for (const arg of args) {
|
|
69
|
+
if (text.length > MAX_MESSAGE) break;
|
|
70
|
+
const room = MAX_MESSAGE - text.length + 1;
|
|
71
|
+
text += (text ? " " : "") + formatArg(arg, room);
|
|
72
|
+
}
|
|
73
|
+
return truncateText(text, MAX_MESSAGE, "…");
|
|
74
|
+
}
|
|
75
|
+
function formatArg(arg, budget) {
|
|
76
|
+
if (typeof arg === "string") return truncateText(arg, budget);
|
|
77
|
+
try {
|
|
78
|
+
if (arg instanceof Error) return truncateText(`${arg.name}: ${arg.message}`, budget);
|
|
79
|
+
return boundedStringify(arg, budget);
|
|
80
|
+
} catch {
|
|
81
|
+
return "[Unserializable]";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* JSON-like text of `value`, stopping once roughly `budget` characters are
|
|
86
|
+
* written. Reads own data properties only, never getters or `toJSON`, and caps
|
|
87
|
+
* keys, elements and depth, so logging a store object costs bounded time and
|
|
88
|
+
* runs none of the page's code. A hostile object (a throwing Proxy) ends the
|
|
89
|
+
* walk with what was written so far.
|
|
90
|
+
*/
|
|
91
|
+
function boundedStringify(value, budget) {
|
|
92
|
+
let out = "";
|
|
93
|
+
const ancestors = [];
|
|
94
|
+
const full = () => out.length > budget;
|
|
95
|
+
const room = () => Math.max(0, budget - out.length) + 2;
|
|
96
|
+
/** A quoted string cut to the room left: escaping can grow it sixfold. */
|
|
97
|
+
const quote = (text) => truncateText(JSON.stringify(truncateText(text, room())), room());
|
|
98
|
+
const visit = (node, depth) => {
|
|
99
|
+
if (full()) return;
|
|
100
|
+
switch (typeof node) {
|
|
101
|
+
case "string":
|
|
102
|
+
out += quote(node);
|
|
103
|
+
return;
|
|
104
|
+
case "number":
|
|
105
|
+
case "boolean":
|
|
106
|
+
case "bigint":
|
|
107
|
+
case "symbol":
|
|
108
|
+
case "undefined":
|
|
109
|
+
out += truncateText(String(node), room());
|
|
110
|
+
return;
|
|
111
|
+
case "function":
|
|
112
|
+
out += "[Function]";
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (typeof node !== "object" || node === null) {
|
|
116
|
+
out += "null";
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (ancestors.includes(node)) {
|
|
120
|
+
out += "\"[Circular]\"";
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (node instanceof Date) {
|
|
124
|
+
out += Number.isNaN(node.getTime()) ? "null" : JSON.stringify(node.toISOString());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (depth >= MAX_DEPTH) {
|
|
128
|
+
out += Array.isArray(node) ? "[…]" : "{…}";
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
ancestors.push(node);
|
|
132
|
+
try {
|
|
133
|
+
if (Array.isArray(node)) visitArray(node, depth);
|
|
134
|
+
else if (isJsonObject(node)) visitObject(node, depth);
|
|
135
|
+
} catch {} finally {
|
|
136
|
+
ancestors.pop();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const visitArray = (node, depth) => {
|
|
140
|
+
out += "[";
|
|
141
|
+
try {
|
|
142
|
+
const count = Math.min(node.length, MAX_ARRAY_ELEMENTS);
|
|
143
|
+
for (let index = 0; index < count && !full(); index++) {
|
|
144
|
+
if (index > 0) out += ",";
|
|
145
|
+
visitProperty(node, String(index), depth + 1);
|
|
146
|
+
}
|
|
147
|
+
if (node.length > count && !full()) out += ",…";
|
|
148
|
+
} finally {
|
|
149
|
+
out += "]";
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/** An own property's value, without running a getter: one could have side effects. */
|
|
153
|
+
const visitProperty = (node, key, depth) => {
|
|
154
|
+
const descriptor = Object.getOwnPropertyDescriptor(node, key);
|
|
155
|
+
if (!descriptor) out += "undefined";
|
|
156
|
+
else visit("value" in descriptor ? descriptor.value : "[Getter]", depth);
|
|
157
|
+
};
|
|
158
|
+
const visitObject = (node, depth) => {
|
|
159
|
+
out += "{";
|
|
160
|
+
try {
|
|
161
|
+
let written = 0;
|
|
162
|
+
for (const key in node) {
|
|
163
|
+
if (full()) break;
|
|
164
|
+
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
|
|
165
|
+
if (written === MAX_OBJECT_KEYS) {
|
|
166
|
+
out += ",…";
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
out += `${written > 0 ? "," : ""}${quote(key)}:`;
|
|
170
|
+
visitProperty(node, key, depth + 1);
|
|
171
|
+
written++;
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
out += "}";
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
visit(value, 0);
|
|
179
|
+
} catch {}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
export { installConsoleCapture as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as
|
|
3
|
-
import { t as installConsoleCapture } from "./console-
|
|
1
|
+
import { E as ingestHeaders, H as consentAllowedSince, K as onConsentChange, M as sdkHint, P as selectorOf, U as consentRevokedAt, W as hasConsent, X as scrubUrl, Z as mayUseStorage, l as onSessionEvent } from "./sink-Ji0J42ul.mjs";
|
|
2
|
+
import { _ as Severity, a as attachLogSink, c as liveMapleTracer, d as SDK_VERSION, f as OtlpExporter, g as onErrorRecorded, h as claimPageSignal, i as onDocumentPageload, l as resourceAttributes, m as attachSpanStash, o as detachLogSink, p as newestFirstOnExit, r as navigationSpanAt, s as emitLog, u as SDK_NAME, v as severityOf } from "./src-JUynR8Jc.mjs";
|
|
3
|
+
import { t as installConsoleCapture } from "./console-iNhlufZV.mjs";
|
|
4
4
|
import { ROOT_CONTEXT, TraceFlags, context, trace } from "@opentelemetry/api";
|
|
5
5
|
import { JsonLogsSerializer, JsonTraceSerializer } from "@opentelemetry/otlp-transformer";
|
|
6
6
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
@@ -505,6 +505,19 @@ function startReports(emitLog, options) {
|
|
|
505
505
|
//#endregion
|
|
506
506
|
//#region ../sdk-core/src/browser/web-vitals.ts
|
|
507
507
|
let registered = false;
|
|
508
|
+
/**
|
|
509
|
+
* The path of the page view the metrics measure. web-vitals reports CLS, INP and
|
|
510
|
+
* LCP for the whole document (often when it is hidden, long after an SPA moved
|
|
511
|
+
* on), so the path is the document's, and a bfcache restore starts a new one.
|
|
512
|
+
*/
|
|
513
|
+
let pagePath = "";
|
|
514
|
+
function documentPath() {
|
|
515
|
+
const [entry] = performance.getEntriesByType?.("navigation") ?? [];
|
|
516
|
+
try {
|
|
517
|
+
if (entry?.name) return new URL(entry.name).pathname;
|
|
518
|
+
} catch {}
|
|
519
|
+
return location.pathname;
|
|
520
|
+
}
|
|
508
521
|
let emit;
|
|
509
522
|
let pageload;
|
|
510
523
|
function report(metric) {
|
|
@@ -519,7 +532,7 @@ function report(metric) {
|
|
|
519
532
|
"browser.web_vital.id": metric.id,
|
|
520
533
|
"browser.web_vital.rating": metric.rating,
|
|
521
534
|
"browser.web_vital.navigation_type": metric.navigationType,
|
|
522
|
-
"url.path":
|
|
535
|
+
"url.path": pagePath
|
|
523
536
|
},
|
|
524
537
|
link: pageload?.()
|
|
525
538
|
});
|
|
@@ -530,6 +543,10 @@ function startWebVitals(sink, getPageload) {
|
|
|
530
543
|
pageload = getPageload;
|
|
531
544
|
if (!registered) {
|
|
532
545
|
registered = true;
|
|
546
|
+
pagePath = scrubUrl(documentPath());
|
|
547
|
+
window.addEventListener("pageshow", (event) => {
|
|
548
|
+
if (event.persisted) pagePath = scrubUrl(location.pathname);
|
|
549
|
+
});
|
|
533
550
|
for (const on of [
|
|
534
551
|
onCLS,
|
|
535
552
|
onFCP,
|
|
@@ -595,7 +612,16 @@ function startLogs(config, stashOffline) {
|
|
|
595
612
|
ingestKey: config.ingestKey,
|
|
596
613
|
sdk: sdkHint(SDK_NAME, SDK_VERSION)
|
|
597
614
|
}), JsonLogsSerializer);
|
|
598
|
-
|
|
615
|
+
let leaving = false;
|
|
616
|
+
const exporter = new ConsentLogExporter(newestFirstOnExit(stashOffline ? new OfflineLogExporter(otlp, stashOffline) : otlp, JsonLogsSerializer, 0, () => leaving));
|
|
617
|
+
const onPageHide = () => {
|
|
618
|
+
leaving = true;
|
|
619
|
+
provider.forceFlush().catch(() => {}).finally(() => {
|
|
620
|
+
leaving = false;
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
const canListen = typeof window !== "undefined" && typeof window.addEventListener === "function";
|
|
624
|
+
if (canListen) window.addEventListener("pagehide", onPageHide);
|
|
599
625
|
const provider = new LoggerProvider({
|
|
600
626
|
resource: resourceFromAttributes(resourceAttributes(config)),
|
|
601
627
|
processors: [new BatchLogRecordProcessor({
|
|
@@ -614,6 +640,7 @@ function startLogs(config, stashOffline) {
|
|
|
614
640
|
context: record.link ? trace.setSpanContext(ROOT_CONTEXT, record.link) : ROOT_CONTEXT
|
|
615
641
|
}));
|
|
616
642
|
return async () => {
|
|
643
|
+
if (canListen) window.removeEventListener("pagehide", onPageHide);
|
|
617
644
|
detachLogSink();
|
|
618
645
|
await provider.shutdown();
|
|
619
646
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -283,9 +283,15 @@ interface CaptureExceptionOptions {
|
|
|
283
283
|
//#endregion
|
|
284
284
|
//#region src/init.d.ts
|
|
285
285
|
interface MapleBrowserHandle {
|
|
286
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* Empty until consent is granted when `requireConsent` is enabled, and while
|
|
288
|
+
* a previous `shutdown()` is still flushing.
|
|
289
|
+
*/
|
|
287
290
|
readonly sessionId: string;
|
|
288
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Tear down tracing + session capture, flushing the final buffers. The SDK
|
|
293
|
+
* is released at once: an `init()` right after it gets a new, live handle.
|
|
294
|
+
*/
|
|
289
295
|
readonly shutdown: () => Promise<void>;
|
|
290
296
|
}
|
|
291
297
|
//#endregion
|
|
@@ -321,10 +327,11 @@ export interface MapleBrowserApi {
|
|
|
321
327
|
*/
|
|
322
328
|
track: (name: string, props?: TrackProps) => void;
|
|
323
329
|
/**
|
|
324
|
-
* Report an error your app already caught
|
|
330
|
+
* Report an error your app already caught: the case the global handlers
|
|
325
331
|
* cannot see, because catching it is what stops it reaching them. A
|
|
326
332
|
* framework error boundary is the canonical caller. The same error object
|
|
327
|
-
* is recorded once, even if it is rethrown afterwards.
|
|
333
|
+
* is recorded once, even if it is rethrown afterwards. Safe before `init`:
|
|
334
|
+
* calls are held (capped) and recorded when it runs.
|
|
328
335
|
*
|
|
329
336
|
* BOUNDARY: a thrown value is unparsed by definition — JavaScript can throw
|
|
330
337
|
* anything. `captureException` narrows it before it reaches a span.
|
|
@@ -340,7 +347,7 @@ export interface MapleBrowserApi {
|
|
|
340
347
|
* Run data loading, like a route loader, in a span under the current navigation.
|
|
341
348
|
* Errors are recorded once and rethrown. Only requests started before `fn`'s first `await` nest under the span.
|
|
342
349
|
*/
|
|
343
|
-
traced: <T>(name: string, fn: () => Promise<T>, options?: TracedOptions) => Promise<T>;
|
|
350
|
+
traced: <T>(name: string, fn: () => T | Promise<T>, options?: TracedOptions) => Promise<T>;
|
|
344
351
|
/**
|
|
345
352
|
* Structured logs, exported as OpenTelemetry log records linked to the active
|
|
346
353
|
* span and the session. Safe before `init`: records queue until it runs.
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as MapleBrowser } from "./src-
|
|
1
|
+
import { t as MapleBrowser } from "./src-JUynR8Jc.mjs";
|
|
2
2
|
export { MapleBrowser };
|
package/dist/react.d.mts
CHANGED
|
@@ -24,14 +24,17 @@ export declare class MapleErrorBoundary extends Component<MapleErrorBoundaryProp
|
|
|
24
24
|
private readonly reset;
|
|
25
25
|
render(): ReactNode;
|
|
26
26
|
}
|
|
27
|
+
type ReactRootErrorHandler = (error: unknown, info: {
|
|
28
|
+
readonly componentStack?: string | null | undefined;
|
|
29
|
+
}) => void;
|
|
27
30
|
/**
|
|
28
31
|
* For React 19's `createRoot(el, { onCaughtError, onUncaughtError, onRecoverableError })`:
|
|
29
32
|
* reports what React caught, with its component stack. Errors already reported
|
|
30
|
-
* (by a `MapleErrorBoundary`, say) are not reported twice.
|
|
33
|
+
* (by a `MapleErrorBoundary`, say) are not reported twice. Passing a handler
|
|
34
|
+
* replaces React's own console logging, so the error is then handed to `next`,
|
|
35
|
+
* which defaults to `console.error`.
|
|
31
36
|
*/
|
|
32
|
-
export declare function mapleReactErrorHandler(source?: string
|
|
33
|
-
readonly componentStack?: string | null | undefined;
|
|
34
|
-
}) => void;
|
|
37
|
+
export declare function mapleReactErrorHandler(source?: string, next?: ReactRootErrorHandler): ReactRootErrorHandler;
|
|
35
38
|
/** A React Router data router (`createBrowserRouter` and friends), as far as Maple reads it. */
|
|
36
39
|
export interface ReactRouterLike {
|
|
37
40
|
readonly state: ReactRouterState;
|
package/dist/react.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as MapleBrowser } from "./src-
|
|
1
|
+
import { n as interruptNavigation, t as MapleBrowser } from "./src-JUynR8Jc.mjs";
|
|
2
2
|
import { Component } from "react";
|
|
3
3
|
//#region src/react.ts
|
|
4
4
|
/** Component stacks can run to hundreds of lines; the top of it names the failing tree. */
|
|
@@ -46,10 +46,35 @@ var MapleErrorBoundary = class extends Component {
|
|
|
46
46
|
/**
|
|
47
47
|
* For React 19's `createRoot(el, { onCaughtError, onUncaughtError, onRecoverableError })`:
|
|
48
48
|
* reports what React caught, with its component stack. Errors already reported
|
|
49
|
-
* (by a `MapleErrorBoundary`, say) are not reported twice.
|
|
49
|
+
* (by a `MapleErrorBoundary`, say) are not reported twice. Passing a handler
|
|
50
|
+
* replaces React's own console logging, so the error is then handed to `next`,
|
|
51
|
+
* which defaults to `console.error`.
|
|
50
52
|
*/
|
|
51
|
-
function mapleReactErrorHandler(source = "react.root") {
|
|
52
|
-
return (error, info) =>
|
|
53
|
+
function mapleReactErrorHandler(source = "react.root", next = (error) => console.error(error)) {
|
|
54
|
+
return (error, info) => {
|
|
55
|
+
reportReactError(error, info.componentStack, source);
|
|
56
|
+
next(error, info);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const attachedRouters = /* @__PURE__ */ new WeakMap();
|
|
60
|
+
function attachOnce(router, attach) {
|
|
61
|
+
const entry = attachedRouters.get(router) ?? {
|
|
62
|
+
count: 0,
|
|
63
|
+
stop: attach()
|
|
64
|
+
};
|
|
65
|
+
entry.count += 1;
|
|
66
|
+
attachedRouters.set(router, entry);
|
|
67
|
+
let released = false;
|
|
68
|
+
return () => {
|
|
69
|
+
if (released) return;
|
|
70
|
+
released = true;
|
|
71
|
+
entry.count -= 1;
|
|
72
|
+
queueMicrotask(() => {
|
|
73
|
+
if (entry.count > 0 || attachedRouters.get(router) !== entry) return;
|
|
74
|
+
attachedRouters.delete(router);
|
|
75
|
+
entry.stop();
|
|
76
|
+
});
|
|
77
|
+
};
|
|
53
78
|
}
|
|
54
79
|
/** `/projects/:id/settings`, from the matched routes' own path segments. */
|
|
55
80
|
function reactRouterTemplate(state) {
|
|
@@ -67,6 +92,9 @@ function reactRouterTemplate(state) {
|
|
|
67
92
|
* page load. Returns an unsubscribe.
|
|
68
93
|
*/
|
|
69
94
|
function instrumentReactRouter(router) {
|
|
95
|
+
return attachOnce(router, () => attachReactRouter(router));
|
|
96
|
+
}
|
|
97
|
+
function attachReactRouter(router) {
|
|
70
98
|
let open = false;
|
|
71
99
|
let pathname = router.state.location.pathname;
|
|
72
100
|
const start = (path) => {
|
|
@@ -80,7 +108,7 @@ function instrumentReactRouter(router) {
|
|
|
80
108
|
};
|
|
81
109
|
start(pathname);
|
|
82
110
|
if (router.state.initialized && router.state.navigation.state === "idle") end(router.state);
|
|
83
|
-
|
|
111
|
+
const unsubscribe = router.subscribe((state) => {
|
|
84
112
|
const target = state.navigation.location?.pathname;
|
|
85
113
|
if (state.navigation.state !== "idle" && target !== void 0) {
|
|
86
114
|
if (target !== pathname) start(target);
|
|
@@ -92,6 +120,11 @@ function instrumentReactRouter(router) {
|
|
|
92
120
|
pathname = state.location.pathname;
|
|
93
121
|
end(state);
|
|
94
122
|
});
|
|
123
|
+
return () => {
|
|
124
|
+
unsubscribe();
|
|
125
|
+
if (open) interruptNavigation();
|
|
126
|
+
open = false;
|
|
127
|
+
};
|
|
95
128
|
}
|
|
96
129
|
/**
|
|
97
130
|
* Span each TanStack Router navigation from `onBeforeNavigate` to `onResolved`,
|
|
@@ -99,6 +132,9 @@ function instrumentReactRouter(router) {
|
|
|
99
132
|
* changes are not navigations. The first is the page load. Returns an unsubscribe.
|
|
100
133
|
*/
|
|
101
134
|
function instrumentTanStackRouter(router) {
|
|
135
|
+
return attachOnce(router, () => attachTanStackRouter(router));
|
|
136
|
+
}
|
|
137
|
+
function attachTanStackRouter(router) {
|
|
102
138
|
/** The path of the navigation in flight, if any. */
|
|
103
139
|
let open;
|
|
104
140
|
const template = () => {
|
|
@@ -124,6 +160,8 @@ function instrumentTanStackRouter(router) {
|
|
|
124
160
|
return () => {
|
|
125
161
|
stopBefore();
|
|
126
162
|
stopResolved();
|
|
163
|
+
if (open !== void 0) interruptNavigation();
|
|
164
|
+
open = void 0;
|
|
127
165
|
};
|
|
128
166
|
}
|
|
129
167
|
//#endregion
|