@cherrypeak-org/cherryboard-web
v1.3.0
Published
Browser & React error tracking client for the CherryBoard dashboard (CherryPeak).
Maintainers
Readme
@cherrypeak-org/cherryboard-web
Browser & React error tracking for the CherryBoard dashboard. Drop it into any React app (Next.js, Vite, CRA) and uncaught errors, promise rejections, and React render errors are captured, batched, and shipped to your CherryBoard project — where they show up as grouped issues alongside your backend errors.
- Zero-config capture — global errors, unhandled rejections, resource failures.
- React-native —
ErrorBoundary, a provider, hooks, and Next.js App Router helpers. - Reliable — batching, offline queue, retry, dedupe, rate-limiting,
keepalive. - Private & safe — PII scrubbing before send, SSR-safe, tiny, zero-dependency core.
- Readable stacks — upload source maps and see original files, lines and functions.
Wire-compatible with the same
/api/v1/errorsingest the .NETCherryPeak.CherryBoard.Clientuses — one dashboard, both stacks.
Contents
- Install
- Get an ingest API key
- Quick start — Next.js (App Router)
- Quick start — Vite / CRA / plain React
- Reporting errors manually
- Identifying users & adding context
- Configuration reference
- What gets captured
- Security & privacy
- Troubleshooting
- Readable stack traces (source maps)
- Server-side errors (Next.js)
- Why didn't my error show up?
- Performance & visits
1. Install
npm install @cherrypeak-org/cherryboard-web
# or: pnpm add / yarn addreact and react-dom are optional peer dependencies — you only need them if
you import from @cherrypeak-org/cherryboard-web/react. The core works in any browser app.
2. Get an ingest API key
- Open the CherryBoard dashboard → your Project → the Environment you want errors filed under (e.g. Production).
- Create an API key and copy it. This key resolves the project + environment server-side, so front-end errors land in the right place automatically.
⚠️ This key ships in your browser bundle and is public. That's expected (it's how every browser error tracker works), but see Security for how to keep it safe (write-only scope, CORS, rate limiting).
Put the key and API host in public env vars:
# .env.local (Next.js) — NEXT_PUBLIC_ vars are exposed to the browser
NEXT_PUBLIC_CHERRYBOARD_KEY=cpd_xxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_CHERRYBOARD_URL=https://<your-cherryboard-api-host># .env (Vite)
VITE_CHERRYBOARD_KEY=cpd_xxxxxxxxxxxxxxxxxxxxxxxx
VITE_CHERRYBOARD_URL=https://<your-cherryboard-api-host>apiUrl is the API host root — the SDK appends /api/v1/errors/batch for you.
3. Quick start — Next.js (App Router)
Works with Next.js 13.4+ (App Router) including Next.js 16 / React 19.
3a. Initialize once, as early as possible
Create instrumentation-client.ts at your project root (or src/). Next.js runs
this on the client before your app hydrates — the ideal place to start the tracker.
// instrumentation-client.ts
import { init } from '@cherrypeak-org/cherryboard-web';
init({
apiKey: process.env.NEXT_PUBLIC_CHERRYBOARD_KEY!,
apiUrl: process.env.NEXT_PUBLIC_CHERRYBOARD_URL!,
environment: process.env.NEXT_PUBLIC_ENV ?? 'production',
release: process.env.NEXT_PUBLIC_COMMIT_SHA, // optional, recommended
});On older Next without
instrumentation-client.ts, use the<CherryBoardProvider>instead.
3b. Report render errors from the App Router boundaries
Next catches render errors with error.tsx (per route) and global-error.tsx (root
layout). They only render a fallback — add one line to also report them:
// app/error.tsx
'use client';
import { useEffect } from 'react';
import { captureRouteError } from '@cherrypeak-org/cherryboard-web/react';
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
captureRouteError(error); // includes the Next.js `digest` for correlation
}, [error]);
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={reset}>Try again</button>
</div>
);
}// app/global-error.tsx
'use client';
import { useEffect } from 'react';
import { captureRouteError } from '@cherrypeak-org/cherryboard-web/react';
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
captureRouteError(error);
}, [error]);
return (
<html>
<body>
<h2>Application error.</h2>
<button onClick={reset}>Reload</button>
</body>
</html>
);
}That's it — global errors, unhandled rejections, and route-level render errors are now tracked.
3c. (Optional) Wrap a subtree in an ErrorBoundary
For a nicer fallback around a specific area (and to keep the rest of the page alive):
'use client';
import { ErrorBoundary } from '@cherrypeak-org/cherryboard-web/react';
export function Widget() {
return (
<ErrorBoundary fallback={({ reset }) => <button onClick={reset}>Reload widget</button>}>
<FlakyChart />
</ErrorBoundary>
);
}3d. Alternative: the provider
If you'd rather not use instrumentation-client.ts, initialize with the provider in
your root layout. It's a client component, so it's safe to render from the (server)
layout:
// app/layout.tsx
import { CherryBoardProvider } from '@cherrypeak-org/cherryboard-web/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<CherryBoardProvider
config={{
apiKey: process.env.NEXT_PUBLIC_CHERRYBOARD_KEY!,
apiUrl: process.env.NEXT_PUBLIC_CHERRYBOARD_URL!,
environment: process.env.NEXT_PUBLIC_ENV ?? 'production',
}}
>
{children}
</CherryBoardProvider>
</body>
</html>
);
}Amplify note: on AWS Amplify, only env vars matching the build's allowlist reach the runtime.
NEXT_PUBLIC_*vars are picked up automatically — make sure your key uses that prefix.
4. Quick start — Vite / CRA / plain React
Call init once at your entry point, before rendering:
// main.tsx
import { init } from '@cherrypeak-org/cherryboard-web';
init({
apiKey: import.meta.env.VITE_CHERRYBOARD_KEY,
apiUrl: import.meta.env.VITE_CHERRYBOARD_URL,
environment: import.meta.env.MODE,
});
// ...then ReactDOM.createRoot(...).render(<App />)Wrap your tree in the boundary to catch render errors:
import { ErrorBoundary } from '@cherrypeak-org/cherryboard-web/react';
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<App />
</ErrorBoundary>5. Reporting errors manually
Error boundaries and global handlers can't see errors you catch yourself (in try/catch,
event handlers, or async code). Report those explicitly:
import { captureException, captureMessage } from '@cherrypeak-org/cherryboard-web';
try {
await riskyThing();
} catch (err) {
captureException(err, { context: { orderId, step: 'checkout' } });
}
// Log a noteworthy non-error event
captureMessage('Payment provider returned an unexpected shape', 'Warning');In components, the hook gives you a stable callback:
'use client';
import { useCaptureError } from '@cherrypeak-org/cherryboard-web/react';
function SaveButton() {
const capture = useCaptureError();
const onClick = async () => {
try {
await save();
} catch (err) {
capture(err, { context: { feature: 'save' } });
}
};
return <button onClick={onClick}>Save</button>;
}6. Identifying users & adding context
import { setUser, setTag, addBreadcrumb } from '@cherrypeak-org/cherryboard-web';
// After login — keep it to an opaque id; do NOT pass emails/names.
setUser({ id: user.id });
// Global tags attached to every subsequent event
setTag('tenant', tenantId);
setTag('plan', 'pro');
// A manual breadcrumb
addBreadcrumb({ category: 'ui', message: 'Opened export dialog' });
// On logout
setUser(null);Navigation, clicks, and fetch calls are recorded as breadcrumbs automatically.
7. Configuration reference
Only apiKey and apiUrl are required.
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | — | Ingest API key, sent as X-API-Key. |
| apiUrl | string | — | API host root; /api/v1/errors/batch is appended. |
| environment | string | "production" | Tag shown in the dashboard metadata. |
| release | string | – | App version / git SHA (regression tracking, source maps). |
| enabled | boolean | true | Master switch — set false to disable entirely. |
| sampleRate | number | 1 | Fraction of events kept (0–1). |
| maxBatchSize | number | 20 | Events per request (backend caps at 100). |
| flushIntervalMs | number | 4000 | Debounce before an idle buffer flushes. |
| maxQueueItems | number | 100 | Max events persisted to the offline queue. |
| maxRetries | number | 3 | Retry attempts (5xx / network only). |
| maxBreadcrumbs | number | 30 | Breadcrumbs retained per event. |
| offlineStorage | boolean | true | Persist undelivered events to localStorage. |
| captureUnhandledErrors | boolean | true | Capture window uncaught errors. |
| captureUnhandledRejections | boolean | true | Capture unhandled promise rejections. |
| captureResourceErrors | boolean | true | Capture failed img/script/css loads. |
| captureConsole | boolean | true | Turn console.error/warn into breadcrumbs. |
| autoBreadcrumbs | boolean | true | Auto navigation/click/fetch breadcrumbs. |
| denyUrls | (string \| RegExp)[] | [] | Drop events whose stack/URL matches. |
| allowUrls | (string \| RegExp)[] | [] | If set, keep only matching events. |
| beforeSend | (event) => event \| null | – | Mutate/scrub, or drop (return null). |
| trackPerformance | boolean | false | Collect API request timings and page views. See §14. |
| metricsFlushIntervalMs | number | 60000 | How often performance rollups are posted. |
| debug | boolean | false | Log SDK diagnostics to the console. |
beforeSend
Runs after the built-in PII scrub, right before an event is queued:
init({
apiKey, apiUrl,
beforeSend(event) {
if (event.message.includes('ResizeObserver loop')) return null; // drop noise
event.context.build = '2026.7.1';
return event;
},
});8. What gets captured
| Source | How | Severity |
|---|---|---|
| Uncaught exceptions | window error | Error |
| Unhandled promise rejections | unhandledrejection | Error |
| React render/lifecycle errors | <ErrorBoundary> / Next error.tsx | Error |
| Failed resource loads | error (capture phase) | Warning |
| Manual captureException | you | Error (override via hint) |
| Manual captureMessage | you | your choice |
| console.error / console.warn | breadcrumb only (not an event) | – |
Each event carries: message, stack, exception type, error.cause chain, the current
route, user agent, viewport, release + environment, your tags, and the recent
breadcrumb trail (all under the dashboard's Additional data). The same error hitting
multiple hooks is de-duplicated into a single report.
9. Security & privacy (read this)
The API key is public. Use a write-only / ingest-only key so a leaked key can only submit errors — never read or manage data. (Ask your CherryBoard admin to scope the key to error ingest.)
CORS & rate limiting live on the backend. The ingest endpoint should allow your app's origin(s) and rate-limit per key to blunt abuse of the public key.
PII scrubbing is on by default —
Authorization/tokens/cookies, emails, and sensitive query params/keys (password,secret,token, …) are redacted before anything leaves the browser. Add your own rules viabeforeSend. The SDK never sends request/response bodies or form values.Error tracking writes to
localStorageby default.offlineStorage(defaulttrue) persists undelivered events undercherryboard:queue:v1so they survive a reload and are retried later. Storing data on a visitor's device is covered by ePrivacy / "cookie law" in the EU, and analytics-style storage is generally not treated as strictly necessary — so under a consent banner this belongs behind consent, or setofflineStorage: falseto make the SDK storage-free. The trade is that events buffered when a tab closes are lost instead of retried.Performance tracking (§14) stores nothing on the device either way.
Source maps: see Readable stack traces. Don't serve
.mapfiles publicly — upload them instead.SSR-safe: the core touches no browser globals at import time; the React entry is marked
"use client". Don't call the SDK from"use server"modules — it's for the browser.
10. Troubleshooting
- No errors appear. Confirm
initran (setdebug: true), the key/URL are correct, and the browser Network tab shows aPOST …/api/v1/errors/batch. A401/403means a bad/expired key; a CORS error means the backend must allow your origin. - "Script error." with no stack. A cross-origin script threw. Add
crossorigin="anonymous"to the script tag andAccess-Control-Allow-Originon the asset host. The SDK already drops bareScript error.noise. - Duplicate reports. Shouldn't happen (built-in dedupe), but avoid manually calling
captureExceptionfor an error yourErrorBoundaryalready handles. window is not definedduring build. You imported the SDK into server-only code. Call it from client components /instrumentation-client.tsonly.- Events lost on tab close. Handled — the SDK flushes via
keepaliveonpagehide/ visibility change, and persists to an offline queue that drains when you're back online.
11. Readable stack traces (source maps)
Production stacks are minified (main.4f2c1b.js:1:24817). Upload your build's source
maps and CherryBoard resolves them server-side into real files, lines and function
names — shown on the issue page as original sources.
Two things must line up: the release you pass to init() and the --release you
upload with. If they differ, nothing is symbolicated (silently — it's recorded as
"not applicable", not an error).
Add to CI, after the build:
npx cherryboard-upload-sourcemaps \
--dir .next/static \
--url-prefix /_next/static \
--release "$GIT_SHA" \
--api-url https://<your-cherryboard-api-host> \
--api-key "$CHERRYBOARD_UPLOAD_KEY"- Use a General (server) key — never the browser key. The write-only browser scope deliberately cannot upload build artifacts.
--dry-runlists what would upload without sending anything.- Uploading the same file for a release again replaces it, so re-running a build is safe.
- Maps are stored privately and expire after 90 days.
Next.js needs productionBrowserSourceMaps: true in next.config.ts to emit them.
Prefer not serving the .map files to the public — uploading is enough.
12. Server-side errors (Next.js)
Browser capture never sees RSC render, route handler or server action errors. Catch
them in instrumentation.ts:
// instrumentation.ts
import { init, captureRequestError } from '@cherrypeak-org/cherryboard-web';
export function register() {
init({
apiKey: process.env.CHERRYBOARD_KEY!, // server-side key
apiUrl: process.env.CHERRYBOARD_URL!,
environment: process.env.NEXT_PUBLIC_ENV ?? 'production',
release: process.env.NEXT_PUBLIC_COMMIT_SHA,
});
}
export const onRequestError = captureRequestError;The core runs fine outside the browser: it installs no window handlers there and
delivers over fetch.
13. Why didn't my error show up?
Events can be dropped on purpose (sampling, dedupe, filters) — ask the SDK:
import { getDiscardedEvents } from '@cherrypeak-org/cherryboard-web';
console.log(getDiscardedEvents());
// { deduped: 3, sampled: 1, rate_limited: 0, filtered: 0, send_failed: 0 }API summary
// Core — @cherrypeak-org/cherryboard-web
init(config): CherryBoardClient
captureException(error, hint?): void
captureMessage(message, severity?, hint?): void
addBreadcrumb(crumb): void
setUser(user | null): void
setTag(key, value): void
setContext(key, value): void
flush(): Promise<void>
close(): void
getClient(): CherryBoardClient | null
getDiscardedEvents(): Record<string, number>
captureRequestError(error, request?, context?) // Next.js instrumentation.ts
// CLI
cherryboard-upload-sourcemaps --dir <dir> --release <id> --api-url <url> --api-key <key>
// React — @cherrypeak-org/cherryboard-web/react
<CherryBoardProvider config withBoundary? fallback?>
<ErrorBoundary fallback? onError? resetKeys?>
useCaptureError(): (error, hint?) => void
useCherryBoard(): CherryBoardClient | null
captureRouteError(error): void // for Next error.tsx / global-error.tsx14. Performance & visits
Off by default. One option turns on API request timings and page-view statistics:
init({
apiKey: process.env.NEXT_PUBLIC_CHERRYBOARD_KEY!,
apiUrl: process.env.NEXT_PUBLIC_CHERRYBOARD_URL!,
trackPerformance: true,
});Timings appear on the environment page in the dashboard within a few minutes.
What it costs the page
Effectively nothing. Timings come from PerformanceObserver, so the browser is
already recording them and nothing is added to the request path. No fetch
patching is involved, which also means no conflict with other libraries that
wrap fetch themselves.
Rollups are aggregated in memory and posted once a minute. A page making 200 API calls sends one small summary, not 200 events — payload size tracks the number of distinct routes, not traffic.
What is recorded
| Kept | Not kept |
|---|---|
| Route templates — /api/orders/:id | Concrete URLs; identifiers collapse to :id before anything is recorded |
| Counts, durations, error counts | Query strings — dropped entirely, since they carry tokens and emails |
| Page views and visits | Anything identifying a visitor |
Performance tracking writes nothing to the visitor's device and stores no identifier. "Visits" counts document loads, not people — a refresh or a second tab is another visit.
Counting people (v1.3.0+)
If you call setUser({ id }), that id is also sent with performance rollups so
the dashboard can report distinct signed-in users. The server hashes it on
arrival, salted per project, and never stores the value you sent. Nothing is
written to the visitor's device either way.
Two things worth knowing before enabling this:
- It counts signed-in traffic only. Anonymous visitors have no id, and inventing one would mean device storage.
- The hash is pseudonymised, not anonymous — anyone holding a user id can hash it and find the rows. It remains personal data, ages out with the 60-day retention, and counts towards a subject request.
Because this uses an identifier you already send for error tracking, it adds no
new data — but it is a new purpose, and under GDPR that has to be disclosed
in your privacy notice on its own footing. To opt out, don't call setUser, or
strip userId in beforeSend; the rest of the performance data is unaffected.
Note this is not true of error tracking, which persists undelivered events to
localStorage by default. See §9.
Reading the numbers
Percentiles come from latency histograms, so they are accurate to the nearest bucket rather than exact. Watch p95 — it is the experience of your slowest 1 in 20 requests, which averages hide.
Cross-origin timings require Timing-Allow-Origin on the responding server, or
the browser reports zero.
License
Apache License 2.0 — see LICENSE.
Free to use, modify and redistribute, including commercially. It includes an express patent grant, and asks that you keep the copyright notice and state any changes you make.
