@maple-dev/browser 0.9.0 → 0.10.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 +85 -3
- package/dist/console-BrjBnOiX.mjs +73 -0
- package/dist/deferred-CdtlxG0Q.mjs +723 -0
- package/dist/index.d.mts +164 -14
- package/dist/index.mjs +1 -854
- package/dist/react.d.mts +88 -0
- package/dist/react.mjs +130 -0
- package/dist/replay-session-DdVYdHc_.mjs +527 -0
- package/dist/{sink-DHPiMgU0.mjs → sink-BOR9_z4b.mjs} +293 -104
- package/dist/src-Bzmp8rVr.mjs +1722 -0
- package/package.json +33 -11
- package/dist/replay-session-gSlAYwqV.mjs +0 -360
package/README.md
CHANGED
|
@@ -66,14 +66,26 @@ stack and no filename. Those are dropped rather than recorded: they all
|
|
|
66
66
|
fingerprint to one contentless issue that buries the real ones. Add
|
|
67
67
|
`crossorigin` to the script tag to get the real error instead.
|
|
68
68
|
|
|
69
|
+
### Failed HTTP requests
|
|
70
|
+
|
|
71
|
+
`errors: { captureHttpStatus: [[500, 599]] }` makes those `fetch`/XHR responses errors, typed by
|
|
72
|
+
status. By default a response status alone is not an error; a network failure always is.
|
|
73
|
+
|
|
74
|
+
### Breadcrumbs
|
|
75
|
+
|
|
76
|
+
The last 50 clicks, inputs, navigations and console lines are kept in memory and exported, as OTel
|
|
77
|
+
log records linked to the error's span, only when an error is recorded. `breadcrumbs: false` turns
|
|
78
|
+
this off; `logs: { captureConsole: ["warn", "error"] }` sends those console levels as logs right away.
|
|
79
|
+
|
|
69
80
|
## Bundle size
|
|
70
81
|
|
|
71
82
|
Bundled, minified and gzipped, as your bundler would ship it:
|
|
72
83
|
|
|
73
84
|
| | gzipped | what it is |
|
|
74
85
|
| ---------------- | ------- | --------------------------------------------------------- |
|
|
75
|
-
| **eager** | ~
|
|
76
|
-
| ↳ our code alone | ~
|
|
86
|
+
| **eager** | ~40 kB | every page load, before any sampling decision |
|
|
87
|
+
| ↳ our code alone | ~16 kB | the marginal cost if your app already ships OpenTelemetry |
|
|
88
|
+
| **deferred** | ~9 kB | logs SDK, Web Vitals, document timing, after `init()` |
|
|
77
89
|
| **lazy** | ~61 kB | rrweb — downloaded only by sessions sampled into replay |
|
|
78
90
|
|
|
79
91
|
The eager figure is ~90% OpenTelemetry. If your app already uses the OTel web
|
|
@@ -118,6 +130,33 @@ a separate analytics silo. Calls before `init()` finishes are queued.
|
|
|
118
130
|
MapleBrowser.track("checkout_completed", { plan: "pro", seats: 12 })
|
|
119
131
|
```
|
|
120
132
|
|
|
133
|
+
## Logs
|
|
134
|
+
|
|
135
|
+
`MapleBrowser.logger` writes OpenTelemetry log records, linked to the active span and the session.
|
|
136
|
+
Calls before `init()` are queued.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
MapleBrowser.logger.info("checkout started", { "cart.items": 3 })
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Web Vitals
|
|
143
|
+
|
|
144
|
+
LCP, CLS, INP, FCP and TTFB are reported as `browser.web_vital` OpenTelemetry log events
|
|
145
|
+
(browser semantic conventions), linked to the `pageload` span and the session. Opt out with
|
|
146
|
+
`webVitals: false`.
|
|
147
|
+
|
|
148
|
+
## Replay on error
|
|
149
|
+
|
|
150
|
+
`replay: { sampleRate: 0.05, onErrorSampleRate: 1 }` records 5% of sessions, and has every other
|
|
151
|
+
session keep the last minute in memory, uploading it and recording the rest of the session only if
|
|
152
|
+
an error is recorded.
|
|
153
|
+
|
|
154
|
+
## Trace sampling
|
|
155
|
+
|
|
156
|
+
`tracing: { sampleRate: 0.25 }` exports the traces of ~25% of sessions. The decision is per
|
|
157
|
+
session, so a sampled session keeps all of its traces. Reported errors (uncaught errors, unhandled
|
|
158
|
+
rejections, `captureException`) are always exported.
|
|
159
|
+
|
|
121
160
|
## Regions
|
|
122
161
|
|
|
123
162
|
Maple runs separate US and EU instances, and an ingest key only works in the
|
|
@@ -127,7 +166,7 @@ explicit `endpoint` (a proxy, or self-hosted ingest) always wins over `region`.
|
|
|
127
166
|
|
|
128
167
|
## Tracing across origins
|
|
129
168
|
|
|
130
|
-
`fetch` spans carry the W3C `traceparent` header to same-origin requests only.
|
|
169
|
+
`fetch` and XHR spans carry the W3C `traceparent` header to same-origin requests only.
|
|
131
170
|
When your API lives on another origin, list it so browser and backend spans
|
|
132
171
|
join one trace, and allow the `traceparent` header in the API's CORS policy:
|
|
133
172
|
|
|
@@ -138,6 +177,49 @@ MapleBrowser.init({
|
|
|
138
177
|
})
|
|
139
178
|
```
|
|
140
179
|
|
|
180
|
+
## Navigations and data loading
|
|
181
|
+
|
|
182
|
+
Call these from your router's hooks to make one click one trace: a navigation
|
|
183
|
+
span, the data-loading spans under it, and the `fetch` spans they start.
|
|
184
|
+
[maple.dev/docs/frontend](https://maple.dev/docs/frontend) shows where for each
|
|
185
|
+
framework.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
MapleBrowser.startNavigation(location.pathname) // a navigation starts
|
|
189
|
+
const data = await MapleBrowser.traced("loader /projects/:id", () => load(id), {
|
|
190
|
+
isFailure: (error) => !isRedirect(error), // redirects aren't failures
|
|
191
|
+
})
|
|
192
|
+
MapleBrowser.endNavigation("/projects/:id") // the route is ready: its template
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- The first `startNavigation` opens a `pageload` span, joined to the server
|
|
196
|
+
render's trace from a `Server-Timing: traceparent;desc="…"` entry or a
|
|
197
|
+
`<meta name="traceparent">` tag; later calls open `navigate` spans, and end
|
|
198
|
+
one still open as `app.navigation.interrupted`.
|
|
199
|
+
- The document's `pageload` starts at navigation start, and gets child spans
|
|
200
|
+
from the Navigation Timing entry once the page has loaded: `documentFetch`
|
|
201
|
+
(with `dns`, `connect`, `request` and `response` under it), `domProcessing`
|
|
202
|
+
and `loadEvent`.
|
|
203
|
+
- `traced` returns `fn`'s result and rethrows its error unchanged. Only requests
|
|
204
|
+
started before `fn`'s first `await` nest under its span. An error it recorded
|
|
205
|
+
isn't reported again by `captureException` or the global handlers.
|
|
206
|
+
- All three are no-ops on the server, before `init()`, with tracing disabled or
|
|
207
|
+
without consent (`traced` then only runs `fn`).
|
|
208
|
+
|
|
209
|
+
## React
|
|
210
|
+
|
|
211
|
+
`@maple-dev/browser/react` has `MapleErrorBoundary`, `mapleReactErrorHandler()` for React 19's
|
|
212
|
+
`createRoot` error options, and router adapters that call `startNavigation`/`endNavigation` for you:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { instrumentReactRouter, instrumentTanStackRouter } from "@maple-dev/browser/react"
|
|
216
|
+
|
|
217
|
+
instrumentReactRouter(createBrowserRouter(routes)) // navigate /projects/:id
|
|
218
|
+
instrumentTanStackRouter(router) // navigate /projects/$projectId
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Attach them after `MapleBrowser.init`: a navigation reported earlier uses up the page load.
|
|
222
|
+
|
|
141
223
|
## Linking a marketing site to your app
|
|
142
224
|
|
|
143
225
|
The visitor id lives in localStorage **and** a cookie scoped to your registered
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { O as safeEmit } from "./sink-BOR9_z4b.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
|
+
/**
|
|
14
|
+
* Capture `console.*` calls as session events. Wraps each method, emits a
|
|
15
|
+
* distilled record, then forwards to the original so the host app's console
|
|
16
|
+
* behaves normally. Never throws into the call site.
|
|
17
|
+
*/
|
|
18
|
+
function installConsoleCapture(emit) {
|
|
19
|
+
const original = {};
|
|
20
|
+
const wrappers = {};
|
|
21
|
+
let active = true;
|
|
22
|
+
for (const level of LEVELS) {
|
|
23
|
+
const orig = console[level];
|
|
24
|
+
original[level] = orig;
|
|
25
|
+
const wrapper = (...args) => {
|
|
26
|
+
if (active) safeEmit(emit, {
|
|
27
|
+
type: "console",
|
|
28
|
+
level,
|
|
29
|
+
message: formatArgs(args)
|
|
30
|
+
});
|
|
31
|
+
orig.apply(console, args);
|
|
32
|
+
};
|
|
33
|
+
wrappers[level] = wrapper;
|
|
34
|
+
console[level] = wrapper;
|
|
35
|
+
}
|
|
36
|
+
return () => {
|
|
37
|
+
active = false;
|
|
38
|
+
for (const level of LEVELS) {
|
|
39
|
+
const orig = original[level];
|
|
40
|
+
if (orig && console[level] === wrappers[level]) console[level] = orig;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function formatArgs(args) {
|
|
45
|
+
const text = args.map((a) => {
|
|
46
|
+
if (typeof a === "string") return a;
|
|
47
|
+
if (a instanceof Error) return `${a.name}: ${a.message}`;
|
|
48
|
+
try {
|
|
49
|
+
return boundedStringify(a, MAX_MESSAGE) ?? String(a);
|
|
50
|
+
} catch {
|
|
51
|
+
return String(a);
|
|
52
|
+
}
|
|
53
|
+
}).join(" ");
|
|
54
|
+
return text.length > MAX_MESSAGE ? `${text.slice(0, MAX_MESSAGE)}…` : text;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `JSON.stringify` that stops descending once roughly `budget` characters have
|
|
58
|
+
* been produced. The message is cut to `MAX_MESSAGE` anyway, and serializing a
|
|
59
|
+
* whole store object on every `console.log` first was a main-thread cost paid
|
|
60
|
+
* on the host app's hot path. A replacer returning `undefined` skips a value
|
|
61
|
+
* without visiting its children, so the walk is bounded by the budget.
|
|
62
|
+
*/
|
|
63
|
+
function boundedStringify(value, budget) {
|
|
64
|
+
let used = 0;
|
|
65
|
+
return JSON.stringify(value, (key, nested) => {
|
|
66
|
+
if (used > budget) return void 0;
|
|
67
|
+
used += key.length + (typeof nested === "string" ? nested.length : 4);
|
|
68
|
+
if (Array.isArray(nested) && nested.length > MAX_ARRAY_ELEMENTS) return nested.slice(0, MAX_ARRAY_ELEMENTS);
|
|
69
|
+
return nested;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { installConsoleCapture as t };
|