@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 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** | ~36 kB | every page load, before any sampling decision |
76
- | ↳ our code alone | ~13 kB | the marginal cost if your app already ships OpenTelemetry |
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 };