@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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { ReactNode, ErrorInfo, Component } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ErrorBoundaryFallbackProps {
|
|
4
|
+
error: Error;
|
|
5
|
+
reset: () => void;
|
|
6
|
+
}
|
|
7
|
+
interface ErrorBoundaryProps {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** UI shown when a child throws. A render-prop receives `{ error, reset }`. */
|
|
10
|
+
fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
|
|
11
|
+
/** Called after the error is reported. */
|
|
12
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
13
|
+
/** When any value in this array changes, the boundary auto-resets. */
|
|
14
|
+
resetKeys?: unknown[];
|
|
15
|
+
}
|
|
16
|
+
interface ErrorBoundaryState {
|
|
17
|
+
error: Error | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Catches render/lifecycle errors in descendants, reports them to CherryBoard
|
|
21
|
+
* (with the React component stack), and renders a fallback instead of a blank
|
|
22
|
+
* screen. Does NOT catch async/event-handler errors — those are caught by the
|
|
23
|
+
* global handlers installed by `init()`.
|
|
24
|
+
*/
|
|
25
|
+
declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
26
|
+
state: ErrorBoundaryState;
|
|
27
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
28
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
29
|
+
componentDidUpdate(prev: ErrorBoundaryProps): void;
|
|
30
|
+
reset: () => void;
|
|
31
|
+
render(): ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** SDK version — kept in sync with package.json. */
|
|
35
|
+
declare const SDK_VERSION = "1.0.1";
|
|
36
|
+
/** Severity levels — these strings match the backend `ErrorSeverity` enum exactly. */
|
|
37
|
+
type Severity = 'Debug' | 'Info' | 'Warning' | 'Error' | 'Critical';
|
|
38
|
+
/** A trail entry giving context that led up to an error. */
|
|
39
|
+
interface Breadcrumb {
|
|
40
|
+
timestamp: string;
|
|
41
|
+
/** e.g. "navigation" | "click" | "fetch" | "console" | "custom" */
|
|
42
|
+
category: string;
|
|
43
|
+
message: string;
|
|
44
|
+
level?: Severity;
|
|
45
|
+
data?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
/** Minimal user identity attached to events. Keep it to an opaque id by default. */
|
|
48
|
+
interface UserContext {
|
|
49
|
+
id?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The structured event assembled at capture time. `beforeSend` receives this
|
|
54
|
+
* (metadata still structured) so you can scrub/augment before it is serialized
|
|
55
|
+
* to the wire payload.
|
|
56
|
+
*/
|
|
57
|
+
interface CherryBoardEvent {
|
|
58
|
+
message: string;
|
|
59
|
+
severity: Severity;
|
|
60
|
+
timestamp: string;
|
|
61
|
+
exceptionType?: string;
|
|
62
|
+
stackTrace?: string;
|
|
63
|
+
innerException?: string;
|
|
64
|
+
userId?: string;
|
|
65
|
+
requestPath?: string;
|
|
66
|
+
userAgent?: string;
|
|
67
|
+
/** Structured context serialized into the payload's `metadata` JSON string. */
|
|
68
|
+
context: Record<string, unknown>;
|
|
69
|
+
breadcrumbs: Breadcrumb[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The wire payload — field names/shape match the backend `RecordErrorRequest`
|
|
73
|
+
* (ASP.NET binds case-insensitively; `severity` is the enum name string).
|
|
74
|
+
*/
|
|
75
|
+
interface ErrorPayload {
|
|
76
|
+
message: string;
|
|
77
|
+
stackTrace?: string;
|
|
78
|
+
severity: Severity;
|
|
79
|
+
timestamp: string;
|
|
80
|
+
userId?: string;
|
|
81
|
+
requestPath?: string;
|
|
82
|
+
requestMethod?: string;
|
|
83
|
+
ipAddress?: string;
|
|
84
|
+
userAgent?: string;
|
|
85
|
+
metadata?: string;
|
|
86
|
+
exceptionType?: string;
|
|
87
|
+
innerException?: string;
|
|
88
|
+
}
|
|
89
|
+
type UrlPattern = string | RegExp;
|
|
90
|
+
/** User-supplied configuration. Only `apiKey` and `apiUrl` are required. */
|
|
91
|
+
interface CherryBoardConfig {
|
|
92
|
+
/** Ingest API key (public, write-only). Sent as the `X-API-Key` header. */
|
|
93
|
+
apiKey: string;
|
|
94
|
+
/** API host root, e.g. "https://api.cherryboard.cherrypeak.eu". `/api/v1/errors/batch` is appended. */
|
|
95
|
+
apiUrl: string;
|
|
96
|
+
/** Environment tag shown in the dashboard metadata. Default "production". */
|
|
97
|
+
environment?: string;
|
|
98
|
+
/** Release identifier (git SHA / app version) for regression tracking + source-map matching. */
|
|
99
|
+
release?: string;
|
|
100
|
+
/** Master switch. When false, nothing is captured or sent. Default true. */
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
/** Fraction of events to keep, 0..1. Default 1 (100%). */
|
|
103
|
+
sampleRate?: number;
|
|
104
|
+
/** Max events per delivery request (backend caps batches at 100). Default 20. */
|
|
105
|
+
maxBatchSize?: number;
|
|
106
|
+
/** Debounce before an idle buffer is flushed, in ms. Default 4000. */
|
|
107
|
+
flushIntervalMs?: number;
|
|
108
|
+
/** Max events persisted to the offline queue. Default 100. */
|
|
109
|
+
maxQueueItems?: number;
|
|
110
|
+
/** Retry attempts for failed (5xx / network) deliveries. Default 3. */
|
|
111
|
+
maxRetries?: number;
|
|
112
|
+
/** Max breadcrumbs retained per event. Default 30. */
|
|
113
|
+
maxBreadcrumbs?: number;
|
|
114
|
+
/** Persist undelivered events to localStorage and retry later. Default true. */
|
|
115
|
+
offlineStorage?: boolean;
|
|
116
|
+
/** Capture `window` uncaught errors. Default true. */
|
|
117
|
+
captureUnhandledErrors?: boolean;
|
|
118
|
+
/** Capture unhandled promise rejections. Default true. */
|
|
119
|
+
captureUnhandledRejections?: boolean;
|
|
120
|
+
/** Capture failed resource loads (img/script/css). Default true. */
|
|
121
|
+
captureResourceErrors?: boolean;
|
|
122
|
+
/** Turn console.error/warn into breadcrumbs (never into events). Default true. */
|
|
123
|
+
captureConsole?: boolean;
|
|
124
|
+
/** Auto-record navigation / click / fetch breadcrumbs. Default true. */
|
|
125
|
+
autoBreadcrumbs?: boolean;
|
|
126
|
+
/** Drop events whose stack/URL matches any of these. */
|
|
127
|
+
denyUrls?: UrlPattern[];
|
|
128
|
+
/** If set, only keep events whose stack/URL matches one of these. */
|
|
129
|
+
allowUrls?: UrlPattern[];
|
|
130
|
+
/**
|
|
131
|
+
* Final hook to mutate, scrub, or drop (`return null`) an event before it is
|
|
132
|
+
* queued. Runs AFTER the built-in PII scrub, so anything you add here is your
|
|
133
|
+
* own responsibility to keep clean.
|
|
134
|
+
*/
|
|
135
|
+
beforeSend?: (event: CherryBoardEvent) => CherryBoardEvent | null;
|
|
136
|
+
/** Log SDK diagnostics to the console. Default false. */
|
|
137
|
+
debug?: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** Config with all defaults resolved. */
|
|
140
|
+
interface ResolvedConfig {
|
|
141
|
+
apiKey: string;
|
|
142
|
+
apiUrl: string;
|
|
143
|
+
environment: string;
|
|
144
|
+
release?: string;
|
|
145
|
+
enabled: boolean;
|
|
146
|
+
sampleRate: number;
|
|
147
|
+
maxBatchSize: number;
|
|
148
|
+
flushIntervalMs: number;
|
|
149
|
+
maxQueueItems: number;
|
|
150
|
+
maxRetries: number;
|
|
151
|
+
maxBreadcrumbs: number;
|
|
152
|
+
offlineStorage: boolean;
|
|
153
|
+
captureUnhandledErrors: boolean;
|
|
154
|
+
captureUnhandledRejections: boolean;
|
|
155
|
+
captureResourceErrors: boolean;
|
|
156
|
+
captureConsole: boolean;
|
|
157
|
+
autoBreadcrumbs: boolean;
|
|
158
|
+
denyUrls: UrlPattern[];
|
|
159
|
+
allowUrls: UrlPattern[];
|
|
160
|
+
beforeSend?: (event: CherryBoardEvent) => CherryBoardEvent | null;
|
|
161
|
+
debug: boolean;
|
|
162
|
+
}
|
|
163
|
+
/** Optional hint passed to captureException. */
|
|
164
|
+
interface CaptureHint {
|
|
165
|
+
severity?: Severity;
|
|
166
|
+
/** Merged into the event context (surfaced as dashboard "Additional data"). */
|
|
167
|
+
context?: Record<string, unknown>;
|
|
168
|
+
/** React component stack (from an error boundary). */
|
|
169
|
+
componentStack?: string;
|
|
170
|
+
/** Next.js error digest for correlation. */
|
|
171
|
+
digest?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface CherryBoardProviderProps {
|
|
175
|
+
config: CherryBoardConfig;
|
|
176
|
+
children: ReactNode;
|
|
177
|
+
/** Wrap children in an <ErrorBoundary> that reports render errors. Default false. */
|
|
178
|
+
withBoundary?: boolean;
|
|
179
|
+
/** Fallback UI for the built-in boundary (only used when withBoundary). */
|
|
180
|
+
fallback?: ErrorBoundaryProps['fallback'];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Convenience initializer for React apps. Calls `init(config)` once on mount.
|
|
184
|
+
*
|
|
185
|
+
* For the earliest possible capture (before first render), prefer calling
|
|
186
|
+
* `init(config)` at app bootstrap — e.g. Next.js `instrumentation-client.ts`.
|
|
187
|
+
*/
|
|
188
|
+
declare function CherryBoardProvider({ config, children, withBoundary, fallback, }: CherryBoardProviderProps): ReactNode;
|
|
189
|
+
|
|
190
|
+
/** Bounded ring buffer of recent activity attached to each outgoing event. */
|
|
191
|
+
declare class BreadcrumbBuffer {
|
|
192
|
+
private readonly max;
|
|
193
|
+
private readonly items;
|
|
194
|
+
private readonly teardown;
|
|
195
|
+
constructor(max: number);
|
|
196
|
+
add(crumb: Omit<Breadcrumb, 'timestamp'> & {
|
|
197
|
+
timestamp?: string;
|
|
198
|
+
}): void;
|
|
199
|
+
snapshot(): Breadcrumb[];
|
|
200
|
+
/** Auto-record navigation, clicks and fetch calls. Call close() to detach. */
|
|
201
|
+
install(): void;
|
|
202
|
+
private installHistory;
|
|
203
|
+
private installClicks;
|
|
204
|
+
private installFetch;
|
|
205
|
+
close(): void;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
declare class CherryBoardClient {
|
|
209
|
+
readonly config: ResolvedConfig;
|
|
210
|
+
readonly breadcrumbs: BreadcrumbBuffer;
|
|
211
|
+
private readonly transport;
|
|
212
|
+
private readonly deduper;
|
|
213
|
+
private readonly limiter;
|
|
214
|
+
private readonly queue;
|
|
215
|
+
private buffer;
|
|
216
|
+
private flushTimer;
|
|
217
|
+
private processing;
|
|
218
|
+
/** Epoch ms until which the server has told us to stop sending (429). */
|
|
219
|
+
private rateLimitedUntil;
|
|
220
|
+
/**
|
|
221
|
+
* Counts of events discarded before delivery, by reason. Without this an SDK
|
|
222
|
+
* drops events completely silently, which makes "why is this error missing?"
|
|
223
|
+
* unanswerable. Surfaced via getDiscardedEvents().
|
|
224
|
+
*/
|
|
225
|
+
private discarded;
|
|
226
|
+
private closed;
|
|
227
|
+
private scope;
|
|
228
|
+
private teardown;
|
|
229
|
+
constructor(config: CherryBoardConfig);
|
|
230
|
+
captureException(error: unknown, hint?: CaptureHint): void;
|
|
231
|
+
captureMessage(message: string, severity?: Severity, hint?: CaptureHint): void;
|
|
232
|
+
/**
|
|
233
|
+
* Events dropped before delivery, keyed by reason (sampled, deduped,
|
|
234
|
+
* rate_limited, filtered, queue_overflow, oversized, send_failed).
|
|
235
|
+
*/
|
|
236
|
+
getDiscardedEvents(): Readonly<Record<string, number>>;
|
|
237
|
+
private discard;
|
|
238
|
+
addBreadcrumb(crumb: Omit<Breadcrumb, 'timestamp'> & {
|
|
239
|
+
timestamp?: string;
|
|
240
|
+
}): void;
|
|
241
|
+
setUser(user: UserContext | null): void;
|
|
242
|
+
setTag(key: string, value: unknown): void;
|
|
243
|
+
setContext(key: string, value: unknown): void;
|
|
244
|
+
close(): void;
|
|
245
|
+
private process;
|
|
246
|
+
private buildContext;
|
|
247
|
+
private toPayload;
|
|
248
|
+
private enqueue;
|
|
249
|
+
private scheduleFlush;
|
|
250
|
+
/** Deliver buffered events. Pass keepalive=true on page unload. */
|
|
251
|
+
flush(keepalive?: boolean): Promise<void>;
|
|
252
|
+
private deliver;
|
|
253
|
+
private drainOffline;
|
|
254
|
+
private installLifecycle;
|
|
255
|
+
private debug;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Access the active client (or null before init). */
|
|
259
|
+
declare function useCherryBoard(): CherryBoardClient | null;
|
|
260
|
+
/**
|
|
261
|
+
* Returns a stable callback for manually reporting errors from event handlers
|
|
262
|
+
* or async code — the gap that error boundaries cannot cover.
|
|
263
|
+
*/
|
|
264
|
+
declare function useCaptureError(): (error: unknown, hint?: CaptureHint) => void;
|
|
265
|
+
/**
|
|
266
|
+
* Drop-in reporter for the Next.js App Router `error.tsx` / `global-error.tsx`
|
|
267
|
+
* boundaries. Call it inside a `useEffect` with the received error.
|
|
268
|
+
*/
|
|
269
|
+
declare function captureRouteError(error: Error & {
|
|
270
|
+
digest?: string;
|
|
271
|
+
}): void;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Initialize CherryBoard once, as early as possible (ideally at app bootstrap).
|
|
275
|
+
* Idempotent: a second call returns the existing client — call `close()` first
|
|
276
|
+
* to re-initialize with new config.
|
|
277
|
+
*/
|
|
278
|
+
declare function init(config: CherryBoardConfig): CherryBoardClient;
|
|
279
|
+
/** The active client, or null if `init` hasn't run. */
|
|
280
|
+
declare function getClient(): CherryBoardClient | null;
|
|
281
|
+
declare function isInitialized(): boolean;
|
|
282
|
+
/** Report an error (any thrown value). No-op until `init` has been called. */
|
|
283
|
+
declare function captureException(error: unknown, hint?: CaptureHint): void;
|
|
284
|
+
/** Report a message with an explicit severity (default "Info"). */
|
|
285
|
+
declare function captureMessage(message: string, severity?: Severity, hint?: CaptureHint): void;
|
|
286
|
+
declare function addBreadcrumb(crumb: Omit<Breadcrumb, 'timestamp'> & {
|
|
287
|
+
timestamp?: string;
|
|
288
|
+
}): void;
|
|
289
|
+
declare function setUser(user: UserContext | null): void;
|
|
290
|
+
declare function setTag(key: string, value: unknown): void;
|
|
291
|
+
declare function setContext(key: string, value: unknown): void;
|
|
292
|
+
/** Force-flush buffered events (returns a promise you can await before navigation). */
|
|
293
|
+
declare function flush(): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Reports a server-side Next.js error from `instrumentation.ts`:
|
|
296
|
+
*
|
|
297
|
+
* export const onRequestError = captureRequestError;
|
|
298
|
+
*
|
|
299
|
+
* The core runs fine outside the browser — it installs no window handlers there
|
|
300
|
+
* and delivers over `fetch` — so the same client covers RSC render, route
|
|
301
|
+
* handler and server action errors, which browser-only capture never sees.
|
|
302
|
+
*/
|
|
303
|
+
declare function captureRequestError(error: unknown, request?: {
|
|
304
|
+
path?: string;
|
|
305
|
+
method?: string;
|
|
306
|
+
headers?: Record<string, string | undefined>;
|
|
307
|
+
}, context?: {
|
|
308
|
+
routerKind?: string;
|
|
309
|
+
routePath?: string;
|
|
310
|
+
renderSource?: string;
|
|
311
|
+
}): void;
|
|
312
|
+
/**
|
|
313
|
+
* Counts of events discarded before delivery, keyed by reason. Useful when an
|
|
314
|
+
* error you expected never showed up.
|
|
315
|
+
*/
|
|
316
|
+
declare function getDiscardedEvents(): Readonly<Record<string, number>>;
|
|
317
|
+
/** Tear down all handlers and clear the active client. */
|
|
318
|
+
declare function close(): void;
|
|
319
|
+
|
|
320
|
+
export { type Breadcrumb, type CaptureHint, type CherryBoardConfig, type CherryBoardEvent, CherryBoardProvider, type CherryBoardProviderProps, ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, type ErrorPayload, SDK_VERSION, type Severity, type UserContext, addBreadcrumb, captureException, captureMessage, captureRequestError, captureRouteError, close, flush, getClient, getDiscardedEvents, init, isInitialized, setContext, setTag, setUser, useCaptureError, useCherryBoard };
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { ReactNode, ErrorInfo, Component } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ErrorBoundaryFallbackProps {
|
|
4
|
+
error: Error;
|
|
5
|
+
reset: () => void;
|
|
6
|
+
}
|
|
7
|
+
interface ErrorBoundaryProps {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** UI shown when a child throws. A render-prop receives `{ error, reset }`. */
|
|
10
|
+
fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
|
|
11
|
+
/** Called after the error is reported. */
|
|
12
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
13
|
+
/** When any value in this array changes, the boundary auto-resets. */
|
|
14
|
+
resetKeys?: unknown[];
|
|
15
|
+
}
|
|
16
|
+
interface ErrorBoundaryState {
|
|
17
|
+
error: Error | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Catches render/lifecycle errors in descendants, reports them to CherryBoard
|
|
21
|
+
* (with the React component stack), and renders a fallback instead of a blank
|
|
22
|
+
* screen. Does NOT catch async/event-handler errors — those are caught by the
|
|
23
|
+
* global handlers installed by `init()`.
|
|
24
|
+
*/
|
|
25
|
+
declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
26
|
+
state: ErrorBoundaryState;
|
|
27
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
28
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
29
|
+
componentDidUpdate(prev: ErrorBoundaryProps): void;
|
|
30
|
+
reset: () => void;
|
|
31
|
+
render(): ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** SDK version — kept in sync with package.json. */
|
|
35
|
+
declare const SDK_VERSION = "1.0.1";
|
|
36
|
+
/** Severity levels — these strings match the backend `ErrorSeverity` enum exactly. */
|
|
37
|
+
type Severity = 'Debug' | 'Info' | 'Warning' | 'Error' | 'Critical';
|
|
38
|
+
/** A trail entry giving context that led up to an error. */
|
|
39
|
+
interface Breadcrumb {
|
|
40
|
+
timestamp: string;
|
|
41
|
+
/** e.g. "navigation" | "click" | "fetch" | "console" | "custom" */
|
|
42
|
+
category: string;
|
|
43
|
+
message: string;
|
|
44
|
+
level?: Severity;
|
|
45
|
+
data?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
/** Minimal user identity attached to events. Keep it to an opaque id by default. */
|
|
48
|
+
interface UserContext {
|
|
49
|
+
id?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The structured event assembled at capture time. `beforeSend` receives this
|
|
54
|
+
* (metadata still structured) so you can scrub/augment before it is serialized
|
|
55
|
+
* to the wire payload.
|
|
56
|
+
*/
|
|
57
|
+
interface CherryBoardEvent {
|
|
58
|
+
message: string;
|
|
59
|
+
severity: Severity;
|
|
60
|
+
timestamp: string;
|
|
61
|
+
exceptionType?: string;
|
|
62
|
+
stackTrace?: string;
|
|
63
|
+
innerException?: string;
|
|
64
|
+
userId?: string;
|
|
65
|
+
requestPath?: string;
|
|
66
|
+
userAgent?: string;
|
|
67
|
+
/** Structured context serialized into the payload's `metadata` JSON string. */
|
|
68
|
+
context: Record<string, unknown>;
|
|
69
|
+
breadcrumbs: Breadcrumb[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The wire payload — field names/shape match the backend `RecordErrorRequest`
|
|
73
|
+
* (ASP.NET binds case-insensitively; `severity` is the enum name string).
|
|
74
|
+
*/
|
|
75
|
+
interface ErrorPayload {
|
|
76
|
+
message: string;
|
|
77
|
+
stackTrace?: string;
|
|
78
|
+
severity: Severity;
|
|
79
|
+
timestamp: string;
|
|
80
|
+
userId?: string;
|
|
81
|
+
requestPath?: string;
|
|
82
|
+
requestMethod?: string;
|
|
83
|
+
ipAddress?: string;
|
|
84
|
+
userAgent?: string;
|
|
85
|
+
metadata?: string;
|
|
86
|
+
exceptionType?: string;
|
|
87
|
+
innerException?: string;
|
|
88
|
+
}
|
|
89
|
+
type UrlPattern = string | RegExp;
|
|
90
|
+
/** User-supplied configuration. Only `apiKey` and `apiUrl` are required. */
|
|
91
|
+
interface CherryBoardConfig {
|
|
92
|
+
/** Ingest API key (public, write-only). Sent as the `X-API-Key` header. */
|
|
93
|
+
apiKey: string;
|
|
94
|
+
/** API host root, e.g. "https://api.cherryboard.cherrypeak.eu". `/api/v1/errors/batch` is appended. */
|
|
95
|
+
apiUrl: string;
|
|
96
|
+
/** Environment tag shown in the dashboard metadata. Default "production". */
|
|
97
|
+
environment?: string;
|
|
98
|
+
/** Release identifier (git SHA / app version) for regression tracking + source-map matching. */
|
|
99
|
+
release?: string;
|
|
100
|
+
/** Master switch. When false, nothing is captured or sent. Default true. */
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
/** Fraction of events to keep, 0..1. Default 1 (100%). */
|
|
103
|
+
sampleRate?: number;
|
|
104
|
+
/** Max events per delivery request (backend caps batches at 100). Default 20. */
|
|
105
|
+
maxBatchSize?: number;
|
|
106
|
+
/** Debounce before an idle buffer is flushed, in ms. Default 4000. */
|
|
107
|
+
flushIntervalMs?: number;
|
|
108
|
+
/** Max events persisted to the offline queue. Default 100. */
|
|
109
|
+
maxQueueItems?: number;
|
|
110
|
+
/** Retry attempts for failed (5xx / network) deliveries. Default 3. */
|
|
111
|
+
maxRetries?: number;
|
|
112
|
+
/** Max breadcrumbs retained per event. Default 30. */
|
|
113
|
+
maxBreadcrumbs?: number;
|
|
114
|
+
/** Persist undelivered events to localStorage and retry later. Default true. */
|
|
115
|
+
offlineStorage?: boolean;
|
|
116
|
+
/** Capture `window` uncaught errors. Default true. */
|
|
117
|
+
captureUnhandledErrors?: boolean;
|
|
118
|
+
/** Capture unhandled promise rejections. Default true. */
|
|
119
|
+
captureUnhandledRejections?: boolean;
|
|
120
|
+
/** Capture failed resource loads (img/script/css). Default true. */
|
|
121
|
+
captureResourceErrors?: boolean;
|
|
122
|
+
/** Turn console.error/warn into breadcrumbs (never into events). Default true. */
|
|
123
|
+
captureConsole?: boolean;
|
|
124
|
+
/** Auto-record navigation / click / fetch breadcrumbs. Default true. */
|
|
125
|
+
autoBreadcrumbs?: boolean;
|
|
126
|
+
/** Drop events whose stack/URL matches any of these. */
|
|
127
|
+
denyUrls?: UrlPattern[];
|
|
128
|
+
/** If set, only keep events whose stack/URL matches one of these. */
|
|
129
|
+
allowUrls?: UrlPattern[];
|
|
130
|
+
/**
|
|
131
|
+
* Final hook to mutate, scrub, or drop (`return null`) an event before it is
|
|
132
|
+
* queued. Runs AFTER the built-in PII scrub, so anything you add here is your
|
|
133
|
+
* own responsibility to keep clean.
|
|
134
|
+
*/
|
|
135
|
+
beforeSend?: (event: CherryBoardEvent) => CherryBoardEvent | null;
|
|
136
|
+
/** Log SDK diagnostics to the console. Default false. */
|
|
137
|
+
debug?: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** Config with all defaults resolved. */
|
|
140
|
+
interface ResolvedConfig {
|
|
141
|
+
apiKey: string;
|
|
142
|
+
apiUrl: string;
|
|
143
|
+
environment: string;
|
|
144
|
+
release?: string;
|
|
145
|
+
enabled: boolean;
|
|
146
|
+
sampleRate: number;
|
|
147
|
+
maxBatchSize: number;
|
|
148
|
+
flushIntervalMs: number;
|
|
149
|
+
maxQueueItems: number;
|
|
150
|
+
maxRetries: number;
|
|
151
|
+
maxBreadcrumbs: number;
|
|
152
|
+
offlineStorage: boolean;
|
|
153
|
+
captureUnhandledErrors: boolean;
|
|
154
|
+
captureUnhandledRejections: boolean;
|
|
155
|
+
captureResourceErrors: boolean;
|
|
156
|
+
captureConsole: boolean;
|
|
157
|
+
autoBreadcrumbs: boolean;
|
|
158
|
+
denyUrls: UrlPattern[];
|
|
159
|
+
allowUrls: UrlPattern[];
|
|
160
|
+
beforeSend?: (event: CherryBoardEvent) => CherryBoardEvent | null;
|
|
161
|
+
debug: boolean;
|
|
162
|
+
}
|
|
163
|
+
/** Optional hint passed to captureException. */
|
|
164
|
+
interface CaptureHint {
|
|
165
|
+
severity?: Severity;
|
|
166
|
+
/** Merged into the event context (surfaced as dashboard "Additional data"). */
|
|
167
|
+
context?: Record<string, unknown>;
|
|
168
|
+
/** React component stack (from an error boundary). */
|
|
169
|
+
componentStack?: string;
|
|
170
|
+
/** Next.js error digest for correlation. */
|
|
171
|
+
digest?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface CherryBoardProviderProps {
|
|
175
|
+
config: CherryBoardConfig;
|
|
176
|
+
children: ReactNode;
|
|
177
|
+
/** Wrap children in an <ErrorBoundary> that reports render errors. Default false. */
|
|
178
|
+
withBoundary?: boolean;
|
|
179
|
+
/** Fallback UI for the built-in boundary (only used when withBoundary). */
|
|
180
|
+
fallback?: ErrorBoundaryProps['fallback'];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Convenience initializer for React apps. Calls `init(config)` once on mount.
|
|
184
|
+
*
|
|
185
|
+
* For the earliest possible capture (before first render), prefer calling
|
|
186
|
+
* `init(config)` at app bootstrap — e.g. Next.js `instrumentation-client.ts`.
|
|
187
|
+
*/
|
|
188
|
+
declare function CherryBoardProvider({ config, children, withBoundary, fallback, }: CherryBoardProviderProps): ReactNode;
|
|
189
|
+
|
|
190
|
+
/** Bounded ring buffer of recent activity attached to each outgoing event. */
|
|
191
|
+
declare class BreadcrumbBuffer {
|
|
192
|
+
private readonly max;
|
|
193
|
+
private readonly items;
|
|
194
|
+
private readonly teardown;
|
|
195
|
+
constructor(max: number);
|
|
196
|
+
add(crumb: Omit<Breadcrumb, 'timestamp'> & {
|
|
197
|
+
timestamp?: string;
|
|
198
|
+
}): void;
|
|
199
|
+
snapshot(): Breadcrumb[];
|
|
200
|
+
/** Auto-record navigation, clicks and fetch calls. Call close() to detach. */
|
|
201
|
+
install(): void;
|
|
202
|
+
private installHistory;
|
|
203
|
+
private installClicks;
|
|
204
|
+
private installFetch;
|
|
205
|
+
close(): void;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
declare class CherryBoardClient {
|
|
209
|
+
readonly config: ResolvedConfig;
|
|
210
|
+
readonly breadcrumbs: BreadcrumbBuffer;
|
|
211
|
+
private readonly transport;
|
|
212
|
+
private readonly deduper;
|
|
213
|
+
private readonly limiter;
|
|
214
|
+
private readonly queue;
|
|
215
|
+
private buffer;
|
|
216
|
+
private flushTimer;
|
|
217
|
+
private processing;
|
|
218
|
+
/** Epoch ms until which the server has told us to stop sending (429). */
|
|
219
|
+
private rateLimitedUntil;
|
|
220
|
+
/**
|
|
221
|
+
* Counts of events discarded before delivery, by reason. Without this an SDK
|
|
222
|
+
* drops events completely silently, which makes "why is this error missing?"
|
|
223
|
+
* unanswerable. Surfaced via getDiscardedEvents().
|
|
224
|
+
*/
|
|
225
|
+
private discarded;
|
|
226
|
+
private closed;
|
|
227
|
+
private scope;
|
|
228
|
+
private teardown;
|
|
229
|
+
constructor(config: CherryBoardConfig);
|
|
230
|
+
captureException(error: unknown, hint?: CaptureHint): void;
|
|
231
|
+
captureMessage(message: string, severity?: Severity, hint?: CaptureHint): void;
|
|
232
|
+
/**
|
|
233
|
+
* Events dropped before delivery, keyed by reason (sampled, deduped,
|
|
234
|
+
* rate_limited, filtered, queue_overflow, oversized, send_failed).
|
|
235
|
+
*/
|
|
236
|
+
getDiscardedEvents(): Readonly<Record<string, number>>;
|
|
237
|
+
private discard;
|
|
238
|
+
addBreadcrumb(crumb: Omit<Breadcrumb, 'timestamp'> & {
|
|
239
|
+
timestamp?: string;
|
|
240
|
+
}): void;
|
|
241
|
+
setUser(user: UserContext | null): void;
|
|
242
|
+
setTag(key: string, value: unknown): void;
|
|
243
|
+
setContext(key: string, value: unknown): void;
|
|
244
|
+
close(): void;
|
|
245
|
+
private process;
|
|
246
|
+
private buildContext;
|
|
247
|
+
private toPayload;
|
|
248
|
+
private enqueue;
|
|
249
|
+
private scheduleFlush;
|
|
250
|
+
/** Deliver buffered events. Pass keepalive=true on page unload. */
|
|
251
|
+
flush(keepalive?: boolean): Promise<void>;
|
|
252
|
+
private deliver;
|
|
253
|
+
private drainOffline;
|
|
254
|
+
private installLifecycle;
|
|
255
|
+
private debug;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Access the active client (or null before init). */
|
|
259
|
+
declare function useCherryBoard(): CherryBoardClient | null;
|
|
260
|
+
/**
|
|
261
|
+
* Returns a stable callback for manually reporting errors from event handlers
|
|
262
|
+
* or async code — the gap that error boundaries cannot cover.
|
|
263
|
+
*/
|
|
264
|
+
declare function useCaptureError(): (error: unknown, hint?: CaptureHint) => void;
|
|
265
|
+
/**
|
|
266
|
+
* Drop-in reporter for the Next.js App Router `error.tsx` / `global-error.tsx`
|
|
267
|
+
* boundaries. Call it inside a `useEffect` with the received error.
|
|
268
|
+
*/
|
|
269
|
+
declare function captureRouteError(error: Error & {
|
|
270
|
+
digest?: string;
|
|
271
|
+
}): void;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Initialize CherryBoard once, as early as possible (ideally at app bootstrap).
|
|
275
|
+
* Idempotent: a second call returns the existing client — call `close()` first
|
|
276
|
+
* to re-initialize with new config.
|
|
277
|
+
*/
|
|
278
|
+
declare function init(config: CherryBoardConfig): CherryBoardClient;
|
|
279
|
+
/** The active client, or null if `init` hasn't run. */
|
|
280
|
+
declare function getClient(): CherryBoardClient | null;
|
|
281
|
+
declare function isInitialized(): boolean;
|
|
282
|
+
/** Report an error (any thrown value). No-op until `init` has been called. */
|
|
283
|
+
declare function captureException(error: unknown, hint?: CaptureHint): void;
|
|
284
|
+
/** Report a message with an explicit severity (default "Info"). */
|
|
285
|
+
declare function captureMessage(message: string, severity?: Severity, hint?: CaptureHint): void;
|
|
286
|
+
declare function addBreadcrumb(crumb: Omit<Breadcrumb, 'timestamp'> & {
|
|
287
|
+
timestamp?: string;
|
|
288
|
+
}): void;
|
|
289
|
+
declare function setUser(user: UserContext | null): void;
|
|
290
|
+
declare function setTag(key: string, value: unknown): void;
|
|
291
|
+
declare function setContext(key: string, value: unknown): void;
|
|
292
|
+
/** Force-flush buffered events (returns a promise you can await before navigation). */
|
|
293
|
+
declare function flush(): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Reports a server-side Next.js error from `instrumentation.ts`:
|
|
296
|
+
*
|
|
297
|
+
* export const onRequestError = captureRequestError;
|
|
298
|
+
*
|
|
299
|
+
* The core runs fine outside the browser — it installs no window handlers there
|
|
300
|
+
* and delivers over `fetch` — so the same client covers RSC render, route
|
|
301
|
+
* handler and server action errors, which browser-only capture never sees.
|
|
302
|
+
*/
|
|
303
|
+
declare function captureRequestError(error: unknown, request?: {
|
|
304
|
+
path?: string;
|
|
305
|
+
method?: string;
|
|
306
|
+
headers?: Record<string, string | undefined>;
|
|
307
|
+
}, context?: {
|
|
308
|
+
routerKind?: string;
|
|
309
|
+
routePath?: string;
|
|
310
|
+
renderSource?: string;
|
|
311
|
+
}): void;
|
|
312
|
+
/**
|
|
313
|
+
* Counts of events discarded before delivery, keyed by reason. Useful when an
|
|
314
|
+
* error you expected never showed up.
|
|
315
|
+
*/
|
|
316
|
+
declare function getDiscardedEvents(): Readonly<Record<string, number>>;
|
|
317
|
+
/** Tear down all handlers and clear the active client. */
|
|
318
|
+
declare function close(): void;
|
|
319
|
+
|
|
320
|
+
export { type Breadcrumb, type CaptureHint, type CherryBoardConfig, type CherryBoardEvent, CherryBoardProvider, type CherryBoardProviderProps, ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, type ErrorPayload, SDK_VERSION, type Severity, type UserContext, addBreadcrumb, captureException, captureMessage, captureRequestError, captureRouteError, close, flush, getClient, getDiscardedEvents, init, isInitialized, setContext, setTag, setUser, useCaptureError, useCherryBoard };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {Component,useRef,useEffect,useCallback}from'react';import {jsx,Fragment}from'react/jsx-runtime';var h="1.0.1";function d(){return typeof window<"u"&&typeof document<"u"}function m(){return new Date().toISOString()}function a(r,e){return e<=0?"":r.length<=e?r:`${r.slice(0,e-1)}\u2026`}function C(r,e=24e3){let t=new WeakSet;try{let n=JSON.stringify(r,(o,s)=>{if(s instanceof Error)return {name:s.name,message:s.message,stack:s.stack};if(typeof s=="object"&&s!==null){if(t.has(s))return "[Circular]";t.add(s);}if(typeof s=="bigint")return s.toString();if(typeof s!="function")return s});return n===void 0||n.length>e?void 0:n}catch{return}}function B(r,e){return !r||e.length===0?false:e.some(t=>typeof t=="string"?r.includes(t):t.test(r))}function _(r){let e=r.replace(/\/+$/,"");return `${/\/api\/v\d+$/.test(e)?e:`${e}/api/v1`}/errors/batch`}var g=class{constructor(e){this.max=e;this.items=[];this.teardown=[];}add(e){this.items.push({timestamp:e.timestamp??m(),category:e.category,message:a(e.message,500),level:e.level,data:e.data}),this.items.length>this.max&&this.items.shift();}snapshot(){return this.items.slice()}install(){d()&&(this.installHistory(),this.installClicks(),this.installFetch());}installHistory(){try{let e=window.history,t=o=>{let s=e[o];if(typeof s!="function")return ()=>{};let i=(...l)=>{try{let u=l[2];typeof u=="string"&&this.add({category:"navigation",message:`${o} \u2192 ${u}`});}catch{}return s.apply(window.history,l)};return e[o]=i,()=>{e[o]===i&&(e[o]=s);}};this.teardown.push(t("pushState"),t("replaceState"));let n=()=>this.add({category:"navigation",message:`popstate \u2192 ${location.pathname}`});window.addEventListener("popstate",n),this.teardown.push(()=>window.removeEventListener("popstate",n));}catch{}}installClicks(){try{let e=t=>{let n=t.target;if(!n||typeof n.tagName!="string")return;let o=n.id?`#${n.id}`:"",s=typeof n.className=="string"?n.className:"",i=s?`.${s.split(/\s+/).filter(Boolean).slice(0,2).join(".")}`:"";this.add({category:"click",message:`${n.tagName.toLowerCase()}${o}${i}`});};window.addEventListener("click",e,{capture:!0,passive:!0}),this.teardown.push(()=>window.removeEventListener("click",e,{capture:!0}));}catch{}}installFetch(){try{let e=window.fetch;if(typeof e!="function")return;let t=(...n)=>{let[o,s]=n,i=s?.method??(typeof o=="object"&&o&&"method"in o?o.method:"GET"),l=typeof o=="string"?o:o instanceof URL?o.href:o.url;return e.apply(window,n).then(u=>(this.add({category:"fetch",message:`${i} ${l} \u2192 ${u.status}`,level:u.ok?void 0:"Warning"}),u),u=>{throw this.add({category:"fetch",message:`${i} ${l} \u2192 failed`,level:"Warning"}),u})};window.fetch=t,this.teardown.push(()=>{window.fetch===t&&(window.fetch=e);});}catch{}}close(){for(let e of this.teardown.splice(0))try{e();}catch{}}};var y=class{constructor(e=4e3,t=100){this.windowMs=e;this.max=t;this.seen=new Map;}shouldSend(e){let t=Date.now(),n=this.seen.get(e);if(n!==void 0&&t-n<this.windowMs)return false;if(this.seen.set(e,t),this.seen.size>this.max){let o=this.seen.keys().next().value;o!==void 0&&this.seen.delete(o);}return true}};function D(r,e){let t=(e??"").split(`
|
|
3
|
+
`).slice(0,4).map(n=>n.replace(/:\d+:\d+/g,"").replace(/\?[^\s)]*/g,"").replace(/https?:\/\/[^/]+/g,"").trim()).join("|");return `${r}::${t}`.slice(0,500)}function j(r){if(!d())return ()=>{};let e=r.config,t=[];if(e.captureUnhandledErrors||e.captureResourceErrors){let n=o=>{try{let s=o.target;if(s instanceof HTMLElement){if(!e.captureResourceErrors)return;let l=s,u=l.src||l.href;if(!u)return;r.captureMessage(`Resource failed to load: ${u}`,"Warning",{context:{resource:s.tagName.toLowerCase(),url:u}});return}if(!e.captureUnhandledErrors)return;let i=o;if(!i.error&&(!i.message||i.message==="Script error."))return;r.captureException(i.error??i.message);}catch{}};window.addEventListener("error",n,true),t.push(()=>window.removeEventListener("error",n,true));}if(e.captureUnhandledRejections){let n=o=>{try{r.captureException(o.reason??"Unhandled promise rejection",{context:{unhandledRejection:!0}});}catch{}};window.addEventListener("unhandledrejection",n),t.push(()=>window.removeEventListener("unhandledrejection",n));}if(e.captureConsole){let n=console;for(let o of ["error","warn"]){let s=n[o];if(typeof s!="function")continue;let i=(...l)=>{try{r.addBreadcrumb({category:"console",level:o==="error"?"Error":"Warning",message:l.map(q).join(" ").slice(0,500)});}catch{}return s.apply(console,l)};n[o]=i,t.push(()=>{n[o]===i&&(n[o]=s);});}}return ()=>{for(let n of t.splice(0))try{n();}catch{}}}function q(r){if(typeof r=="string")return r;if(r instanceof Error)return `${r.name}: ${r.message}`;try{return JSON.stringify(r)??String(r)}catch{return String(r)}}var b=2e3,S=8e3,R=4e3;function $(r){if(r instanceof Error)return {message:a(r.message||r.name||"Error",b),exceptionType:r.name||"Error",stackTrace:r.stack?a(r.stack,S):void 0,innerException:P(r.cause)};if(typeof r=="string")return {message:a(r,b),exceptionType:"Error"};if(r&&typeof r=="object"){let e=r,t=typeof e.message=="string"?e.message:O(e)??Object.prototype.toString.call(r),n=typeof e.name=="string"?e.name:"Error",o=typeof e.stack=="string"?e.stack:void 0;return {message:a(t,b),exceptionType:n,stackTrace:o?a(o,S):void 0,innerException:P(e.cause)}}return {message:a(String(r),b),exceptionType:"Error"}}function L(r){if(r.stackTrace)return r;try{let e=new Error(r.message).stack;if(e){let t=e.split(`
|
|
4
|
+
`).slice(2).join(`
|
|
5
|
+
`);return {...r,stackTrace:a(t||e,S)}}}catch{}return r}function P(r){if(r==null)return;if(r instanceof Error){let t=[`${r.name}: ${r.message}`];r.stack&&t.push(r.stack);let n=P(r.cause);return n&&t.push(`Caused by: ${n}`),a(t.join(`
|
|
6
|
+
`),R)}let e=O(r);return e?a(e,R):a(String(r),R)}function O(r){try{let e=JSON.stringify(r);return e==="{}"?void 0:e}catch{return}}var U="cherryboard:queue:v1",x=class{constructor(e,t){this.enabled=e;this.maxItems=t;}get store(){if(!this.enabled||!d())return null;try{return window.localStorage}catch{return null}}push(e){let t=this.store;if(!(!t||e.length===0))try{let n=this.read().concat(e).slice(-this.maxItems);t.setItem(U,JSON.stringify(n));}catch{}}read(){let e=this.store;if(!e)return [];try{let t=e.getItem(U);if(!t)return [];let n=JSON.parse(t);return Array.isArray(n)?n:[]}catch{return []}}drain(){let e=this.read();return this.clear(),e}clear(){let e=this.store;if(e)try{e.removeItem(U);}catch{}}};var v=class{constructor(e=30,t=5){this.capacity=e;this.refillPerSec=t;this.tokens=e,this.last=Date.now();}allow(){let e=Date.now(),t=(e-this.last)/1e3;return this.tokens=Math.min(this.capacity,this.tokens+t*this.refillPerSec),this.last=e,this.tokens>=1?(this.tokens-=1,true):false}};var z=["password","passwd","secret","token","apikey","api_key","authorization","auth","cookie","session","credit","card","cvv","ssn"],W=["token","access_token","apikey","api_key","email","password","code","secret"],T="[Filtered]",J=/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;function A(r){let e=r;for(let t of W)e=e.replace(new RegExp(`([?&]${t}=)[^&#\\s]*`,"gi"),`$1${T}`);return e}function w(r,e=0){if(e>6)return r;if(typeof r=="string")return A(r).replace(J,T);if(Array.isArray(r))return r.map(t=>w(t,e+1));if(r&&typeof r=="object"){let t={};for(let[n,o]of Object.entries(r)){let s=n.toLowerCase();t[n]=z.some(i=>s.includes(i))?T:w(o,e+1);}return t}return r}function H(r){return {...r,message:A(r.message),context:w(r.context),breadcrumbs:r.breadcrumbs.map(e=>({...e,message:A(e.message),data:e.data?w(e.data):void 0}))}}var E=class{constructor(e,t){this.apiKey=t;this.endpoint=_(e);let n=typeof fetch=="function"?fetch.bind(globalThis):void 0;this.fetchImpl=n??(()=>Promise.reject(new Error("fetch unavailable")));}async send(e,t=false){if(e.length===0)return {ok:true,retryable:false,status:204};try{let n=await this.fetchImpl(this.endpoint,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json","X-API-Key":this.apiKey},body:JSON.stringify({errors:e}),keepalive:t,mode:"cors",credentials:"omit"});if(n.status===429)return {ok:!1,retryable:!1,status:429,retryAfterMs:Q(n.headers?.get?.("Retry-After"))};let o=n.status>=500;return {ok:n.ok,retryable:o,status:n.status}}catch{return {ok:false,retryable:true,status:0}}}};function Q(r){if(!r)return 6e4;let t=Number(r);if(Number.isFinite(t)&&t>=0)return Math.min(t*1e3,36e5);let n=Date.parse(r);return Number.isNaN(n)?6e4:Math.min(Math.max(n-Date.now(),0),36e5)}function I(r,e,t){return Math.max(e,Math.min(t,r))}function Y(r){return new Promise(e=>setTimeout(e,r))}var V=3e3;function G(r){return {apiKey:r.apiKey,apiUrl:r.apiUrl,environment:r.environment??"production",release:r.release,enabled:r.enabled??true,sampleRate:I(r.sampleRate??1,0,1),maxBatchSize:I(r.maxBatchSize??20,1,100),flushIntervalMs:r.flushIntervalMs??4e3,maxQueueItems:r.maxQueueItems??100,maxRetries:r.maxRetries??3,maxBreadcrumbs:r.maxBreadcrumbs??30,offlineStorage:r.offlineStorage??true,captureUnhandledErrors:r.captureUnhandledErrors??true,captureUnhandledRejections:r.captureUnhandledRejections??true,captureResourceErrors:r.captureResourceErrors??true,captureConsole:r.captureConsole??true,autoBreadcrumbs:r.autoBreadcrumbs??true,denyUrls:r.denyUrls??[],allowUrls:r.allowUrls??[],beforeSend:r.beforeSend,debug:r.debug??false}}var k=class{constructor(e){this.buffer=[];this.flushTimer=null;this.processing=false;this.rateLimitedUntil=0;this.discarded={};this.closed=false;this.scope={tags:{}};this.teardown=[];this.config=G(e),this.transport=new E(this.config.apiUrl,this.config.apiKey),this.deduper=new y(V),this.limiter=new v,this.queue=new x(this.config.offlineStorage,this.config.maxQueueItems),this.breadcrumbs=new g(this.config.maxBreadcrumbs),this.config.enabled&&(this.config.autoBreadcrumbs&&this.breadcrumbs.install(),this.teardown.push(j(this)),this.installLifecycle(),this.drainOffline());}captureException(e,t){try{this.process(L($(e)),t?.severity??"Error",t);}catch(n){this.debug("captureException failed",n);}}captureMessage(e,t="Info",n){try{this.process({message:a(e,2e3),exceptionType:"Message"},n?.severity??t,n);}catch(o){this.debug("captureMessage failed",o);}}getDiscardedEvents(){return {...this.discarded}}discard(e,t=1){this.discarded[e]=(this.discarded[e]??0)+t;}addBreadcrumb(e){this.breadcrumbs.add(e);}setUser(e){this.scope.user=e??void 0;}setTag(e,t){this.scope.tags[e]=t;}setContext(e,t){this.scope.tags[e]=t;}close(){this.closed=true,this.flushTimer!==null&&(clearTimeout(this.flushTimer),this.flushTimer=null),this.flush(true),this.breadcrumbs.close();for(let e of this.teardown.splice(0))try{e();}catch{}}process(e,t,n){if(!this.config.enabled||this.closed)return;if(this.config.sampleRate<1&&Math.random()>this.config.sampleRate){this.discard("sampled");return}let o=`${e.stackTrace??""}
|
|
7
|
+
${d()?location.href:""}`;if(B(o,this.config.denyUrls)){this.discard("filtered");return}if(this.config.allowUrls.length>0&&!B(o,this.config.allowUrls)){this.discard("filtered");return}if(!this.deduper.shouldSend(D(e.message,e.stackTrace))){this.discard("deduped");return}let s={message:e.message,severity:t,timestamp:m(),exceptionType:e.exceptionType,stackTrace:e.stackTrace,innerException:e.innerException,userId:this.scope.user?.id,requestPath:d()?location.pathname:void 0,userAgent:d()?navigator.userAgent:void 0,context:this.buildContext(n),breadcrumbs:this.breadcrumbs.snapshot()};if(s=H(s),this.config.beforeSend){let i=this.config.beforeSend(s);if(!i){this.discard("filtered");return}s=i;}this.enqueue(this.toPayload(s));}buildContext(e){let t={sdk:{name:"cherryboard-web",version:h},environment:this.config.environment};return this.config.release&&(t.release=this.config.release),d()&&(t.url=location.href,document.referrer&&(t.referrer=document.referrer),t.language=navigator.language,t.viewport={width:window.innerWidth,height:window.innerHeight}),this.scope.user&&(t.user=this.scope.user),Object.keys(this.scope.tags).length>0&&(t.tags={...this.scope.tags}),e?.componentStack&&(t.componentStack=e.componentStack),e?.digest&&(t.digest=e.digest),e?.context&&Object.assign(t,e.context),t}toPayload(e){let t=C({...e.context,breadcrumbs:e.breadcrumbs});return t===void 0&&(t=C(e.context)),{message:e.message,stackTrace:e.stackTrace,severity:e.severity,timestamp:e.timestamp,userId:e.userId,requestPath:e.requestPath,userAgent:e.userAgent,exceptionType:e.exceptionType,innerException:e.innerException,metadata:t}}enqueue(e){this.buffer.push(e),this.buffer.length>=this.config.maxBatchSize?this.flush():this.scheduleFlush();}scheduleFlush(){this.closed||this.flushTimer===null&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.flush();},this.config.flushIntervalMs));}async flush(e=false){if(this.flushTimer!==null&&(clearTimeout(this.flushTimer),this.flushTimer=null),this.buffer.length!==0){if(e){if(Date.now()<this.rateLimitedUntil)return;for(;this.buffer.length>0;){let t=this.buffer.splice(0,this.config.maxBatchSize);if(!this.limiter.allow())break;this.transport.send(t,true);}return}if(Date.now()<this.rateLimitedUntil){this.scheduleFlush();return}if(!this.processing){this.processing=true;try{for(;this.buffer.length>0;){let t=this.buffer.splice(0,this.config.maxBatchSize);if(!this.limiter.allow()){this.debug("rate limited; dropping",t.length,"events"),this.discard("rate_limited",t.length);break}if(!await this.deliver(t)){this.queue.push(t);break}}}finally{this.processing=false;}}}}async deliver(e){for(let t=0;t<=this.config.maxRetries;t++){let n=await this.transport.send(e,false);if(n.ok)return true;if(n.status===429)return this.rateLimitedUntil=Date.now()+(n.retryAfterMs??6e4),this.debug("rate limited by server; pausing for",n.retryAfterMs,"ms"),false;if(!n.retryable)return this.debug("non-retryable response",n.status,"\u2014 dropping batch"),this.discard("send_failed",e.length),true;if(t===this.config.maxRetries)return false;await Y(I(2**t*1e3+Math.random()*250,0,15e3));}return false}drainOffline(){let e=this.queue.drain();e.length>0&&(this.buffer.push(...e),this.scheduleFlush());}installLifecycle(){if(!d())return;let e=()=>{document.visibilityState==="hidden"&&this.flush(true);},t=()=>{this.flush(true);},n=()=>this.drainOffline();document.addEventListener("visibilitychange",e),window.addEventListener("pagehide",t),window.addEventListener("online",n),this.teardown.push(()=>{document.removeEventListener("visibilitychange",e),window.removeEventListener("pagehide",t),window.removeEventListener("online",n);});}debug(...e){this.config.debug&&console.warn("[cherryboard]",...e);}};var K="__CHERRYBOARD__";function c(){let r=globalThis,e=r[K];return e||(e={},r[K]=e),e}function N(r){let e=c();return e.client||(e.client=new k(r)),e.client}function M(){return c().client??null}function X(){return c().client!=null}function p(r,e){c().client?.captureException(r,e);}function Z(r,e,t){c().client?.captureMessage(r,e,t);}function ee(r){c().client?.addBreadcrumb(r);}function re(r){c().client?.setUser(r);}function te(r,e){c().client?.setTag(r,e);}function ne(r,e){c().client?.setContext(r,e);}function oe(){let r=c().client;return r?r.flush():Promise.resolve()}function se(r,e,t){c().client?.captureException(r,{severity:"Error",context:{source:"nextjs-server",...e?.path?{requestPath:e.path}:{},...e?.method?{requestMethod:e.method}:{},...t??{}}});}function ie(){return c().client?.getDiscardedEvents()??{}}function ae(){let r=c();r.client?.close(),r.client=void 0;}var f=class extends Component{constructor(){super(...arguments);this.state={error:null};this.reset=()=>{this.setState({error:null});};}static getDerivedStateFromError(t){return {error:t}}componentDidCatch(t,n){p(t,{severity:"Error",componentStack:n.componentStack??void 0,context:{source:"react-error-boundary"}}),this.props.onError?.(t,n);}componentDidUpdate(t){this.state.error&&!ue(t.resetKeys,this.props.resetKeys)&&this.reset();}render(){let{error:t}=this.state;if(t){let{fallback:n}=this.props;return typeof n=="function"?n({error:t,reset:this.reset}):n??null}return this.props.children}};function ue(r,e){return r===e?true:!r||!e||r.length!==e.length?false:r.every((t,n)=>Object.is(t,e[n]))}function pe({config:r,children:e,withBoundary:t=false,fallback:n}){let o=useRef(r);return useEffect(()=>{typeof window<"u"&&N(o.current);},[]),t?jsx(f,{fallback:n,children:e}):jsx(Fragment,{children:e})}function me(){return M()}function ge(){return useCallback((r,e)=>p(r,e),[])}function ye(r){p(r,{severity:"Error",digest:r.digest,context:{source:"next-error-boundary"}});}export{pe as CherryBoardProvider,f as ErrorBoundary,h as SDK_VERSION,ee as addBreadcrumb,p as captureException,Z as captureMessage,se as captureRequestError,ye as captureRouteError,ae as close,oe as flush,M as getClient,ie as getDiscardedEvents,N as init,X as isInitialized,ne as setContext,te as setTag,re as setUser,ge as useCaptureError,me as useCherryBoard};
|