@cherrypeak-org/cherryboard-web 1.0.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/CHANGELOG.md +72 -0
- package/README.md +489 -0
- package/bin/cherryboard-upload-sourcemaps.mjs +138 -0
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +256 -0
- package/dist/index.d.ts +256 -0
- package/dist/index.mjs +6 -0
- package/dist/react/index.cjs +7 -0
- package/dist/react/index.d.cts +320 -0
- package/dist/react/index.d.ts +320 -0
- package/dist/react/index.mjs +7 -0
- package/package.json +79 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@cherrypeak-org/cherryboard-web` are documented here.
|
|
4
|
+
This project adheres to [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [1.0.1]
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **Source maps are no longer published.** 1.0.0 shipped `.map` files whose
|
|
10
|
+
`sourcesContent` embedded this package's full TypeScript source, so the
|
|
11
|
+
original code was readable by anyone who downloaded the tarball. Maps are now
|
|
12
|
+
disabled in the build entirely, which also avoids leaving dangling
|
|
13
|
+
`sourceMappingURL` comments. The tarball drops from 16 files to 12.
|
|
14
|
+
|
|
15
|
+
1.0.0 should be treated as containing published source and is unpublished.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [1.0.0]
|
|
20
|
+
|
|
21
|
+
First published release.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Source maps**: `cherryboard-upload-sourcemaps` CLI uploads a build's maps so the
|
|
25
|
+
backend resolves minified stacks into original files, lines and function names.
|
|
26
|
+
The `release` passed to `init()` must match the one uploaded.
|
|
27
|
+
- **Next.js server-side capture**: `captureRequestError` for `instrumentation.ts`,
|
|
28
|
+
covering RSC render, route handler and server action errors.
|
|
29
|
+
- **Discarded-event counts**: `getDiscardedEvents()` reports events dropped by
|
|
30
|
+
sampling, dedupe, filters, rate limiting or failed sends, so a missing error is
|
|
31
|
+
explainable instead of silently absent.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- `429` responses are no longer retried. The SDK honours `Retry-After` and pauses
|
|
35
|
+
sending until it elapses, per the Sentry SDK spec — retrying a rate limit only
|
|
36
|
+
deepens the backlog. Events keep buffering meanwhile.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Closing or unloading while rate-limited scheduled a self-renewing flush timer that
|
|
40
|
+
outlived the client.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## [0.1.0] — Unreleased
|
|
45
|
+
|
|
46
|
+
Initial release. Browser + React error-tracking client for the CherryBoard dashboard.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Automatic capture**: uncaught errors (`window` `error`), unhandled promise
|
|
51
|
+
rejections, failed resource loads (img/script/css), and `console.error` /
|
|
52
|
+
`console.warn` as breadcrumbs.
|
|
53
|
+
- **React layer** (`@cherrypeak-org/cherryboard-web/react`): `ErrorBoundary`,
|
|
54
|
+
`CherryBoardProvider`, `useCaptureError`, `useCherryBoard`, and
|
|
55
|
+
`captureRouteError` for the Next.js App Router `error.tsx` / `global-error.tsx`.
|
|
56
|
+
- **Manual API**: `init`, `captureException`, `captureMessage`, `addBreadcrumb`,
|
|
57
|
+
`setUser`, `setTag`, `setContext`, `flush`, `close`.
|
|
58
|
+
- **Delivery**: batched, wire-compatible with the CherryBoard `/api/v1/errors/batch`
|
|
59
|
+
ingest endpoint (`X-API-Key`), `fetch` with `keepalive` (survives page unload),
|
|
60
|
+
exponential-backoff retry (5xx / network only), a durable `localStorage` offline
|
|
61
|
+
queue, client-side rate limiting, and same-error deduplication.
|
|
62
|
+
- **Data quality**: breadcrumbs (navigation / click / fetch), `release` +
|
|
63
|
+
`environment` tags, browser/viewport context, `error.cause` chains, and React
|
|
64
|
+
component stacks.
|
|
65
|
+
- **Privacy**: built-in PII scrubbing (auth headers/tokens, emails, sensitive
|
|
66
|
+
query params and object keys) plus a `beforeSend` hook, applied before anything
|
|
67
|
+
leaves the browser.
|
|
68
|
+
- **SSR-safe**: no browser globals are touched at import time; safe to import from
|
|
69
|
+
React Server Components / the Next.js App Router. The React entry ships
|
|
70
|
+
`"use client"`.
|
|
71
|
+
- Framework-agnostic core with zero runtime dependencies; `react` / `react-dom`
|
|
72
|
+
are optional peer dependencies. Dual ESM + CJS builds with type declarations.
|
package/README.md
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# @cherrypeak-org/cherryboard-web
|
|
2
|
+
|
|
3
|
+
Browser & React error tracking for the **CherryBoard** dashboard. Drop it into any
|
|
4
|
+
React app (Next.js, Vite, CRA) and uncaught errors, promise rejections, and React
|
|
5
|
+
render errors are captured, batched, and shipped to your CherryBoard project —
|
|
6
|
+
where they show up as grouped issues alongside your backend errors.
|
|
7
|
+
|
|
8
|
+
- **Zero-config capture** — global errors, unhandled rejections, resource failures.
|
|
9
|
+
- **React-native** — `ErrorBoundary`, a provider, hooks, and Next.js App Router helpers.
|
|
10
|
+
- **Reliable** — batching, offline queue, retry, dedupe, rate-limiting, `keepalive`.
|
|
11
|
+
- **Private & safe** — PII scrubbing before send, SSR-safe, tiny, zero-dependency core.
|
|
12
|
+
- **Readable stacks** — upload source maps and see original files, lines and functions.
|
|
13
|
+
|
|
14
|
+
> Wire-compatible with the same `/api/v1/errors` ingest the .NET
|
|
15
|
+
> `CherryPeak.CherryBoard.Client` uses — one dashboard, both stacks.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Contents
|
|
20
|
+
|
|
21
|
+
1. [Install](#1-install)
|
|
22
|
+
2. [Get an ingest API key](#2-get-an-ingest-api-key)
|
|
23
|
+
3. [Quick start — Next.js (App Router)](#3-quick-start--nextjs-app-router)
|
|
24
|
+
4. [Quick start — Vite / CRA / plain React](#4-quick-start--vite--cra--plain-react)
|
|
25
|
+
5. [Reporting errors manually](#5-reporting-errors-manually)
|
|
26
|
+
6. [Identifying users & adding context](#6-identifying-users--adding-context)
|
|
27
|
+
7. [Configuration reference](#7-configuration-reference)
|
|
28
|
+
8. [What gets captured](#8-what-gets-captured)
|
|
29
|
+
9. [Security & privacy](#9-security--privacy-read-this)
|
|
30
|
+
10. [Troubleshooting](#10-troubleshooting)
|
|
31
|
+
11. [Readable stack traces (source maps)](#11-readable-stack-traces-source-maps)
|
|
32
|
+
12. [Server-side errors (Next.js)](#12-server-side-errors-nextjs)
|
|
33
|
+
13. [Why didn't my error show up?](#13-why-didnt-my-error-show-up)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @cherrypeak-org/cherryboard-web
|
|
41
|
+
# or: pnpm add / yarn add
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`react` and `react-dom` are **optional peer dependencies** — you only need them if
|
|
45
|
+
you import from `@cherrypeak-org/cherryboard-web/react`. The core works in any browser app.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Get an ingest API key
|
|
50
|
+
|
|
51
|
+
1. Open the CherryBoard dashboard → your **Project** → the **Environment** you want
|
|
52
|
+
errors filed under (e.g. *Production*).
|
|
53
|
+
2. Create an **API key** and copy it. This key resolves the project + environment
|
|
54
|
+
server-side, so front-end errors land in the right place automatically.
|
|
55
|
+
|
|
56
|
+
> ⚠️ This key ships in your browser bundle and is **public**. That's expected (it's
|
|
57
|
+
> how every browser error tracker works), but see [Security](#9-security--privacy-read-this)
|
|
58
|
+
> for how to keep it safe (write-only scope, CORS, rate limiting).
|
|
59
|
+
|
|
60
|
+
Put the key and API host in **public** env vars:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# .env.local (Next.js) — NEXT_PUBLIC_ vars are exposed to the browser
|
|
64
|
+
NEXT_PUBLIC_CHERRYBOARD_KEY=cpd_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
65
|
+
NEXT_PUBLIC_CHERRYBOARD_URL=https://<your-cherryboard-api-host>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# .env (Vite)
|
|
70
|
+
VITE_CHERRYBOARD_KEY=cpd_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
71
|
+
VITE_CHERRYBOARD_URL=https://<your-cherryboard-api-host>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`apiUrl` is the API host root — the SDK appends `/api/v1/errors/batch` for you.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 3. Quick start — Next.js (App Router)
|
|
79
|
+
|
|
80
|
+
Works with Next.js 13.4+ (App Router) including **Next.js 16 / React 19**.
|
|
81
|
+
|
|
82
|
+
### 3a. Initialize once, as early as possible
|
|
83
|
+
|
|
84
|
+
Create **`instrumentation-client.ts`** at your project root (or `src/`). Next.js runs
|
|
85
|
+
this on the client before your app hydrates — the ideal place to start the tracker.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// instrumentation-client.ts
|
|
89
|
+
import { init } from '@cherrypeak-org/cherryboard-web';
|
|
90
|
+
|
|
91
|
+
init({
|
|
92
|
+
apiKey: process.env.NEXT_PUBLIC_CHERRYBOARD_KEY!,
|
|
93
|
+
apiUrl: process.env.NEXT_PUBLIC_CHERRYBOARD_URL!,
|
|
94
|
+
environment: process.env.NEXT_PUBLIC_ENV ?? 'production',
|
|
95
|
+
release: process.env.NEXT_PUBLIC_COMMIT_SHA, // optional, recommended
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> On **older Next** without `instrumentation-client.ts`, use the
|
|
100
|
+
> [`<CherryBoardProvider>`](#3d-alternative-provider) instead.
|
|
101
|
+
|
|
102
|
+
### 3b. Report render errors from the App Router boundaries
|
|
103
|
+
|
|
104
|
+
Next catches render errors with `error.tsx` (per route) and `global-error.tsx` (root
|
|
105
|
+
layout). They only render a fallback — add one line to also report them:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// app/error.tsx
|
|
109
|
+
'use client';
|
|
110
|
+
import { useEffect } from 'react';
|
|
111
|
+
import { captureRouteError } from '@cherrypeak-org/cherryboard-web/react';
|
|
112
|
+
|
|
113
|
+
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
captureRouteError(error); // includes the Next.js `digest` for correlation
|
|
116
|
+
}, [error]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div>
|
|
120
|
+
<h2>Something went wrong.</h2>
|
|
121
|
+
<button onClick={reset}>Try again</button>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// app/global-error.tsx
|
|
129
|
+
'use client';
|
|
130
|
+
import { useEffect } from 'react';
|
|
131
|
+
import { captureRouteError } from '@cherrypeak-org/cherryboard-web/react';
|
|
132
|
+
|
|
133
|
+
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
captureRouteError(error);
|
|
136
|
+
}, [error]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<html>
|
|
140
|
+
<body>
|
|
141
|
+
<h2>Application error.</h2>
|
|
142
|
+
<button onClick={reset}>Reload</button>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
That's it — global errors, unhandled rejections, and route-level render errors are now
|
|
150
|
+
tracked.
|
|
151
|
+
|
|
152
|
+
### 3c. (Optional) Wrap a subtree in an ErrorBoundary
|
|
153
|
+
|
|
154
|
+
For a nicer fallback around a specific area (and to keep the rest of the page alive):
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
'use client';
|
|
158
|
+
import { ErrorBoundary } from '@cherrypeak-org/cherryboard-web/react';
|
|
159
|
+
|
|
160
|
+
export function Widget() {
|
|
161
|
+
return (
|
|
162
|
+
<ErrorBoundary fallback={({ reset }) => <button onClick={reset}>Reload widget</button>}>
|
|
163
|
+
<FlakyChart />
|
|
164
|
+
</ErrorBoundary>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 3d. Alternative: the provider
|
|
170
|
+
|
|
171
|
+
If you'd rather not use `instrumentation-client.ts`, initialize with the provider in
|
|
172
|
+
your root layout. It's a client component, so it's safe to render from the (server)
|
|
173
|
+
layout:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
// app/layout.tsx
|
|
177
|
+
import { CherryBoardProvider } from '@cherrypeak-org/cherryboard-web/react';
|
|
178
|
+
|
|
179
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
180
|
+
return (
|
|
181
|
+
<html lang="en">
|
|
182
|
+
<body>
|
|
183
|
+
<CherryBoardProvider
|
|
184
|
+
config={{
|
|
185
|
+
apiKey: process.env.NEXT_PUBLIC_CHERRYBOARD_KEY!,
|
|
186
|
+
apiUrl: process.env.NEXT_PUBLIC_CHERRYBOARD_URL!,
|
|
187
|
+
environment: process.env.NEXT_PUBLIC_ENV ?? 'production',
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{children}
|
|
191
|
+
</CherryBoardProvider>
|
|
192
|
+
</body>
|
|
193
|
+
</html>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
> **Amplify note:** on AWS Amplify, only env vars matching the build's allowlist reach
|
|
199
|
+
> the runtime. `NEXT_PUBLIC_*` vars are picked up automatically — make sure your key
|
|
200
|
+
> uses that prefix.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 4. Quick start — Vite / CRA / plain React
|
|
205
|
+
|
|
206
|
+
Call `init` once at your entry point, before rendering:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// main.tsx
|
|
210
|
+
import { init } from '@cherrypeak-org/cherryboard-web';
|
|
211
|
+
|
|
212
|
+
init({
|
|
213
|
+
apiKey: import.meta.env.VITE_CHERRYBOARD_KEY,
|
|
214
|
+
apiUrl: import.meta.env.VITE_CHERRYBOARD_URL,
|
|
215
|
+
environment: import.meta.env.MODE,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ...then ReactDOM.createRoot(...).render(<App />)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Wrap your tree in the boundary to catch render errors:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { ErrorBoundary } from '@cherrypeak-org/cherryboard-web/react';
|
|
225
|
+
|
|
226
|
+
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
|
|
227
|
+
<App />
|
|
228
|
+
</ErrorBoundary>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 5. Reporting errors manually
|
|
234
|
+
|
|
235
|
+
Error boundaries and global handlers can't see errors you catch yourself (in `try/catch`,
|
|
236
|
+
event handlers, or async code). Report those explicitly:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { captureException, captureMessage } from '@cherrypeak-org/cherryboard-web';
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await riskyThing();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
captureException(err, { context: { orderId, step: 'checkout' } });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Log a noteworthy non-error event
|
|
248
|
+
captureMessage('Payment provider returned an unexpected shape', 'Warning');
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
In components, the hook gives you a stable callback:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
'use client';
|
|
255
|
+
import { useCaptureError } from '@cherrypeak-org/cherryboard-web/react';
|
|
256
|
+
|
|
257
|
+
function SaveButton() {
|
|
258
|
+
const capture = useCaptureError();
|
|
259
|
+
const onClick = async () => {
|
|
260
|
+
try {
|
|
261
|
+
await save();
|
|
262
|
+
} catch (err) {
|
|
263
|
+
capture(err, { context: { feature: 'save' } });
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
return <button onClick={onClick}>Save</button>;
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 6. Identifying users & adding context
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { setUser, setTag, addBreadcrumb } from '@cherrypeak-org/cherryboard-web';
|
|
276
|
+
|
|
277
|
+
// After login — keep it to an opaque id; do NOT pass emails/names.
|
|
278
|
+
setUser({ id: user.id });
|
|
279
|
+
|
|
280
|
+
// Global tags attached to every subsequent event
|
|
281
|
+
setTag('tenant', tenantId);
|
|
282
|
+
setTag('plan', 'pro');
|
|
283
|
+
|
|
284
|
+
// A manual breadcrumb
|
|
285
|
+
addBreadcrumb({ category: 'ui', message: 'Opened export dialog' });
|
|
286
|
+
|
|
287
|
+
// On logout
|
|
288
|
+
setUser(null);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Navigation, clicks, and `fetch` calls are recorded as breadcrumbs automatically.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 7. Configuration reference
|
|
296
|
+
|
|
297
|
+
Only `apiKey` and `apiUrl` are required.
|
|
298
|
+
|
|
299
|
+
| Option | Type | Default | Description |
|
|
300
|
+
|---|---|---|---|
|
|
301
|
+
| `apiKey` | `string` | — | Ingest API key, sent as `X-API-Key`. |
|
|
302
|
+
| `apiUrl` | `string` | — | API host root; `/api/v1/errors/batch` is appended. |
|
|
303
|
+
| `environment` | `string` | `"production"` | Tag shown in the dashboard metadata. |
|
|
304
|
+
| `release` | `string` | – | App version / git SHA (regression tracking, source maps). |
|
|
305
|
+
| `enabled` | `boolean` | `true` | Master switch — set `false` to disable entirely. |
|
|
306
|
+
| `sampleRate` | `number` | `1` | Fraction of events kept (0–1). |
|
|
307
|
+
| `maxBatchSize` | `number` | `20` | Events per request (backend caps at 100). |
|
|
308
|
+
| `flushIntervalMs` | `number` | `4000` | Debounce before an idle buffer flushes. |
|
|
309
|
+
| `maxQueueItems` | `number` | `100` | Max events persisted to the offline queue. |
|
|
310
|
+
| `maxRetries` | `number` | `3` | Retry attempts (5xx / network only). |
|
|
311
|
+
| `maxBreadcrumbs` | `number` | `30` | Breadcrumbs retained per event. |
|
|
312
|
+
| `offlineStorage` | `boolean` | `true` | Persist undelivered events to `localStorage`. |
|
|
313
|
+
| `captureUnhandledErrors` | `boolean` | `true` | Capture `window` uncaught errors. |
|
|
314
|
+
| `captureUnhandledRejections` | `boolean` | `true` | Capture unhandled promise rejections. |
|
|
315
|
+
| `captureResourceErrors` | `boolean` | `true` | Capture failed img/script/css loads. |
|
|
316
|
+
| `captureConsole` | `boolean` | `true` | Turn `console.error/warn` into breadcrumbs. |
|
|
317
|
+
| `autoBreadcrumbs` | `boolean` | `true` | Auto navigation/click/fetch breadcrumbs. |
|
|
318
|
+
| `denyUrls` | `(string \| RegExp)[]` | `[]` | Drop events whose stack/URL matches. |
|
|
319
|
+
| `allowUrls` | `(string \| RegExp)[]` | `[]` | If set, keep only matching events. |
|
|
320
|
+
| `beforeSend` | `(event) => event \| null` | – | Mutate/scrub, or drop (`return null`). |
|
|
321
|
+
| `debug` | `boolean` | `false` | Log SDK diagnostics to the console. |
|
|
322
|
+
|
|
323
|
+
### `beforeSend`
|
|
324
|
+
|
|
325
|
+
Runs after the built-in PII scrub, right before an event is queued:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
init({
|
|
329
|
+
apiKey, apiUrl,
|
|
330
|
+
beforeSend(event) {
|
|
331
|
+
if (event.message.includes('ResizeObserver loop')) return null; // drop noise
|
|
332
|
+
event.context.build = '2026.7.1';
|
|
333
|
+
return event;
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## 8. What gets captured
|
|
341
|
+
|
|
342
|
+
| Source | How | Severity |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| Uncaught exceptions | `window` `error` | `Error` |
|
|
345
|
+
| Unhandled promise rejections | `unhandledrejection` | `Error` |
|
|
346
|
+
| React render/lifecycle errors | `<ErrorBoundary>` / Next `error.tsx` | `Error` |
|
|
347
|
+
| Failed resource loads | `error` (capture phase) | `Warning` |
|
|
348
|
+
| Manual `captureException` | you | `Error` (override via hint) |
|
|
349
|
+
| Manual `captureMessage` | you | your choice |
|
|
350
|
+
| `console.error` / `console.warn` | breadcrumb only (not an event) | – |
|
|
351
|
+
|
|
352
|
+
Each event carries: message, stack, exception type, `error.cause` chain, the current
|
|
353
|
+
route, user agent, viewport, `release` + `environment`, your tags, and the recent
|
|
354
|
+
breadcrumb trail (all under the dashboard's **Additional data**). The same error hitting
|
|
355
|
+
multiple hooks is **de-duplicated** into a single report.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 9. Security & privacy (read this)
|
|
360
|
+
|
|
361
|
+
- **The API key is public.** Use a **write-only / ingest-only** key so a leaked key can
|
|
362
|
+
only submit errors — never read or manage data. (Ask your CherryBoard admin to scope
|
|
363
|
+
the key to error ingest.)
|
|
364
|
+
- **CORS & rate limiting** live on the backend. The ingest endpoint should allow your
|
|
365
|
+
app's origin(s) and rate-limit per key to blunt abuse of the public key.
|
|
366
|
+
- **PII scrubbing is on by default** — `Authorization`/tokens/cookies, emails, and
|
|
367
|
+
sensitive query params/keys (`password`, `secret`, `token`, …) are redacted *before*
|
|
368
|
+
anything leaves the browser. Add your own rules via `beforeSend`. The SDK never sends
|
|
369
|
+
request/response bodies or form values.
|
|
370
|
+
- **Source maps**: see [Readable stack traces](#11-readable-stack-traces-source-maps). Don't
|
|
371
|
+
serve `.map` files publicly — upload them instead.
|
|
372
|
+
- **SSR-safe**: the core touches no browser globals at import time; the React entry is
|
|
373
|
+
marked `"use client"`. Don't call the SDK from `"use server"` modules — it's for the
|
|
374
|
+
browser.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 10. Troubleshooting
|
|
379
|
+
|
|
380
|
+
- **No errors appear.** Confirm `init` ran (set `debug: true`), the key/URL are correct,
|
|
381
|
+
and the browser Network tab shows a `POST …/api/v1/errors/batch`. A `401/403` means a
|
|
382
|
+
bad/expired key; a **CORS error** means the backend must allow your origin.
|
|
383
|
+
- **"Script error." with no stack.** A cross-origin script threw. Add
|
|
384
|
+
`crossorigin="anonymous"` to the script tag and `Access-Control-Allow-Origin` on the
|
|
385
|
+
asset host. The SDK already drops bare `Script error.` noise.
|
|
386
|
+
- **Duplicate reports.** Shouldn't happen (built-in dedupe), but avoid manually calling
|
|
387
|
+
`captureException` for an error your `ErrorBoundary` already handles.
|
|
388
|
+
- **`window is not defined` during build.** You imported the SDK into server-only code.
|
|
389
|
+
Call it from client components / `instrumentation-client.ts` only.
|
|
390
|
+
- **Events lost on tab close.** Handled — the SDK flushes via `keepalive` on `pagehide`/
|
|
391
|
+
visibility change, and persists to an offline queue that drains when you're back online.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 11. Readable stack traces (source maps)
|
|
396
|
+
|
|
397
|
+
Production stacks are minified (`main.4f2c1b.js:1:24817`). Upload your build's source
|
|
398
|
+
maps and CherryBoard resolves them server-side into real files, lines and function
|
|
399
|
+
names — shown on the issue page as **original sources**.
|
|
400
|
+
|
|
401
|
+
Two things must line up: the `release` you pass to `init()` and the `--release` you
|
|
402
|
+
upload with. **If they differ, nothing is symbolicated** (silently — it's recorded as
|
|
403
|
+
"not applicable", not an error).
|
|
404
|
+
|
|
405
|
+
Add to CI, after the build:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
npx cherryboard-upload-sourcemaps \
|
|
409
|
+
--dir .next/static \
|
|
410
|
+
--url-prefix /_next/static \
|
|
411
|
+
--release "$GIT_SHA" \
|
|
412
|
+
--api-url https://<your-cherryboard-api-host> \
|
|
413
|
+
--api-key "$CHERRYBOARD_UPLOAD_KEY"
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- Use a **General (server) key** — never the browser key. The write-only browser scope
|
|
417
|
+
deliberately cannot upload build artifacts.
|
|
418
|
+
- `--dry-run` lists what would upload without sending anything.
|
|
419
|
+
- Uploading the same file for a release again **replaces** it, so re-running a build is safe.
|
|
420
|
+
- Maps are stored privately and expire after 90 days.
|
|
421
|
+
|
|
422
|
+
Next.js needs `productionBrowserSourceMaps: true` in `next.config.ts` to emit them.
|
|
423
|
+
Prefer not serving the `.map` files to the public — uploading is enough.
|
|
424
|
+
|
|
425
|
+
## 12. Server-side errors (Next.js)
|
|
426
|
+
|
|
427
|
+
Browser capture never sees RSC render, route handler or server action errors. Catch
|
|
428
|
+
them in `instrumentation.ts`:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
// instrumentation.ts
|
|
432
|
+
import { init, captureRequestError } from '@cherrypeak-org/cherryboard-web';
|
|
433
|
+
|
|
434
|
+
export function register() {
|
|
435
|
+
init({
|
|
436
|
+
apiKey: process.env.CHERRYBOARD_KEY!, // server-side key
|
|
437
|
+
apiUrl: process.env.CHERRYBOARD_URL!,
|
|
438
|
+
environment: process.env.NEXT_PUBLIC_ENV ?? 'production',
|
|
439
|
+
release: process.env.NEXT_PUBLIC_COMMIT_SHA,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export const onRequestError = captureRequestError;
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
The core runs fine outside the browser: it installs no window handlers there and
|
|
447
|
+
delivers over `fetch`.
|
|
448
|
+
|
|
449
|
+
## 13. Why didn't my error show up?
|
|
450
|
+
|
|
451
|
+
Events can be dropped on purpose (sampling, dedupe, filters) — ask the SDK:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
import { getDiscardedEvents } from '@cherrypeak-org/cherryboard-web';
|
|
455
|
+
console.log(getDiscardedEvents());
|
|
456
|
+
// { deduped: 3, sampled: 1, rate_limited: 0, filtered: 0, send_failed: 0 }
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## API summary
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
// Core — @cherrypeak-org/cherryboard-web
|
|
463
|
+
init(config): CherryBoardClient
|
|
464
|
+
captureException(error, hint?): void
|
|
465
|
+
captureMessage(message, severity?, hint?): void
|
|
466
|
+
addBreadcrumb(crumb): void
|
|
467
|
+
setUser(user | null): void
|
|
468
|
+
setTag(key, value): void
|
|
469
|
+
setContext(key, value): void
|
|
470
|
+
flush(): Promise<void>
|
|
471
|
+
close(): void
|
|
472
|
+
getClient(): CherryBoardClient | null
|
|
473
|
+
getDiscardedEvents(): Record<string, number>
|
|
474
|
+
captureRequestError(error, request?, context?) // Next.js instrumentation.ts
|
|
475
|
+
|
|
476
|
+
// CLI
|
|
477
|
+
cherryboard-upload-sourcemaps --dir <dir> --release <id> --api-url <url> --api-key <key>
|
|
478
|
+
|
|
479
|
+
// React — @cherrypeak-org/cherryboard-web/react
|
|
480
|
+
<CherryBoardProvider config withBoundary? fallback?>
|
|
481
|
+
<ErrorBoundary fallback? onError? resetKeys?>
|
|
482
|
+
useCaptureError(): (error, hint?) => void
|
|
483
|
+
useCherryBoard(): CherryBoardClient | null
|
|
484
|
+
captureRouteError(error): void // for Next error.tsx / global-error.tsx
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## License
|
|
488
|
+
|
|
489
|
+
UNLICENSED — internal to CherryPeak.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Uploads a build's source maps to CherryBoard so production stack traces can be
|
|
4
|
+
* resolved to original files, lines and function names.
|
|
5
|
+
*
|
|
6
|
+
* Run this in CI after building, with the SAME release identifier the SDK is
|
|
7
|
+
* configured with — frames are matched by release, so a mismatch silently means
|
|
8
|
+
* no symbolication.
|
|
9
|
+
*
|
|
10
|
+
* cherryboard-upload-sourcemaps \
|
|
11
|
+
* --dir .next/static \
|
|
12
|
+
* --url-prefix /_next/static \
|
|
13
|
+
* --release "$GIT_SHA" \
|
|
14
|
+
* --api-url https://api.example.com \
|
|
15
|
+
* --api-key "$CHERRYBOARD_UPLOAD_KEY"
|
|
16
|
+
*
|
|
17
|
+
* Use a General (server) key, never the write-only browser key.
|
|
18
|
+
*/
|
|
19
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
20
|
+
import { join, relative, sep } from 'node:path';
|
|
21
|
+
import { parseArgs } from 'node:util';
|
|
22
|
+
|
|
23
|
+
const { values } = parseArgs({
|
|
24
|
+
options: {
|
|
25
|
+
dir: { type: 'string' },
|
|
26
|
+
'url-prefix': { type: 'string', default: '/' },
|
|
27
|
+
release: { type: 'string' },
|
|
28
|
+
'api-url': { type: 'string' },
|
|
29
|
+
'api-key': { type: 'string' },
|
|
30
|
+
'dry-run': { type: 'boolean', default: false },
|
|
31
|
+
help: { type: 'boolean', default: false },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (values.help) {
|
|
36
|
+
console.log(`
|
|
37
|
+
Usage: cherryboard-upload-sourcemaps --dir <build-dir> --release <id> --api-url <url> --api-key <key>
|
|
38
|
+
|
|
39
|
+
--dir Directory to scan recursively for .map files (e.g. .next/static)
|
|
40
|
+
--url-prefix URL path the directory is served under (e.g. /_next/static)
|
|
41
|
+
--release Release id; MUST match the SDK's \`release\` option
|
|
42
|
+
--api-url CherryBoard API host
|
|
43
|
+
--api-key A General (server) API key — not the browser key
|
|
44
|
+
--dry-run List what would be uploaded, upload nothing
|
|
45
|
+
`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const required = ['dir', 'release'];
|
|
50
|
+
if (!values['dry-run']) required.push('api-url', 'api-key');
|
|
51
|
+
const missing = required.filter((k) => !values[k]);
|
|
52
|
+
if (missing.length) {
|
|
53
|
+
console.error(`Missing required option(s): ${missing.map((m) => `--${m}`).join(', ')}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Recursively collect every .map file under a directory. */
|
|
58
|
+
async function findMaps(dir) {
|
|
59
|
+
const found = [];
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`Cannot read directory ${dir}: ${err.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const full = join(dir, entry.name);
|
|
69
|
+
if (entry.isDirectory()) found.push(...(await findMaps(full)));
|
|
70
|
+
else if (entry.name.endsWith('.map')) found.push(full);
|
|
71
|
+
}
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ".next/static/chunks/a.js.map" under prefix "/_next/static"
|
|
77
|
+
* -> "/_next/static/chunks/a.js" (the path stack frames report)
|
|
78
|
+
*/
|
|
79
|
+
function toUrlPath(mapFile, baseDir, urlPrefix) {
|
|
80
|
+
const rel = relative(baseDir, mapFile).split(sep).join('/');
|
|
81
|
+
const generated = rel.replace(/\.map$/, '');
|
|
82
|
+
const prefix = urlPrefix.endsWith('/') ? urlPrefix.slice(0, -1) : urlPrefix;
|
|
83
|
+
return `${prefix}/${generated}`.replace(/\/{2,}/g, '/');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const maps = await findMaps(values.dir);
|
|
87
|
+
if (maps.length === 0) {
|
|
88
|
+
// Not an error: many builds legitimately produce none (e.g. maps disabled).
|
|
89
|
+
console.log(`No .map files found under ${values.dir} — nothing to upload.`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`Found ${maps.length} source map(s) for release ${values.release}`);
|
|
94
|
+
|
|
95
|
+
let uploaded = 0;
|
|
96
|
+
let failed = 0;
|
|
97
|
+
|
|
98
|
+
for (const mapFile of maps) {
|
|
99
|
+
const filePath = toUrlPath(mapFile, values.dir, values['url-prefix']);
|
|
100
|
+
const { size } = await stat(mapFile);
|
|
101
|
+
|
|
102
|
+
if (values['dry-run']) {
|
|
103
|
+
console.log(` [dry-run] ${filePath} (${(size / 1024).toFixed(0)} KB)`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const content = await readFile(mapFile, 'utf8');
|
|
109
|
+
const res = await fetch(`${values['api-url'].replace(/\/+$/, '')}/api/v1/SourceMaps`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'Content-Type': 'application/json', 'X-API-Key': values['api-key'] },
|
|
112
|
+
body: JSON.stringify({ release: values.release, filePath, content }),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
const body = await res.text().catch(() => '');
|
|
117
|
+
console.error(` ✗ ${filePath} → HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
118
|
+
failed++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
uploaded++;
|
|
123
|
+
console.log(` ✓ ${filePath} (${(size / 1024).toFixed(0)} KB)`);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(` ✗ ${filePath} → ${err.message}`);
|
|
126
|
+
failed++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (values['dry-run']) {
|
|
131
|
+
console.log(`\nDry run: ${maps.length} map(s) would be uploaded.`);
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`\nUploaded ${uploaded}/${maps.length} source map(s)${failed ? `, ${failed} failed` : ''}.`);
|
|
136
|
+
// Failing the build on upload errors would block deploys over telemetry, so
|
|
137
|
+
// this exits non-zero only if nothing at all got through.
|
|
138
|
+
process.exit(uploaded === 0 ? 1 : 0);
|