@maple-dev/browser 0.9.0 → 0.10.1

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,27 @@ 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
+ `fetch`/XHR responses with a 4xx or 5xx status are errors, typed by status, as the HTTP semantic
72
+ conventions say for client spans. Narrow that with `errors: { captureHttpStatus: [[500, 599]] }`.
73
+ A network failure is always an error.
74
+
75
+ ### Breadcrumbs
76
+
77
+ The last 50 clicks, inputs, navigations and console lines are kept in memory and exported, as OTel
78
+ log records linked to the error's span, only when an error is recorded. `breadcrumbs: false` turns
79
+ this off; `logs: { captureConsole: ["warn", "error"] }` sends those console levels as logs right away.
80
+
69
81
  ## Bundle size
70
82
 
71
83
  Bundled, minified and gzipped, as your bundler would ship it:
72
84
 
73
85
  | | gzipped | what it is |
74
86
  | ---------------- | ------- | --------------------------------------------------------- |
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 |
87
+ | **eager** | ~40 kB | every page load, before any sampling decision |
88
+ | ↳ our code alone | ~16 kB | the marginal cost if your app already ships OpenTelemetry |
89
+ | **deferred** | ~9 kB | logs SDK, Web Vitals, document timing, after `init()` |
77
90
  | **lazy** | ~61 kB | rrweb — downloaded only by sessions sampled into replay |
78
91
 
79
92
  The eager figure is ~90% OpenTelemetry. If your app already uses the OTel web
@@ -118,6 +131,33 @@ a separate analytics silo. Calls before `init()` finishes are queued.
118
131
  MapleBrowser.track("checkout_completed", { plan: "pro", seats: 12 })
119
132
  ```
120
133
 
134
+ ## Logs
135
+
136
+ `MapleBrowser.logger` writes OpenTelemetry log records, linked to the active span and the session.
137
+ Calls before `init()` are queued.
138
+
139
+ ```ts
140
+ MapleBrowser.logger.info("checkout started", { "cart.items": 3 })
141
+ ```
142
+
143
+ ## Web Vitals
144
+
145
+ LCP, CLS, INP, FCP and TTFB are reported as `browser.web_vital` OpenTelemetry log events
146
+ (browser semantic conventions), linked to the `pageload` span and the session. Opt out with
147
+ `webVitals: false`.
148
+
149
+ ## Replay on error
150
+
151
+ `replay: { sampleRate: 0.05, onErrorSampleRate: 1 }` records 5% of sessions, and has every other
152
+ session keep the last minute in memory, uploading it and recording the rest of the session only if
153
+ an error is recorded.
154
+
155
+ ## Trace sampling
156
+
157
+ `tracing: { sampleRate: 0.25 }` exports the traces of ~25% of sessions. The decision is per
158
+ session, so a sampled session keeps all of its traces. Reported errors (uncaught errors, unhandled
159
+ rejections, `captureException`) are always exported.
160
+
121
161
  ## Regions
122
162
 
123
163
  Maple runs separate US and EU instances, and an ingest key only works in the
@@ -127,7 +167,7 @@ explicit `endpoint` (a proxy, or self-hosted ingest) always wins over `region`.
127
167
 
128
168
  ## Tracing across origins
129
169
 
130
- `fetch` spans carry the W3C `traceparent` header to same-origin requests only.
170
+ `fetch` and XHR spans carry the W3C `traceparent` header to same-origin requests only.
131
171
  When your API lives on another origin, list it so browser and backend spans
132
172
  join one trace, and allow the `traceparent` header in the API's CORS policy:
133
173
 
@@ -138,6 +178,49 @@ MapleBrowser.init({
138
178
  })
139
179
  ```
140
180
 
181
+ ## Navigations and data loading
182
+
183
+ Call these from your router's hooks to make one click one trace: a navigation
184
+ span, the data-loading spans under it, and the `fetch` spans they start.
185
+ [maple.dev/docs/frontend](https://maple.dev/docs/frontend) shows where for each
186
+ framework.
187
+
188
+ ```ts
189
+ MapleBrowser.startNavigation(location.pathname) // a navigation starts
190
+ const data = await MapleBrowser.traced("loader /projects/:id", () => load(id), {
191
+ isFailure: (error) => !isRedirect(error), // redirects aren't failures
192
+ })
193
+ MapleBrowser.endNavigation("/projects/:id") // the route is ready: its template
194
+ ```
195
+
196
+ - The first `startNavigation` opens a `pageload` span, joined to the server
197
+ render's trace from a `Server-Timing: traceparent;desc="…"` entry or a
198
+ `<meta name="traceparent">` tag; later calls open `navigate` spans, and end
199
+ one still open as `app.navigation.interrupted`.
200
+ - The document's `pageload` starts at navigation start, and gets child spans
201
+ from the Navigation Timing entry once the page has loaded: `documentFetch`
202
+ (with `dns`, `connect`, `request` and `response` under it), `domProcessing`
203
+ and `loadEvent`.
204
+ - `traced` returns `fn`'s result and rethrows its error unchanged. Only requests
205
+ started before `fn`'s first `await` nest under its span. An error it recorded
206
+ isn't reported again by `captureException` or the global handlers.
207
+ - All three are no-ops on the server, before `init()`, with tracing disabled or
208
+ without consent (`traced` then only runs `fn`).
209
+
210
+ ## React
211
+
212
+ `@maple-dev/browser/react` has `MapleErrorBoundary`, `mapleReactErrorHandler()` for React 19's
213
+ `createRoot` error options, and router adapters that call `startNavigation`/`endNavigation` for you:
214
+
215
+ ```ts
216
+ import { instrumentReactRouter, instrumentTanStackRouter } from "@maple-dev/browser/react"
217
+
218
+ instrumentReactRouter(createBrowserRouter(routes)) // navigate /projects/:id
219
+ instrumentTanStackRouter(router) // navigate /projects/$projectId
220
+ ```
221
+
222
+ Attach them after `MapleBrowser.init`: a navigation reported earlier uses up the page load.
223
+
141
224
  ## Linking a marketing site to your app
142
225
 
143
226
  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 };