@maple-dev/browser 0.4.0 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Maple Tech Labs
3
+ Copyright (c) 2026 Makisuo, Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -19,6 +19,7 @@ import { MapleBrowser } from "@maple-dev/browser"
19
19
  MapleBrowser.init({
20
20
  ingestKey: "maple_pk_...", // public ingest key
21
21
  serviceName: "acme-web",
22
+ region: "eu", // "us" (default) or "eu"; must match your organization's region
22
23
  environment: "production",
23
24
  replay: { enabled: true, sampleRate: 1.0 },
24
25
  privacy: { maskAllInputs: true },
@@ -33,9 +34,55 @@ That single call:
33
34
  gzipping them with the native `CompressionStream`, and uploading to
34
35
  `POST /v1/sessionReplays/blob`. rrweb ships in a lazy code-split chunk loaded
35
36
  only once a session is sampled in, so a `sampleRate` below 1 costs the
36
- unsampled visitors nothing beyond the ~8 kB gzipped base SDK;
37
+ unsampled visitors nothing beyond the base SDK (see [Bundle size](#bundle-size));
37
38
  - writes session metadata at start (`active`) and on page hide (`ended`),
38
- including the trace ids observed during the session.
39
+ including the trace ids observed during the session;
40
+ - captures uncaught errors and unhandled promise rejections as error spans, so
41
+ browser crashes reach Maple's error tracking.
42
+
43
+ ## Errors
44
+
45
+ Every uncaught error and unhandled rejection becomes a span with status `Error`
46
+ and an `exception` event, which is the shape Maple fingerprints — so browser
47
+ crashes group beside your server-side errors instead of in a silo.
48
+
49
+ Errors your app _catches_ never reach the global handlers, because catching them
50
+ is what stops them. Report those explicitly:
51
+
52
+ ```ts
53
+ try {
54
+ render()
55
+ } catch (error) {
56
+ MapleBrowser.captureException(error, { name: "browser.render_error" })
57
+ }
58
+ ```
59
+
60
+ Opt out of the global handlers with `tracing: { captureErrors: false }` — worth
61
+ doing only when another tracker already owns them, or the same crash is recorded
62
+ twice.
63
+
64
+ A cross-origin script reports to the browser as a bare `"Script error."` with no
65
+ stack and no filename. Those are dropped rather than recorded: they all
66
+ fingerprint to one contentless issue that buries the real ones. Add
67
+ `crossorigin` to the script tag to get the real error instead.
68
+
69
+ ## Bundle size
70
+
71
+ Bundled, minified and gzipped, as your bundler would ship it:
72
+
73
+ | | gzipped | what it is |
74
+ | ---------------- | ------- | --------------------------------------------------------- |
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 |
77
+ | **lazy** | ~61 kB | rrweb — downloaded only by sessions sampled into replay |
78
+
79
+ The eager figure is ~90% OpenTelemetry. If your app already uses the OTel web
80
+ SDK, your bundler should dedupe it and you pay closer to the second row; if it
81
+ doesn't dedupe, you will ship two copies, so pin matching versions.
82
+
83
+ Run `bun run size` in this package for the current numbers. It fails past a
84
+ budget, so a regression has to be argued for in review rather than discovered
85
+ in production.
39
86
 
40
87
  ## Identifying users
41
88
 
@@ -71,6 +118,26 @@ a separate analytics silo. Calls before `init()` finishes are queued.
71
118
  MapleBrowser.track("checkout_completed", { plan: "pro", seats: 12 })
72
119
  ```
73
120
 
121
+ ## Regions
122
+
123
+ Maple runs separate US and EU instances, and an ingest key only works in the
124
+ region it was created in. Set `region: "eu"` for an organization on
125
+ `app.eu.maple.dev`; the SDK then sends to `https://ingest.eu.maple.dev`. An
126
+ explicit `endpoint` (a proxy, or self-hosted ingest) always wins over `region`.
127
+
128
+ ## Tracing across origins
129
+
130
+ `fetch` spans carry the W3C `traceparent` header to same-origin requests only.
131
+ When your API lives on another origin, list it so browser and backend spans
132
+ join one trace, and allow the `traceparent` header in the API's CORS policy:
133
+
134
+ ```ts
135
+ MapleBrowser.init({
136
+ // ...
137
+ tracing: { propagateTraceHeaderCorsUrls: [/^https:\/\/api\.example\.com\//] },
138
+ })
139
+ ```
140
+
74
141
  ## Linking a marketing site to your app
75
142
 
76
143
  The visitor id lives in localStorage **and** a cookie scoped to your registered
@@ -85,6 +152,18 @@ stay per-origin; `VisitorId` is the join key. Override the scope with
85
152
  attribute hooks (`data-rr-block`, `.rr-block`, `.rr-ignore`) to block elements
86
153
  or subtrees from capture.
87
154
 
155
+ URLs are redacted before they leave the page: the values of credential-shaped
156
+ query and fragment parameters (`token`, `code`, `access_token`, `password`, …)
157
+ become `REDACTED` in session rows, events, network events, replay meta events
158
+ and span attributes. Add your own rewriting with `privacy.sanitizeUrl`, for
159
+ example to collapse ids in paths:
160
+
161
+ ```ts
162
+ privacy: {
163
+ sanitizeUrl: (url) => url.replace(/\/users\/\d+/, "/users/:id")
164
+ }
165
+ ```
166
+
88
167
  `privacy.requireConsent` holds all capture until `MapleBrowser.setConsent(true)`.
89
168
  Global Privacy Control is honored by default and suppresses the persistent
90
169
  visitor id; `doNotTrack` is not, unless `privacy.respectDoNotTrack` is set.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../browser-session/src/identity.d.ts
1
+ //#region ../browser-session/src/identity/identity.d.ts
2
2
  /**
3
3
  * Who the current visitor is.
4
4
  *
@@ -26,10 +26,14 @@ interface MapleIdentity {
26
26
  */
27
27
  type IdentifyInput = string | null | undefined | MapleIdentity;
28
28
  //#endregion
29
- //#region ../browser-session/src/track.d.ts
29
+ //#region ../browser-session/src/events/props.d.ts
30
30
  /** Properties a host app may attach to a custom event. */
31
31
  type TrackProps = Readonly<Record<string, unknown>>;
32
32
  //#endregion
33
+ //#region ../browser-session/src/platform/region.d.ts
34
+ /** A Maple hosting region. Each is a separate instance with its own ingest keys. */
35
+ type MapleRegion = "us" | "eu";
36
+ //#endregion
33
37
  //#region src/config.d.ts
34
38
  /** Public configuration for `MapleBrowser.init`. */
35
39
  interface MapleBrowserConfig {
@@ -37,7 +41,13 @@ interface MapleBrowserConfig {
37
41
  readonly ingestKey: string;
38
42
  /** Service name reported on traces and stored on replay sessions. */
39
43
  readonly serviceName: string;
40
- /** Maple ingest base URL. Defaults to `https://ingest.maple.dev`. */
44
+ /**
45
+ * Region your Maple organization lives in: `"us"` (default,
46
+ * `https://ingest.maple.dev`) or `"eu"` (`https://ingest.eu.maple.dev`).
47
+ * Ingest keys belong to one region. Ignored when `endpoint` is set.
48
+ */
49
+ readonly region?: MapleRegion;
50
+ /** Maple ingest base URL. Overrides `region`; use it for a proxy or self-hosted ingest. */
41
51
  readonly endpoint?: string;
42
52
  /**
43
53
  * Logical group this service belongs to, emitted as the OTel
@@ -67,6 +77,19 @@ interface MapleBrowserConfig {
67
77
  * sink, and disabling this avoids redundant duplicate network spans.
68
78
  */
69
79
  readonly instrumentFetch?: boolean;
80
+ /**
81
+ * Capture uncaught errors and unhandled promise rejections as error
82
+ * spans. Default true. Turn off only when another tracker already owns
83
+ * the page's global error handlers, or the same crash lands twice.
84
+ */
85
+ readonly captureErrors?: boolean;
86
+ /**
87
+ * Cross-origin URLs whose `fetch()` requests carry the W3C `traceparent`
88
+ * header, so the browser span and your backend's span join one trace.
89
+ * Same-origin requests always carry it. Your API must allow the
90
+ * `traceparent` header in CORS. Example: `[/^https:\/\/api\.example\.com\//]`.
91
+ */
92
+ readonly propagateTraceHeaderCorsUrls?: ReadonlyArray<string | RegExp>;
70
93
  };
71
94
  readonly replay?: {
72
95
  /** Default true. */
@@ -107,9 +130,25 @@ interface MapleBrowserConfig {
107
130
  readonly captureUserEmail?: boolean;
108
131
  /** Treat `navigator.doNotTrack` like Global Privacy Control. Default false. */
109
132
  readonly respectDoNotTrack?: boolean;
133
+ /**
134
+ * Rewrite every URL before it leaves the page: session entry and exit
135
+ * URLs, event rows, network events, replay meta events, and span
136
+ * attributes. Runs after the built-in redaction, which already replaces
137
+ * the values of credential-shaped query and fragment parameters
138
+ * (`token`, `code`, `access_token`, `password`, …).
139
+ */
140
+ readonly sanitizeUrl?: (url: string) => string;
110
141
  };
111
142
  }
112
143
  //#endregion
144
+ //#region src/errors.d.ts
145
+ interface CaptureExceptionOptions {
146
+ /** Span name. Default `"exception"`. */
147
+ readonly name?: string | undefined;
148
+ /** Extra span attributes. */
149
+ readonly attributes?: Record<string, string | number | boolean> | undefined;
150
+ }
151
+ //#endregion
113
152
  //#region src/init.d.ts
114
153
  interface MapleBrowserHandle {
115
154
  /** Empty until consent is granted when `requireConsent` is enabled. */
@@ -119,6 +158,34 @@ interface MapleBrowserHandle {
119
158
  }
120
159
  //#endregion
121
160
  //#region src/index.d.ts
161
+ /** The `MapleBrowser` namespace object. */
162
+ interface MapleBrowserApi {
163
+ init: (config: MapleBrowserConfig) => MapleBrowserHandle;
164
+ /**
165
+ * Attach, replace, or clear the end-user identity on the active session.
166
+ * Accepts a bare user id or the full identity object. Safe to call repeatedly,
167
+ * and before `init` (the latest call is applied when `init` runs).
168
+ */
169
+ identify: (input?: IdentifyInput) => void;
170
+ /**
171
+ * Record a custom product event against the active session. Safe to call
172
+ * before `init` — events are queued (capped) and drained once the session
173
+ * starts.
174
+ */
175
+ track: (name: string, props?: TrackProps) => void;
176
+ /**
177
+ * Report an error your app already caught — the case the global handlers
178
+ * cannot see, because catching it is what stops it reaching them. A
179
+ * framework error boundary is the canonical caller. The same error object
180
+ * is recorded once, even if it is rethrown afterwards.
181
+ *
182
+ * BOUNDARY: a thrown value is unparsed by definition — JavaScript can throw
183
+ * anything. `captureException` narrows it before it reaches a span.
184
+ */
185
+ captureException: (error: unknown, options?: CaptureExceptionOptions) => void;
186
+ /** Grant or revoke consent when `privacy.requireConsent` is on. */
187
+ setConsent: (granted: boolean) => void;
188
+ }
122
189
  /**
123
190
  * Maple browser SDK. One call wires up OpenTelemetry tracing and rrweb session
124
191
  * replay, both tagged with a shared session id.
@@ -130,27 +197,13 @@ interface MapleBrowserHandle {
130
197
  * MapleBrowser.init({
131
198
  * ingestKey: "maple_pk_...",
132
199
  * serviceName: "acme-web",
200
+ * region: "eu", // omit for the US region
133
201
  * })
134
202
  *
135
203
  * MapleBrowser.identify({ id: user.id, email: user.email, groupId: org.id, groupName: org.name })
136
204
  * MapleBrowser.track("checkout_completed", { plan: "pro", seats: 5 })
137
205
  * ```
138
206
  */
139
- declare const MapleBrowser: {
140
- init: (config: MapleBrowserConfig) => MapleBrowserHandle;
141
- /**
142
- * Attach, replace, or clear the end-user identity on the active session.
143
- * Accepts a bare user id or the full identity object. Safe to call repeatedly.
144
- */
145
- identify: (input?: IdentifyInput) => void;
146
- /**
147
- * Record a custom product event against the active session. Safe to call
148
- * before `init` — events are queued (capped) and drained once the session
149
- * starts.
150
- */
151
- track: (name: string, props?: TrackProps) => void;
152
- /** Grant or revoke consent when `privacy.requireConsent` is on. */
153
- setConsent: (granted: boolean) => void;
154
- };
207
+ declare const MapleBrowser: MapleBrowserApi;
155
208
  //#endregion
156
- export { type IdentifyInput, MapleBrowser, type MapleBrowserConfig, type MapleBrowserHandle, type MapleIdentity, type TrackProps, type TraitValue };
209
+ export { type CaptureExceptionOptions, type IdentifyInput, MapleBrowser, MapleBrowserApi, type MapleBrowserConfig, type MapleBrowserHandle, type MapleIdentity, type MapleRegion, type TrackProps, type TraitValue };