@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 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);