@catdoes/watch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,486 @@
1
+ export { WatchErrorBoundary, WatchErrorBoundaryProps, withWatchErrorBoundary } from './react.js';
2
+ import 'react';
3
+
4
+ /**
5
+ * CatDoes Watch SDK - Type Definitions
6
+ *
7
+ * These types define the structure of error events, configuration,
8
+ * and other data used by the Watch SDK.
9
+ */
10
+ /**
11
+ * Configuration options for initializing the Watch client.
12
+ */
13
+ interface WatchConfig {
14
+ /**
15
+ * The API key for authenticating with CatDoes Watch.
16
+ * Format: cd_watch_xxxxx
17
+ */
18
+ apiKey: string;
19
+ /**
20
+ * The endpoint URL for the ingestion API.
21
+ * @default "https://app.catdoes.com/api/watch/ingest"
22
+ */
23
+ endpoint?: string;
24
+ /**
25
+ * The environment to report errors for.
26
+ * Auto-detected from __DEV__ if not specified.
27
+ * @default Auto-detected
28
+ */
29
+ environment?: "development" | "production";
30
+ /**
31
+ * Whether to capture console.error calls as errors.
32
+ * This can be noisy and is disabled by default.
33
+ * @default false
34
+ */
35
+ captureConsoleErrors?: boolean;
36
+ /**
37
+ * Maximum number of breadcrumbs to store.
38
+ * @default 20
39
+ */
40
+ maxBreadcrumbs?: number;
41
+ /**
42
+ * Maximum number of events to buffer before flushing.
43
+ * @default 10
44
+ */
45
+ maxBufferSize?: number;
46
+ /**
47
+ * Interval in milliseconds between automatic flushes.
48
+ * @default 5000
49
+ */
50
+ flushInterval?: number;
51
+ /**
52
+ * Callback invoked before sending an event.
53
+ * Return null to drop the event, or modify and return it.
54
+ */
55
+ beforeSend?: (event: WatchEvent) => WatchEvent | null;
56
+ /**
57
+ * Enable debug logging to console.
58
+ * @default false
59
+ */
60
+ debug?: boolean;
61
+ /**
62
+ * Initial context to attach to all events.
63
+ */
64
+ initialContext?: Record<string, unknown>;
65
+ /**
66
+ * Time window in milliseconds to consider errors as duplicates.
67
+ * Errors with the same key occurring within this window will be deduplicated.
68
+ * @default 5000
69
+ */
70
+ dedupWindowMs?: number;
71
+ /**
72
+ * Maximum number of recent error keys to keep in memory for deduplication.
73
+ * When exceeded, the oldest keys will be evicted.
74
+ * @default 500
75
+ */
76
+ dedupMaxEntries?: number;
77
+ }
78
+ /**
79
+ * Required configuration with defaults applied.
80
+ */
81
+ interface WatchConfigResolved {
82
+ apiKey: string;
83
+ endpoint: string;
84
+ environment: "development" | "production";
85
+ captureConsoleErrors: boolean;
86
+ maxBreadcrumbs: number;
87
+ maxBufferSize: number;
88
+ flushInterval: number;
89
+ beforeSend: (event: WatchEvent) => WatchEvent | null;
90
+ debug: boolean;
91
+ dedupWindowMs: number;
92
+ dedupMaxEntries: number;
93
+ }
94
+ /**
95
+ * Device and environment information collected automatically.
96
+ */
97
+ interface DeviceInfo {
98
+ deviceModel?: string;
99
+ deviceName?: string;
100
+ deviceType?: string;
101
+ brand?: string;
102
+ manufacturer?: string;
103
+ modelName?: string;
104
+ isDevice?: boolean;
105
+ isEmulator?: boolean;
106
+ isTablet?: boolean;
107
+ osName?: string;
108
+ osVersion?: string;
109
+ osBuildId?: string;
110
+ platformApiLevel?: number;
111
+ appVersion?: string;
112
+ appBuildNumber?: string;
113
+ appName?: string;
114
+ bundleId?: string;
115
+ runtimeVersion?: string;
116
+ expoVersion?: string;
117
+ nativeAppVersion?: string;
118
+ nativeBuildVersion?: string;
119
+ screenWidth?: number;
120
+ screenHeight?: number;
121
+ screenScale?: number;
122
+ locale?: string;
123
+ timezone?: string;
124
+ networkType?: string;
125
+ isConnected?: boolean;
126
+ browserName?: string;
127
+ browserVersion?: string;
128
+ userAgent?: string;
129
+ }
130
+ /**
131
+ * A breadcrumb representing an action or event before an error.
132
+ */
133
+ interface Breadcrumb {
134
+ /**
135
+ * The type of breadcrumb.
136
+ */
137
+ type: "navigation" | "ui" | "http" | "console" | "custom";
138
+ /**
139
+ * A human-readable message describing the breadcrumb.
140
+ */
141
+ message: string;
142
+ /**
143
+ * ISO 8601 timestamp of when the breadcrumb was created.
144
+ */
145
+ timestamp: string;
146
+ /**
147
+ * Additional data associated with the breadcrumb.
148
+ */
149
+ data?: Record<string, unknown>;
150
+ }
151
+ /**
152
+ * An error event to be sent to CatDoes Watch.
153
+ */
154
+ interface WatchEvent {
155
+ /**
156
+ * The error message.
157
+ */
158
+ message: string;
159
+ /**
160
+ * The stack trace of the error.
161
+ */
162
+ stack?: string;
163
+ /**
164
+ * React component stack trace.
165
+ */
166
+ componentStack?: string;
167
+ /**
168
+ * The filename where the error occurred.
169
+ */
170
+ filename?: string;
171
+ /**
172
+ * The line number where the error occurred.
173
+ */
174
+ lineno?: number;
175
+ /**
176
+ * The column number where the error occurred.
177
+ */
178
+ colno?: number;
179
+ /**
180
+ * ISO 8601 timestamp of when the error occurred.
181
+ */
182
+ timestamp: string;
183
+ /**
184
+ * The environment where the error occurred.
185
+ */
186
+ environment: "development" | "production";
187
+ /**
188
+ * The platform where the error occurred.
189
+ */
190
+ platform: "ios" | "android" | "web";
191
+ /**
192
+ * A unique identifier for the current session.
193
+ */
194
+ sessionId: string;
195
+ /**
196
+ * Device and environment information.
197
+ */
198
+ deviceInfo?: DeviceInfo;
199
+ /**
200
+ * Additional context data.
201
+ */
202
+ extra?: Record<string, unknown>;
203
+ /**
204
+ * Breadcrumbs leading up to the error.
205
+ */
206
+ breadcrumbs?: Breadcrumb[];
207
+ /**
208
+ * SDK version for debugging. Helps correlate reports across SDK releases.
209
+ */
210
+ sdkVersion?: string;
211
+ }
212
+ /**
213
+ * Input for adding a breadcrumb (timestamp is auto-generated).
214
+ */
215
+ type BreadcrumbInput = Omit<Breadcrumb, "timestamp">;
216
+ /**
217
+ * Response from the ingestion API.
218
+ */
219
+ interface IngestResponse {
220
+ accepted?: number;
221
+ filtered?: boolean;
222
+ error?: string;
223
+ retryAfter?: number;
224
+ }
225
+
226
+ /**
227
+ * CatDoes Watch SDK - Transport Layer
228
+ *
229
+ * Handles HTTP communication with the CatDoes Watch ingestion API.
230
+ * Features:
231
+ * - Batching: Groups multiple events into single requests
232
+ * - Retry with exponential backoff on failures
233
+ * - Respects rate limiting (429 responses)
234
+ * - Silent failures (never throws to avoid breaking the app)
235
+ */
236
+
237
+ interface FlushOptions {
238
+ /**
239
+ * Hint browsers to allow the request to outlive the page lifecycle.
240
+ */
241
+ keepalive?: boolean;
242
+ }
243
+
244
+ /**
245
+ * CatDoes Watch SDK - Main Client
246
+ *
247
+ * The primary interface for the CatDoes Watch error tracking SDK.
248
+ * Implements a singleton pattern for ease of use.
249
+ */
250
+
251
+ /**
252
+ * The main CatDoes Watch client class.
253
+ */
254
+ declare class WatchClient {
255
+ private static instance;
256
+ private config;
257
+ private transport;
258
+ private breadcrumbs;
259
+ private context;
260
+ private user;
261
+ private isInitialized;
262
+ private recentErrors;
263
+ private recentErrorsCleanupTimer;
264
+ private constructor();
265
+ /**
266
+ * Initializes the Watch client with the given configuration.
267
+ */
268
+ static init(config: WatchConfig): WatchClient;
269
+ /**
270
+ * Gets the existing Watch client instance, or null if not initialized.
271
+ */
272
+ static getInstance(): WatchClient | null;
273
+ /**
274
+ * Captures an error and sends it to CatDoes Watch.
275
+ */
276
+ captureError(error: Error, extra?: Record<string, unknown>): void;
277
+ /**
278
+ * Captures a message as an error.
279
+ */
280
+ captureMessage(message: string, level?: "info" | "warning" | "error"): void;
281
+ /**
282
+ * Adds a breadcrumb to the trail.
283
+ */
284
+ addBreadcrumb(breadcrumb: BreadcrumbInput): void;
285
+ /**
286
+ * Sets a context value that will be attached to all future events.
287
+ */
288
+ setContext(key: string, value: unknown): void;
289
+ /**
290
+ * Clears a context value.
291
+ */
292
+ clearContext(key: string): void;
293
+ /**
294
+ * Sets user information to attach to events.
295
+ */
296
+ setUser(user: {
297
+ id?: string;
298
+ [key: string]: unknown;
299
+ } | null): void;
300
+ /**
301
+ * Flushes all queued events immediately.
302
+ */
303
+ flush(options?: FlushOptions): Promise<void>;
304
+ /**
305
+ * Gets the current configuration.
306
+ */
307
+ getConfig(): Readonly<WatchConfigResolved>;
308
+ /**
309
+ * Checks if the client is initialized and ready to capture events.
310
+ */
311
+ get initialized(): boolean;
312
+ private buildEvent;
313
+ private shouldCapture;
314
+ private ensureStack;
315
+ private getErrorKey;
316
+ private markErrorAsSeen;
317
+ private hasSeenErrorRecently;
318
+ private scheduleRecentErrorsCleanup;
319
+ private pruneRecentErrors;
320
+ }
321
+ /**
322
+ * Static interface for convenience methods.
323
+ */
324
+ declare const Watch: {
325
+ init(config: WatchConfig): WatchClient;
326
+ getInstance(): WatchClient | null;
327
+ captureError(error: Error, extra?: Record<string, unknown>): void;
328
+ captureMessage(message: string, level?: "info" | "warning" | "error"): void;
329
+ addBreadcrumb(breadcrumb: BreadcrumbInput): void;
330
+ setContext(key: string, value: unknown): void;
331
+ setUser(user: {
332
+ id?: string;
333
+ [key: string]: unknown;
334
+ } | null): void;
335
+ flush(options?: FlushOptions): Promise<void>;
336
+ };
337
+
338
+ /**
339
+ * CatDoes Watch SDK - Global Error Handlers
340
+ *
341
+ * Sets up global error handlers to automatically capture unhandled errors.
342
+ * Supports both web (window.onerror) and React Native (ErrorUtils).
343
+ */
344
+
345
+ /**
346
+ * Sets up global error handlers for the given Watch client.
347
+ */
348
+ declare function setupGlobalHandlers(client: WatchClient): void;
349
+ /**
350
+ * Sets up console.error interception (optional, can be noisy).
351
+ */
352
+ declare function setupConsoleErrorCapture(client: WatchClient): void;
353
+ /**
354
+ * Removes all installed global handlers.
355
+ */
356
+ declare function removeGlobalHandlers(): void;
357
+
358
+ /**
359
+ * CatDoes Watch SDK - Session Management
360
+ *
361
+ * Generates and manages a unique session ID for the current app session.
362
+ * The session ID is used to group errors from the same user session.
363
+ */
364
+ /**
365
+ * Gets the current session ID, generating one if it doesn't exist.
366
+ * The session ID persists for the lifetime of the app process.
367
+ */
368
+ declare function getSessionId(): string;
369
+ /**
370
+ * Resets the session ID, forcing a new one to be generated.
371
+ * This can be called when a user logs out or the app wants to start fresh.
372
+ */
373
+ declare function resetSession(): void;
374
+ /**
375
+ * Sets a specific session ID (useful for testing or migration).
376
+ */
377
+ declare function setSessionId(sessionId: string): void;
378
+
379
+ /**
380
+ * CatDoes Watch SDK - Context Collection
381
+ *
382
+ * Collects device and environment information to attach to error events.
383
+ * Uses Expo and React Native APIs where available.
384
+ */
385
+
386
+ /**
387
+ * Gets the current platform: 'ios', 'android', or 'web'.
388
+ */
389
+ declare function getPlatform(): "ios" | "android" | "web";
390
+ /**
391
+ * Gets the current environment based on __DEV__ flag.
392
+ */
393
+ declare function getEnvironment(): "development" | "production";
394
+ /**
395
+ * Collects device and environment information.
396
+ * Only includes fields that are in the server's allowlist.
397
+ */
398
+ declare function collectDeviceInfo(): DeviceInfo;
399
+ declare function getCachedDeviceInfo(): DeviceInfo;
400
+ /**
401
+ * Clears the cached device info, forcing re-collection on next call.
402
+ */
403
+ declare function clearDeviceInfoCache(): void;
404
+
405
+ /**
406
+ * CatDoes Watch SDK - Breadcrumb Management
407
+ *
408
+ * Manages a rolling buffer of breadcrumbs that are attached to error events.
409
+ * Breadcrumbs help understand the sequence of events leading to an error.
410
+ */
411
+
412
+ /**
413
+ * Manages a collection of breadcrumbs with a maximum size.
414
+ */
415
+ declare class BreadcrumbManager {
416
+ private breadcrumbs;
417
+ private maxBreadcrumbs;
418
+ constructor(maxBreadcrumbs?: number);
419
+ /**
420
+ * Adds a new breadcrumb to the collection.
421
+ * If the collection is at max capacity, the oldest breadcrumb is removed.
422
+ */
423
+ add(input: BreadcrumbInput): void;
424
+ /**
425
+ * Gets a copy of all current breadcrumbs.
426
+ */
427
+ getAll(): Breadcrumb[];
428
+ /**
429
+ * Clears all breadcrumbs.
430
+ */
431
+ clear(): void;
432
+ /**
433
+ * Gets the current count of breadcrumbs.
434
+ */
435
+ get count(): number;
436
+ /**
437
+ * Updates the maximum number of breadcrumbs.
438
+ */
439
+ setMaxBreadcrumbs(max: number): void;
440
+ }
441
+ /**
442
+ * Creates a navigation breadcrumb.
443
+ */
444
+ declare function createNavigationBreadcrumb(from: string, to: string): BreadcrumbInput;
445
+ /**
446
+ * Creates a UI interaction breadcrumb.
447
+ */
448
+ declare function createUIBreadcrumb(action: string, target?: string): BreadcrumbInput;
449
+ /**
450
+ * Creates an HTTP request breadcrumb.
451
+ */
452
+ declare function createHttpBreadcrumb(method: string, url: string, statusCode?: number): BreadcrumbInput;
453
+ /**
454
+ * Creates a console breadcrumb.
455
+ */
456
+ declare function createConsoleBreadcrumb(level: "log" | "warn" | "error" | "info", message: string): BreadcrumbInput;
457
+ /**
458
+ * Creates a custom breadcrumb.
459
+ */
460
+ declare function createCustomBreadcrumb(message: string, data?: Record<string, unknown>): BreadcrumbInput;
461
+
462
+ /**
463
+ * CatDoes Watch SDK - Symbolication Helpers
464
+ *
465
+ * Utilities for processing stack traces and file paths.
466
+ */
467
+ /**
468
+ * Produces a readable file path from a Metro/URL-style file reference.
469
+ * - Strips query params
470
+ * - Prefers repo-relative paths like app/... or src/...
471
+ * - Falls back to URL pathname
472
+ */
473
+ declare function deriveReadableFile(file: string): string;
474
+ /**
475
+ * Checks if a derived filename is usable (not a noisy bundle/node_modules path)
476
+ */
477
+ declare function isUsableFilename(filename: string): boolean;
478
+
479
+ /**
480
+ * CatDoes Watch SDK - Version
481
+ *
482
+ * Keep this value updated when making SDK changes.
483
+ */
484
+ declare const SDK_VERSION = "1.0.0";
485
+
486
+ export { type Breadcrumb, type BreadcrumbInput, BreadcrumbManager, type DeviceInfo, type IngestResponse, SDK_VERSION, Watch, WatchClient, type WatchConfig, type WatchConfigResolved, type WatchEvent, clearDeviceInfoCache, collectDeviceInfo, createConsoleBreadcrumb, createCustomBreadcrumb, createHttpBreadcrumb, createNavigationBreadcrumb, createUIBreadcrumb, deriveReadableFile, getCachedDeviceInfo, getEnvironment, getPlatform, getSessionId, isUsableFilename, removeGlobalHandlers, resetSession, setSessionId, setupConsoleErrorCapture, setupGlobalHandlers };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ var t=require("react-native"),e=require("react"),s=require("react/jsx-runtime");function i(t){return t&&t.t?t:{default:t}}var r=i(e),n=(t=>"undefined"!=typeof require?require:"undefined"!=typeof Proxy?new Proxy(t,{get:(t,e)=>("undefined"!=typeof require?require:t)[e]}):t)(function(t){if("undefined"!=typeof require)return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')}),o=null;try{o=n("@react-native-async-storage/async-storage").default}catch{}function h(t){return"object"==typeof t&&null!==t}function a(t){return!!h(t)&&"string"==typeof t.message&&"string"==typeof t.timestamp}var c=class{constructor(t){if(this.queue=[],this.flushTimer=null,this.backoffMs=0,this.backoffUntil=0,this.flushPromise=null,this.consecutiveFailures=0,this.disabledUntil=0,this.disabledReason=null,this.persistRequestedVersion=0,this.persistCompletedVersion=0,this.persistInFlight=null,this.config=t,this.maxConsecutiveFailures=t.maxConsecutiveFailures??50,this.failuresCooldownMs=t.failuresCooldownMs??9e5,this.disableOnAuthError=t.disableOnAuthError??!0,!t.apiKey)return this.config.debug&&console.warn("[CatDoes Watch] No API key; transport disabled."),this.disabledReason="auth",this.disabledUntil=1/0,this.apiKeyHash="",this.storageKey="",void(this.hydratePromise=Promise.resolve());this.apiKeyHash=function(t){let e=5381;for(let s=0;s<t.length;s++)e=(e<<5)+e^t.charCodeAt(s);return(e>>>0).toString(36)}(t.apiKey),this.storageKey=`@catdoes_watch_queue_v1_${this.apiKeyHash}`,this.hydratePromise=this.hydrateQueue().catch(t=>{this.config.debug&&console.debug("[CatDoes Watch] Failed to hydrate queue:",t)}).finally(()=>{this.requestPersist(),this.queue.length>0&&this.scheduleFlush(0)})}send(t){if(this.config.apiKey)if(this.isDisabled()){if(this.config.debug){const t=this.disabledUntil===1/0?"permanently":`for ${Math.max(0,this.disabledUntil-Date.now())}ms`;console.warn(`[CatDoes Watch] Transport disabled (${this.disabledReason}), dropping event ${t}`)}}else this.queue.push(t),this.trimQueue(),this.requestPersist(),this.config.debug&&console.debug("[CatDoes Watch] Event queued:",t.message),this.queue.length>=this.config.maxBufferSize?this.flush():this.scheduleFlush()}scheduleFlush(t=this.config.flushInterval){this.flushTimer||(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.flush()},Math.max(0,t)))}async flush(t){return this.flushPromise?(await this.flushPromise,this.queue.length>0&&!this.isDisabled()&&Date.now()>=this.backoffUntil?this.flush(t):void 0):(this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null),0!==this.queue.length?(this.flushPromise=this.doFlushWithLock(t).finally(()=>{this.flushPromise=null}),this.flushPromise):void 0)}async doFlushWithLock(t){if(0===this.queue.length)return;if(this.isDisabled()){if(this.config.debug){const t=this.disabledUntil===1/0?"permanently":`for ${Math.max(0,this.disabledUntil-Date.now())}ms`;console.warn(`[CatDoes Watch] Transport disabled (${this.disabledReason}), skipping flush ${t}`)}if(this.disabledUntil!==1/0){const t=Math.max(0,this.disabledUntil-Date.now());this.scheduleFlush(t)}return}if(Date.now()<this.backoffUntil){if(this.config.debug){const t=this.backoffUntil-Date.now();console.debug(`[CatDoes Watch] In backoff, waiting ${t}ms`)}return void this.scheduleFlush(this.backoffUntil-Date.now())}const e=Math.max(1,this.config.maxBufferSize),s=t?.keepalive?Math.min(e,10):e,i=this.queue.slice(0,s);try{await this.doFlush(i,t),this.backoffMs=0,this.consecutiveFailures=0,this.queue=this.queue.slice(i.length),this.requestPersist(),this.queue.length>0&&this.scheduleFlush(0)}catch(t){this.config.debug&&console.debug("[CatDoes Watch] Flush failed, events retained for retry:",t),this.isDisabled()||this.scheduleFlush()}}async doFlush(t,e){if(0===t.length)return;this.config.debug&&console.debug(`[CatDoes Watch] Sending ${t.length} event(s)`);const s=JSON.stringify({events:t});try{const t={method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:s};e?.keepalive&&(t.keepalive=!0);const i=await fetch(this.config.endpoint,t);if(401===i.status||403===i.status)throw this.config.debug&&console.warn("[CatDoes Watch] Invalid API key. Clearing queue and disabling."),this.queue=[],this.requestPersist(),this.disableOnAuthError&&this.disable("auth",1/0),this.config.onTransportError?.("auth",{status:i.status}),new Error("Invalid API key");if(429===i.status){const t=i.headers.get("Retry-After"),e=t?1e3*parseInt(t,10):this.calculateBackoff();throw this.backoffMs=e,this.backoffUntil=Date.now()+e,this.config.debug&&console.debug(`[CatDoes Watch] Rate limited, backing off ${e}ms`),new Error("Rate limited")}if(!i.ok)throw this.applyBackoff(),new Error(`HTTP ${i.status}`);const r=await i.json();this.config.debug&&console.debug(`[CatDoes Watch] Sent successfully, accepted: ${r.accepted}`)}catch(t){throw t instanceof Error&&"Rate limited"===t.message||this.applyBackoff(),t}}applyBackoff(){this.consecutiveFailures++,this.backoffMs=this.calculateBackoff(),this.backoffUntil=Date.now()+this.backoffMs,this.config.debug&&console.debug(`[CatDoes Watch] Failure #${this.consecutiveFailures}, backing off ${this.backoffMs}ms`),this.consecutiveFailures>=this.maxConsecutiveFailures&&(this.config.debug&&console.warn("[CatDoes Watch] Max consecutive failures reached. Clearing queue."),this.queue=[],this.requestPersist(),this.disable("failures",this.failuresCooldownMs),this.config.onTransportError?.("failures"))}calculateBackoff(){const t=1e3*Math.pow(2,this.consecutiveFailures);return Math.min(t,6e4)}isDisabled(){return null!==this.disabledReason&&Date.now()<this.disabledUntil}disable(t,e){this.disabledReason=t,this.disabledUntil=e===1/0?1/0:Date.now()+Math.max(0,e)}get queueSize(){return this.queue.length}clear(){this.queue=[],this.requestPersist(),this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null)}shutdown(){this.clear()}trimQueue(){this.queue.length<=200||(this.queue=this.queue.slice(-200))}requestPersist(){o&&(this.persistRequestedVersion++,this.persistInFlight||(this.persistInFlight=this.persistUntilStable().catch(t=>{this.config.debug&&console.debug("[CatDoes Watch] Failed to persist queue:",t)}).finally(()=>{this.persistInFlight=null})))}async persistUntilStable(){if(o)for(await this.hydratePromise.catch(()=>{});this.persistCompletedVersion<this.persistRequestedVersion;){const t=this.persistRequestedVersion;await this.persistSnapshot(),this.persistCompletedVersion=t}}async persistSnapshot(){if(o)try{if(0===this.queue.length)return void await o.removeItem(this.storageKey);const t={v:1,savedAt:(new Date).toISOString(),apiKeyHash:this.apiKeyHash,events:this.queue};await o.setItem(this.storageKey,JSON.stringify(t))}catch(t){this.config.debug&&console.debug("[CatDoes Watch] Queue persistence failed:",t)}}async hydrateQueue(){if(!o)return;let t=null;try{t=await o.getItem(this.storageKey)}catch{return}if(t)try{const e=JSON.parse(t);let s=[];if(Array.isArray(e))s=e.filter(a);else if(h(e)&&1===e.v&&Array.isArray(e.events)){const t=e.apiKeyHash;if(t&&t!==this.apiKeyHash){try{await o.removeItem(this.storageKey)}catch{}return}s=e.events.filter(a)}if(0===s.length)return void await o.removeItem(this.storageKey);this.queue=[...s,...this.queue],this.trimQueue(),this.config.debug&&console.debug(`[CatDoes Watch] Hydrated ${s.length} queued event(s)`)}catch{try{await o.removeItem(this.storageKey)}catch{}}}},u=null;function l(){return u||(u=`sess_${Date.now()}_${function(){const t="abcdefghijklmnopqrstuvwxyz0123456789";try{const e=n("expo-crypto");if(e?.getRandomValues){const s=new Uint8Array(8);e.getRandomValues(s);let i="";for(let e=0;e<8;e++)i+=t.charAt(s[e]%36);return i}}catch{}if("undefined"!=typeof crypto&&crypto.getRandomValues){const e=new Uint8Array(8);crypto.getRandomValues(e);let s="";for(let i=0;i<8;i++)s+=t.charAt(e[i]%36);return s}let e="";for(let s=0;s<8;s++)e+=t.charAt(Math.floor(36*Math.random()));return e}()}`),u}function d(){const e=t.Platform.OS;return"ios"===e?"ios":"android"===e?"android":"web"}function f(){return"undefined"!=typeof __DEV__&&__DEV__?"development":"production"}function p(){const e=function(){try{return n("expo-device")}catch{return null}}(),s=function(){try{return n("expo-constants").default}catch{return null}}(),i=function(){try{return n("expo-localization")}catch{return null}}(),r=d(),o={};try{const{width:e,height:s,scale:i}=t.Dimensions.get("window");o.screenWidth=Math.round(e),o.screenHeight=Math.round(s),o.screenScale=i}catch{}if(o.osName=t.Platform.OS,t.Platform.Version&&(o.osVersion=String(t.Platform.Version)),e&&"web"!==r)try{e.brand&&(o.brand=e.brand),e.manufacturer&&(o.manufacturer=e.manufacturer),e.modelName&&(o.modelName=e.modelName),e.deviceName&&(o.deviceName=e.deviceName),e.osName&&(o.osName=e.osName),e.osVersion&&(o.osVersion=e.osVersion),e.osBuildId&&(o.osBuildId=e.osBuildId),e.platformApiLevel&&(o.platformApiLevel=e.platformApiLevel),"boolean"==typeof e.isDevice&&(o.isDevice=e.isDevice)}catch{}if(s)try{const t=s.expoConfig||s.manifest;t?.version&&(o.appVersion=t.version),s.expoVersion&&(o.expoVersion=s.expoVersion),t?.name&&(o.appName=t.name),"ios"===r&&t?.ios?.bundleIdentifier?o.bundleId=t.ios.bundleIdentifier:"android"===r&&t?.android?.package&&(o.bundleId=t.android.package)}catch{}if(i)try{i.locale&&(o.locale=i.locale),i.timezone&&(o.timezone=i.timezone)}catch{}if("web"===r&&"undefined"!=typeof window)try{const t=window.navigator?.userAgent;if(t){o.userAgent=t.slice(0,500);const e=t.match(/(Chrome|Firefox|Safari|Edge|Opera)\/(\d+(\.\d+)?)/);e&&(o.browserName=e[1],o.browserVersion=e[2])}}catch{}return o}var y=null;function w(){return y||(y=p()),y}var m=class{constructor(t=20){this.breadcrumbs=[],this.maxBreadcrumbs=t}add(t){const e={...t,timestamp:(new Date).toISOString()};this.breadcrumbs.push(e),this.breadcrumbs.length>this.maxBreadcrumbs&&(this.breadcrumbs=this.breadcrumbs.slice(-this.maxBreadcrumbs))}getAll(){return[...this.breadcrumbs]}clear(){this.breadcrumbs=[]}get count(){return this.breadcrumbs.length}setMaxBreadcrumbs(t){this.maxBreadcrumbs=t,this.breadcrumbs.length>t&&(this.breadcrumbs=this.breadcrumbs.slice(-t))}},g="1.0.0";function v(t){const e=function(t){if(!t)return t;let e=t;const s=e.indexOf("?");s>=0&&(e=e.slice(0,s));const i=e.indexOf("&");for(i>=0&&(e=e.slice(0,i));e.endsWith("/")&&e.length>1;)e=e.slice(0,-1);return e}(t),s=["/app/","/src/","/components/","/screens/"];for(const t of s){const s=e.indexOf(t);if(s>=0)return D(e.slice(s+1))}try{const t=new URL(e).pathname.replace(/^\//,"");if(t)return t}catch{}return D(e)}function C(t){return!(!t||t.includes("node_modules")||/\.bundle(\/|$|:)/.test(t)||/bundle(\.js|\.map)$/.test(t)||t.startsWith("[native code]")||t.startsWith("native "))}function D(t){if(!t)return t;if(/^[a-z]+:\/\//i.test(t))return t;let e=t.replace(/\/{2,}/g,"/");return e.startsWith("./")&&(e=e.slice(2)),e}var b=class _WatchClient{constructor(t){this.context={},this.user=null,this.isInitialized=!1,this.recentErrors=new Map,this.recentErrorsCleanupTimer=null,this.config={apiKey:t.apiKey,endpoint:t.endpoint||"https://app.catdoes.com/api/watch/ingest",environment:t.environment||f(),captureConsoleErrors:t.captureConsoleErrors||!1,maxBreadcrumbs:t.maxBreadcrumbs||20,maxBufferSize:t.maxBufferSize||10,flushInterval:t.flushInterval||5e3,beforeSend:t.beforeSend||(t=>t),debug:t.debug??!1,dedupWindowMs:t.dedupWindowMs??5e3,dedupMaxEntries:t.dedupMaxEntries??500},this.transport=new c({endpoint:this.config.endpoint,apiKey:this.config.apiKey,maxBufferSize:this.config.maxBufferSize,flushInterval:this.config.flushInterval,debug:this.config.debug}),this.breadcrumbs=new m(this.config.maxBreadcrumbs),t.initialContext&&(this.context={...t.initialContext}),this.isInitialized=!0,this.config.debug&&console.debug("[CatDoes Watch] Initialized with config:",{endpoint:this.config.endpoint,environment:this.config.environment,apiKey:this.config.apiKey.slice(0,12)+"..."})}static init(t){return t.apiKey||"undefined"!=typeof __DEV__&&__DEV__&&console.warn("[CatDoes Watch] No API key provided. Errors will not be reported."),_WatchClient.instance?(t.debug||_WatchClient.instance.config.debug)&&console.debug("[CatDoes Watch] Already initialized. Returning existing instance."):_WatchClient.instance=new _WatchClient(t),_WatchClient.instance}static getInstance(){return _WatchClient.instance}captureError(t,e){if(!this.shouldCapture())return;let s;try{throw new Error("__CALL_SITE_CAPTURE__")}catch(t){s=t.stack}const i=this.ensureStack(t,s),r=this.getErrorKey(i);if(this.hasSeenErrorRecently(r))return void(this.config.debug&&console.debug("[CatDoes Watch] Skipping duplicate error:",i.message));this.markErrorAsSeen(r);const n=this.buildEvent(i,e),o=this.config.beforeSend(n);o?this.transport.send(o):this.config.debug&&console.debug("[CatDoes Watch] Event dropped by beforeSend hook")}captureMessage(t,e="error"){if(!this.shouldCapture())return;const s=new Error(t);if(s.stack){const t=s.stack.split("\n");s.stack=[t[0],...t.slice(2)].join("\n")}this.captureError(s,{level:e,synthetic:!0})}addBreadcrumb(t){this.breadcrumbs.add(t)}setContext(t,e){this.context[t]=e}clearContext(t){delete this.context[t]}setUser(t){this.user=t}async flush(t){await this.transport.flush(t)}getConfig(){return this.config}get initialized(){return this.isInitialized}buildEvent(t,e){const s=w(),i={message:t.message||"Unknown error",stack:t.stack,timestamp:(new Date).toISOString(),environment:this.config.environment,platform:d(),sessionId:l(),deviceInfo:s,sdkVersion:g,extra:{...this.context,...e,...this.user?{user:this.user}:{}},breadcrumbs:this.breadcrumbs.getAll()};if("development"!==this.config.environment&&t.stack){let s=t.stack.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?/);if(s||(s=t.stack.match(/^[^@]+@(.+?):(\d+):(\d+)$/m)),s){const t=v(s[1]);t&&C(t)&&(i.filename=t),i.lineno=parseInt(s[2],10),i.colno=parseInt(s[3],10)}void 0===i.lineno&&"number"==typeof e?.lineno&&(i.lineno=e.lineno),void 0===i.colno&&"number"==typeof e?.colno&&(i.colno=e.colno)}return i}shouldCapture(){return!!this.isInitialized&&!!this.config.apiKey}ensureStack(t,e){if(t.stack&&t.stack.length>100&&/:\d+:\d+/.test(t.stack))return t;if(e&&e.length>100&&/:\d+:\d+/.test(e)){const s=e.split("\n"),i=[];i.push(`${t.name}: ${t.message}`);let r=!1;for(const t of s){if(t.includes("__CALL_SITE_CAPTURE__")||t.includes("ensureStack")||t.includes("captureError")||t.includes("WatchClient"))continue;const e=t.trim();e&&(e.includes("@")||e.startsWith("at "))&&(i.push(e),r=!0)}if(r&&i.length>1){const e=new Error(t.message);return e.name=t.name,e.stack=i.join("\n"),Object.assign(e,t),e}}try{throw t}catch(t){const e=t;if(e.stack&&e.stack.length>100&&/:\d+:\d+/.test(e.stack))return e}return t}getErrorKey(t){const e=t.stack?.split("\n")[1]?.trim()||"";return`${t.message}::${e}`}markErrorAsSeen(t){const e=Date.now();if(this.recentErrors.has(t)&&this.recentErrors.delete(t),this.recentErrors.set(t,e),this.recentErrors.size>this.config.dedupMaxEntries){const t=this.recentErrors.keys().next().value;t&&this.recentErrors.delete(t)}this.scheduleRecentErrorsCleanup()}hasSeenErrorRecently(t){const e=this.recentErrors.get(t);if(null==e)return!1;const s=Date.now()-e<=this.config.dedupWindowMs;return s||this.recentErrors.delete(t),s}scheduleRecentErrorsCleanup(){this.recentErrorsCleanupTimer||(this.recentErrorsCleanupTimer=setTimeout(()=>{this.pruneRecentErrors(),this.recentErrorsCleanupTimer=null,this.recentErrors.size>0&&this.scheduleRecentErrorsCleanup()},this.config.dedupWindowMs))}pruneRecentErrors(){const t=Date.now(),e=this.config.dedupWindowMs;for(const[s,i]of this.recentErrors.entries())t-i>e&&this.recentErrors.delete(s)}};b.instance=null;var E=b,x={init:t=>E.init(t),getInstance:()=>E.getInstance(),captureError(t,e){E.getInstance()?.captureError(t,e)},captureMessage(t,e="error"){E.getInstance()?.captureMessage(t,e)},addBreadcrumb(t){E.getInstance()?.addBreadcrumb(t)},setContext(t,e){E.getInstance()?.setContext(t,e)},setUser(t){E.getInstance()?.setUser(t)},flush:t=>E.getInstance()?.flush(t)||Promise.resolve()},_=!1,W=null,$=null,S=null,k=null,I=null,q=null,M=class extends r.default.Component{constructor(t){super(t),this.resetError=()=>{this.setState({hasError:!1,error:null,errorInfo:null})},this.state={hasError:!1,error:null,errorInfo:null}}static getDerivedStateFromError(t){return{hasError:!0,error:t}}componentDidCatch(t,e){this.setState({errorInfo:e});const{captureErrors:s=!0,onError:i}=this.props;if(s){const s=E.getInstance();s&&s.captureError(t,{componentStack:e.componentStack,source:"WatchErrorBoundary"})}i&&i(t,e)}render(){const{hasError:t,error:e,errorInfo:i}=this.state,{children:r,fallback:n}=this.props;return t&&e?n?s.jsx(n,{error:e,errorInfo:i,resetError:this.resetError}):null:r}};exports.BreadcrumbManager=m,exports.SDK_VERSION=g,exports.Watch=x,exports.WatchClient=E,exports.WatchErrorBoundary=M,exports.clearDeviceInfoCache=function(){y=null},exports.collectDeviceInfo=p,exports.createConsoleBreadcrumb=function(t,e){return{type:"console",message:`[${t}] ${e}`.slice(0,200),data:{level:t}}},exports.createCustomBreadcrumb=function(t,e){return{type:"custom",message:t,data:e}},exports.createHttpBreadcrumb=function(t,e,s){return{type:"http",message:`${t} ${e}${s?` [${s}]`:""}`,data:{method:t,url:e,statusCode:s}}},exports.createNavigationBreadcrumb=function(t,e){return{type:"navigation",message:`Navigated from ${t} to ${e}`,data:{from:t,to:e}}},exports.createUIBreadcrumb=function(t,e){return{type:"ui",message:e?`${t} on ${e}`:t,data:{action:t,target:e}}},exports.deriveReadableFile=v,exports.getCachedDeviceInfo=w,exports.getEnvironment=f,exports.getPlatform=d,exports.getSessionId=l,exports.isUsableFilename=C,exports.removeGlobalHandlers=function(){if(_){if("web"===t.Platform.OS&&"undefined"!=typeof window)window.onerror=W,W=null,S&&(window.removeEventListener("beforeunload",S),S=null),k&&(window.removeEventListener("pagehide",k),k=null),I&&(window.removeEventListener("unhandledrejection",I),I=null);else{const t=global.ErrorUtils;t&&$&&(t.setGlobalHandler($),$=null),q&&(q.remove(),q=null)}_=!1}},exports.resetSession=function(){u=null},exports.setSessionId=function(t){u=t},exports.setupConsoleErrorCapture=function(t){const e=console.error;console.error=(...s)=>{let i;e.apply(console,s);const r=s.find(t=>t instanceof Error);if(r)i=r;else{const t=s.map(t=>{if("string"==typeof t)return t;try{return JSON.stringify(t)}catch{return String(t)}}).join(" ");if(i=new Error(t),i.stack){const t=i.stack.split("\n");i.stack=[t[0],...t.slice(3)].join("\n")}}t.captureError(i,{source:"console.error",synthetic:!r})}},exports.setupGlobalHandlers=function(e){_?e.getConfig().debug&&console.debug("[CatDoes Watch] Global handlers already installed"):("web"===t.Platform.OS?function(t){"undefined"!=typeof window&&(W=window.onerror,window.onerror=(e,s,i,r,n)=>{W&&W(e,s,i,r,n);const o=n||new Error("string"==typeof e?e:"Unknown error");return!n&&s&&(o.filename=s,o.lineno=i,o.colno=r),t.captureError(o,{source:"global.onerror",filename:s,lineno:i,colno:r}),!1},I=e=>{if(e.defaultPrevented)return;const s=e.reason instanceof Error?e.reason:new Error(String(e.reason??"Unhandled Promise rejection"));t.captureError(s,{source:"global.unhandledrejection",isPromiseRejection:!0})},window.addEventListener("unhandledrejection",I),S=()=>{t.flush({keepalive:!0}).catch(()=>{})},k=()=>{t.flush({keepalive:!0}).catch(()=>{})},window.addEventListener("beforeunload",S),window.addEventListener("pagehide",k))}(e):function(e){try{q=t.AppState.addEventListener("change",t=>{"background"!==t&&"inactive"!==t||e.flush().catch(()=>{})})}catch{}const s=global.ErrorUtils;s?($=s.getGlobalHandler(),s.setGlobalHandler((t,s)=>{e.captureError(t,{source:"ErrorUtils.globalHandler",isFatal:s}),s&&e.flush().catch(()=>{}),$&&$(t,s)})):e.getConfig().debug&&console.debug("[CatDoes Watch] ErrorUtils not available")}(e),_=!0,e.getConfig().debug&&console.debug("[CatDoes Watch] Global handlers installed"))},exports.withWatchErrorBoundary=function(t,e){const i=t.displayName||t.name||"Component",r=i=>s.jsx(M,{...e,children:s.jsx(t,{...i})});return r.displayName=`withWatchErrorBoundary(${i})`,r};
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{Platform as t,Dimensions as e,AppState as i}from"react-native";import s from"react";import{jsx as n}from"react/jsx-runtime";var r=(t=>"undefined"!=typeof require?require:"undefined"!=typeof Proxy?new Proxy(t,{get:(t,e)=>("undefined"!=typeof require?require:t)[e]}):t)(function(t){if("undefined"!=typeof require)return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')}),o=null;try{o=r("@react-native-async-storage/async-storage").default}catch{}function h(t){return"object"==typeof t&&null!==t}function a(t){return!!h(t)&&"string"==typeof t.message&&"string"==typeof t.timestamp}var c=class{constructor(t){if(this.queue=[],this.flushTimer=null,this.backoffMs=0,this.backoffUntil=0,this.flushPromise=null,this.consecutiveFailures=0,this.disabledUntil=0,this.disabledReason=null,this.persistRequestedVersion=0,this.persistCompletedVersion=0,this.persistInFlight=null,this.config=t,this.maxConsecutiveFailures=t.maxConsecutiveFailures??50,this.failuresCooldownMs=t.failuresCooldownMs??9e5,this.disableOnAuthError=t.disableOnAuthError??!0,!t.apiKey)return this.config.debug&&console.warn("[CatDoes Watch] No API key; transport disabled."),this.disabledReason="auth",this.disabledUntil=1/0,this.apiKeyHash="",this.storageKey="",void(this.hydratePromise=Promise.resolve());this.apiKeyHash=function(t){let e=5381;for(let i=0;i<t.length;i++)e=(e<<5)+e^t.charCodeAt(i);return(e>>>0).toString(36)}(t.apiKey),this.storageKey=`@catdoes_watch_queue_v1_${this.apiKeyHash}`,this.hydratePromise=this.hydrateQueue().catch(t=>{this.config.debug&&console.debug("[CatDoes Watch] Failed to hydrate queue:",t)}).finally(()=>{this.requestPersist(),this.queue.length>0&&this.scheduleFlush(0)})}send(t){if(this.config.apiKey)if(this.isDisabled()){if(this.config.debug){const t=this.disabledUntil===1/0?"permanently":`for ${Math.max(0,this.disabledUntil-Date.now())}ms`;console.warn(`[CatDoes Watch] Transport disabled (${this.disabledReason}), dropping event ${t}`)}}else this.queue.push(t),this.trimQueue(),this.requestPersist(),this.config.debug&&console.debug("[CatDoes Watch] Event queued:",t.message),this.queue.length>=this.config.maxBufferSize?this.flush():this.scheduleFlush()}scheduleFlush(t=this.config.flushInterval){this.flushTimer||(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.flush()},Math.max(0,t)))}async flush(t){return this.flushPromise?(await this.flushPromise,this.queue.length>0&&!this.isDisabled()&&Date.now()>=this.backoffUntil?this.flush(t):void 0):(this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null),0!==this.queue.length?(this.flushPromise=this.doFlushWithLock(t).finally(()=>{this.flushPromise=null}),this.flushPromise):void 0)}async doFlushWithLock(t){if(0===this.queue.length)return;if(this.isDisabled()){if(this.config.debug){const t=this.disabledUntil===1/0?"permanently":`for ${Math.max(0,this.disabledUntil-Date.now())}ms`;console.warn(`[CatDoes Watch] Transport disabled (${this.disabledReason}), skipping flush ${t}`)}if(this.disabledUntil!==1/0){const t=Math.max(0,this.disabledUntil-Date.now());this.scheduleFlush(t)}return}if(Date.now()<this.backoffUntil){if(this.config.debug){const t=this.backoffUntil-Date.now();console.debug(`[CatDoes Watch] In backoff, waiting ${t}ms`)}return void this.scheduleFlush(this.backoffUntil-Date.now())}const e=Math.max(1,this.config.maxBufferSize),i=t?.keepalive?Math.min(e,10):e,s=this.queue.slice(0,i);try{await this.doFlush(s,t),this.backoffMs=0,this.consecutiveFailures=0,this.queue=this.queue.slice(s.length),this.requestPersist(),this.queue.length>0&&this.scheduleFlush(0)}catch(t){this.config.debug&&console.debug("[CatDoes Watch] Flush failed, events retained for retry:",t),this.isDisabled()||this.scheduleFlush()}}async doFlush(t,e){if(0===t.length)return;this.config.debug&&console.debug(`[CatDoes Watch] Sending ${t.length} event(s)`);const i=JSON.stringify({events:t});try{const t={method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:i};e?.keepalive&&(t.keepalive=!0);const s=await fetch(this.config.endpoint,t);if(401===s.status||403===s.status)throw this.config.debug&&console.warn("[CatDoes Watch] Invalid API key. Clearing queue and disabling."),this.queue=[],this.requestPersist(),this.disableOnAuthError&&this.disable("auth",1/0),this.config.onTransportError?.("auth",{status:s.status}),new Error("Invalid API key");if(429===s.status){const t=s.headers.get("Retry-After"),e=t?1e3*parseInt(t,10):this.calculateBackoff();throw this.backoffMs=e,this.backoffUntil=Date.now()+e,this.config.debug&&console.debug(`[CatDoes Watch] Rate limited, backing off ${e}ms`),new Error("Rate limited")}if(!s.ok)throw this.applyBackoff(),new Error(`HTTP ${s.status}`);const n=await s.json();this.config.debug&&console.debug(`[CatDoes Watch] Sent successfully, accepted: ${n.accepted}`)}catch(t){throw t instanceof Error&&"Rate limited"===t.message||this.applyBackoff(),t}}applyBackoff(){this.consecutiveFailures++,this.backoffMs=this.calculateBackoff(),this.backoffUntil=Date.now()+this.backoffMs,this.config.debug&&console.debug(`[CatDoes Watch] Failure #${this.consecutiveFailures}, backing off ${this.backoffMs}ms`),this.consecutiveFailures>=this.maxConsecutiveFailures&&(this.config.debug&&console.warn("[CatDoes Watch] Max consecutive failures reached. Clearing queue."),this.queue=[],this.requestPersist(),this.disable("failures",this.failuresCooldownMs),this.config.onTransportError?.("failures"))}calculateBackoff(){const t=1e3*Math.pow(2,this.consecutiveFailures);return Math.min(t,6e4)}isDisabled(){return null!==this.disabledReason&&Date.now()<this.disabledUntil}disable(t,e){this.disabledReason=t,this.disabledUntil=e===1/0?1/0:Date.now()+Math.max(0,e)}get queueSize(){return this.queue.length}clear(){this.queue=[],this.requestPersist(),this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null)}shutdown(){this.clear()}trimQueue(){this.queue.length<=200||(this.queue=this.queue.slice(-200))}requestPersist(){o&&(this.persistRequestedVersion++,this.persistInFlight||(this.persistInFlight=this.persistUntilStable().catch(t=>{this.config.debug&&console.debug("[CatDoes Watch] Failed to persist queue:",t)}).finally(()=>{this.persistInFlight=null})))}async persistUntilStable(){if(o)for(await this.hydratePromise.catch(()=>{});this.persistCompletedVersion<this.persistRequestedVersion;){const t=this.persistRequestedVersion;await this.persistSnapshot(),this.persistCompletedVersion=t}}async persistSnapshot(){if(o)try{if(0===this.queue.length)return void await o.removeItem(this.storageKey);const t={v:1,savedAt:(new Date).toISOString(),apiKeyHash:this.apiKeyHash,events:this.queue};await o.setItem(this.storageKey,JSON.stringify(t))}catch(t){this.config.debug&&console.debug("[CatDoes Watch] Queue persistence failed:",t)}}async hydrateQueue(){if(!o)return;let t=null;try{t=await o.getItem(this.storageKey)}catch{return}if(t)try{const e=JSON.parse(t);let i=[];if(Array.isArray(e))i=e.filter(a);else if(h(e)&&1===e.v&&Array.isArray(e.events)){const t=e.apiKeyHash;if(t&&t!==this.apiKeyHash){try{await o.removeItem(this.storageKey)}catch{}return}i=e.events.filter(a)}if(0===i.length)return void await o.removeItem(this.storageKey);this.queue=[...i,...this.queue],this.trimQueue(),this.config.debug&&console.debug(`[CatDoes Watch] Hydrated ${i.length} queued event(s)`)}catch{try{await o.removeItem(this.storageKey)}catch{}}}},u=null;function l(){return u||(u=`sess_${Date.now()}_${function(){const t="abcdefghijklmnopqrstuvwxyz0123456789";try{const e=r("expo-crypto");if(e?.getRandomValues){const i=new Uint8Array(8);e.getRandomValues(i);let s="";for(let e=0;e<8;e++)s+=t.charAt(i[e]%36);return s}}catch{}if("undefined"!=typeof crypto&&crypto.getRandomValues){const e=new Uint8Array(8);crypto.getRandomValues(e);let i="";for(let s=0;s<8;s++)i+=t.charAt(e[s]%36);return i}let e="";for(let i=0;i<8;i++)e+=t.charAt(Math.floor(36*Math.random()));return e}()}`),u}function d(){u=null}function f(t){u=t}function p(){const e=t.OS;return"ios"===e?"ios":"android"===e?"android":"web"}function y(){return"undefined"!=typeof __DEV__&&__DEV__?"development":"production"}function w(){const i=function(){try{return r("expo-device")}catch{return null}}(),s=function(){try{return r("expo-constants").default}catch{return null}}(),n=function(){try{return r("expo-localization")}catch{return null}}(),o=p(),h={};try{const{width:t,height:i,scale:s}=e.get("window");h.screenWidth=Math.round(t),h.screenHeight=Math.round(i),h.screenScale=s}catch{}if(h.osName=t.OS,t.Version&&(h.osVersion=String(t.Version)),i&&"web"!==o)try{i.brand&&(h.brand=i.brand),i.manufacturer&&(h.manufacturer=i.manufacturer),i.modelName&&(h.modelName=i.modelName),i.deviceName&&(h.deviceName=i.deviceName),i.osName&&(h.osName=i.osName),i.osVersion&&(h.osVersion=i.osVersion),i.osBuildId&&(h.osBuildId=i.osBuildId),i.platformApiLevel&&(h.platformApiLevel=i.platformApiLevel),"boolean"==typeof i.isDevice&&(h.isDevice=i.isDevice)}catch{}if(s)try{const t=s.expoConfig||s.manifest;t?.version&&(h.appVersion=t.version),s.expoVersion&&(h.expoVersion=s.expoVersion),t?.name&&(h.appName=t.name),"ios"===o&&t?.ios?.bundleIdentifier?h.bundleId=t.ios.bundleIdentifier:"android"===o&&t?.android?.package&&(h.bundleId=t.android.package)}catch{}if(n)try{n.locale&&(h.locale=n.locale),n.timezone&&(h.timezone=n.timezone)}catch{}if("web"===o&&"undefined"!=typeof window)try{const t=window.navigator?.userAgent;if(t){h.userAgent=t.slice(0,500);const e=t.match(/(Chrome|Firefox|Safari|Edge|Opera)\/(\d+(\.\d+)?)/);e&&(h.browserName=e[1],h.browserVersion=e[2])}}catch{}return h}var m=null;function g(){return m||(m=w()),m}function v(){m=null}var C=class{constructor(t=20){this.breadcrumbs=[],this.maxBreadcrumbs=t}add(t){const e={...t,timestamp:(new Date).toISOString()};this.breadcrumbs.push(e),this.breadcrumbs.length>this.maxBreadcrumbs&&(this.breadcrumbs=this.breadcrumbs.slice(-this.maxBreadcrumbs))}getAll(){return[...this.breadcrumbs]}clear(){this.breadcrumbs=[]}get count(){return this.breadcrumbs.length}setMaxBreadcrumbs(t){this.maxBreadcrumbs=t,this.breadcrumbs.length>t&&(this.breadcrumbs=this.breadcrumbs.slice(-t))}};function D(t,e){return{type:"navigation",message:`Navigated from ${t} to ${e}`,data:{from:t,to:e}}}function b(t,e){return{type:"ui",message:e?`${t} on ${e}`:t,data:{action:t,target:e}}}function E(t,e,i){return{type:"http",message:`${t} ${e}${i?` [${i}]`:""}`,data:{method:t,url:e,statusCode:i}}}function _(t,e){return{type:"console",message:`[${t}] ${e}`.slice(0,200),data:{level:t}}}function W(t,e){return{type:"custom",message:t,data:e}}var $="1.0.0";function S(t){const e=function(t){if(!t)return t;let e=t;const i=e.indexOf("?");i>=0&&(e=e.slice(0,i));const s=e.indexOf("&");for(s>=0&&(e=e.slice(0,s));e.endsWith("/")&&e.length>1;)e=e.slice(0,-1);return e}(t),i=["/app/","/src/","/components/","/screens/"];for(const t of i){const i=e.indexOf(t);if(i>=0)return I(e.slice(i+1))}try{const t=new URL(e).pathname.replace(/^\//,"");if(t)return t}catch{}return I(e)}function k(t){return!(!t||t.includes("node_modules")||/\.bundle(\/|$|:)/.test(t)||/bundle(\.js|\.map)$/.test(t)||t.startsWith("[native code]")||t.startsWith("native "))}function I(t){if(!t)return t;if(/^[a-z]+:\/\//i.test(t))return t;let e=t.replace(/\/{2,}/g,"/");return e.startsWith("./")&&(e=e.slice(2)),e}var x=class _WatchClient{constructor(t){this.context={},this.user=null,this.isInitialized=!1,this.recentErrors=new Map,this.recentErrorsCleanupTimer=null,this.config={apiKey:t.apiKey,endpoint:t.endpoint||"https://app.catdoes.com/api/watch/ingest",environment:t.environment||y(),captureConsoleErrors:t.captureConsoleErrors||!1,maxBreadcrumbs:t.maxBreadcrumbs||20,maxBufferSize:t.maxBufferSize||10,flushInterval:t.flushInterval||5e3,beforeSend:t.beforeSend||(t=>t),debug:t.debug??!1,dedupWindowMs:t.dedupWindowMs??5e3,dedupMaxEntries:t.dedupMaxEntries??500},this.transport=new c({endpoint:this.config.endpoint,apiKey:this.config.apiKey,maxBufferSize:this.config.maxBufferSize,flushInterval:this.config.flushInterval,debug:this.config.debug}),this.breadcrumbs=new C(this.config.maxBreadcrumbs),t.initialContext&&(this.context={...t.initialContext}),this.isInitialized=!0,this.config.debug&&console.debug("[CatDoes Watch] Initialized with config:",{endpoint:this.config.endpoint,environment:this.config.environment,apiKey:this.config.apiKey.slice(0,12)+"..."})}static init(t){return t.apiKey||"undefined"!=typeof __DEV__&&__DEV__&&console.warn("[CatDoes Watch] No API key provided. Errors will not be reported."),_WatchClient.instance?(t.debug||_WatchClient.instance.config.debug)&&console.debug("[CatDoes Watch] Already initialized. Returning existing instance."):_WatchClient.instance=new _WatchClient(t),_WatchClient.instance}static getInstance(){return _WatchClient.instance}captureError(t,e){if(!this.shouldCapture())return;let i;try{throw new Error("__CALL_SITE_CAPTURE__")}catch(t){i=t.stack}const s=this.ensureStack(t,i),n=this.getErrorKey(s);if(this.hasSeenErrorRecently(n))return void(this.config.debug&&console.debug("[CatDoes Watch] Skipping duplicate error:",s.message));this.markErrorAsSeen(n);const r=this.buildEvent(s,e),o=this.config.beforeSend(r);o?this.transport.send(o):this.config.debug&&console.debug("[CatDoes Watch] Event dropped by beforeSend hook")}captureMessage(t,e="error"){if(!this.shouldCapture())return;const i=new Error(t);if(i.stack){const t=i.stack.split("\n");i.stack=[t[0],...t.slice(2)].join("\n")}this.captureError(i,{level:e,synthetic:!0})}addBreadcrumb(t){this.breadcrumbs.add(t)}setContext(t,e){this.context[t]=e}clearContext(t){delete this.context[t]}setUser(t){this.user=t}async flush(t){await this.transport.flush(t)}getConfig(){return this.config}get initialized(){return this.isInitialized}buildEvent(t,e){const i=g(),s={message:t.message||"Unknown error",stack:t.stack,timestamp:(new Date).toISOString(),environment:this.config.environment,platform:p(),sessionId:l(),deviceInfo:i,sdkVersion:$,extra:{...this.context,...e,...this.user?{user:this.user}:{}},breadcrumbs:this.breadcrumbs.getAll()};if("development"!==this.config.environment&&t.stack){let i=t.stack.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?/);if(i||(i=t.stack.match(/^[^@]+@(.+?):(\d+):(\d+)$/m)),i){const t=S(i[1]);t&&k(t)&&(s.filename=t),s.lineno=parseInt(i[2],10),s.colno=parseInt(i[3],10)}void 0===s.lineno&&"number"==typeof e?.lineno&&(s.lineno=e.lineno),void 0===s.colno&&"number"==typeof e?.colno&&(s.colno=e.colno)}return s}shouldCapture(){return!!this.isInitialized&&!!this.config.apiKey}ensureStack(t,e){if(t.stack&&t.stack.length>100&&/:\d+:\d+/.test(t.stack))return t;if(e&&e.length>100&&/:\d+:\d+/.test(e)){const i=e.split("\n"),s=[];s.push(`${t.name}: ${t.message}`);let n=!1;for(const t of i){if(t.includes("__CALL_SITE_CAPTURE__")||t.includes("ensureStack")||t.includes("captureError")||t.includes("WatchClient"))continue;const e=t.trim();e&&(e.includes("@")||e.startsWith("at "))&&(s.push(e),n=!0)}if(n&&s.length>1){const e=new Error(t.message);return e.name=t.name,e.stack=s.join("\n"),Object.assign(e,t),e}}try{throw t}catch(t){const e=t;if(e.stack&&e.stack.length>100&&/:\d+:\d+/.test(e.stack))return e}return t}getErrorKey(t){const e=t.stack?.split("\n")[1]?.trim()||"";return`${t.message}::${e}`}markErrorAsSeen(t){const e=Date.now();if(this.recentErrors.has(t)&&this.recentErrors.delete(t),this.recentErrors.set(t,e),this.recentErrors.size>this.config.dedupMaxEntries){const t=this.recentErrors.keys().next().value;t&&this.recentErrors.delete(t)}this.scheduleRecentErrorsCleanup()}hasSeenErrorRecently(t){const e=this.recentErrors.get(t);if(null==e)return!1;const i=Date.now()-e<=this.config.dedupWindowMs;return i||this.recentErrors.delete(t),i}scheduleRecentErrorsCleanup(){this.recentErrorsCleanupTimer||(this.recentErrorsCleanupTimer=setTimeout(()=>{this.pruneRecentErrors(),this.recentErrorsCleanupTimer=null,this.recentErrors.size>0&&this.scheduleRecentErrorsCleanup()},this.config.dedupWindowMs))}pruneRecentErrors(){const t=Date.now(),e=this.config.dedupWindowMs;for(const[i,s]of this.recentErrors.entries())t-s>e&&this.recentErrors.delete(i)}};x.instance=null;var M=x,A={init:t=>M.init(t),getInstance:()=>M.getInstance(),captureError(t,e){M.getInstance()?.captureError(t,e)},captureMessage(t,e="error"){M.getInstance()?.captureMessage(t,e)},addBreadcrumb(t){M.getInstance()?.addBreadcrumb(t)},setContext(t,e){M.getInstance()?.setContext(t,e)},setUser(t){M.getInstance()?.setUser(t)},flush:t=>M.getInstance()?.flush(t)||Promise.resolve()},q=!1,P=null,T=null,U=null,R=null,j=null,B=null;function z(e){q?e.getConfig().debug&&console.debug("[CatDoes Watch] Global handlers already installed"):("web"===t.OS?function(t){"undefined"!=typeof window&&(P=window.onerror,window.onerror=(e,i,s,n,r)=>{P&&P(e,i,s,n,r);const o=r||new Error("string"==typeof e?e:"Unknown error");return!r&&i&&(o.filename=i,o.lineno=s,o.colno=n),t.captureError(o,{source:"global.onerror",filename:i,lineno:s,colno:n}),!1},j=e=>{if(e.defaultPrevented)return;const i=e.reason instanceof Error?e.reason:new Error(String(e.reason??"Unhandled Promise rejection"));t.captureError(i,{source:"global.unhandledrejection",isPromiseRejection:!0})},window.addEventListener("unhandledrejection",j),U=()=>{t.flush({keepalive:!0}).catch(()=>{})},R=()=>{t.flush({keepalive:!0}).catch(()=>{})},window.addEventListener("beforeunload",U),window.addEventListener("pagehide",R))}(e):function(t){try{B=i.addEventListener("change",e=>{"background"!==e&&"inactive"!==e||t.flush().catch(()=>{})})}catch{}const e=global.ErrorUtils;e?(T=e.getGlobalHandler(),e.setGlobalHandler((e,i)=>{t.captureError(e,{source:"ErrorUtils.globalHandler",isFatal:i}),i&&t.flush().catch(()=>{}),T&&T(e,i)})):t.getConfig().debug&&console.debug("[CatDoes Watch] ErrorUtils not available")}(e),q=!0,e.getConfig().debug&&console.debug("[CatDoes Watch] Global handlers installed"))}function F(t){const e=console.error;console.error=(...i)=>{let s;e.apply(console,i);const n=i.find(t=>t instanceof Error);if(n)s=n;else{const t=i.map(t=>{if("string"==typeof t)return t;try{return JSON.stringify(t)}catch{return String(t)}}).join(" ");if(s=new Error(t),s.stack){const t=s.stack.split("\n");s.stack=[t[0],...t.slice(3)].join("\n")}}t.captureError(s,{source:"console.error",synthetic:!n})}}function N(){if(q){if("web"===t.OS&&"undefined"!=typeof window)window.onerror=P,P=null,U&&(window.removeEventListener("beforeunload",U),U=null),R&&(window.removeEventListener("pagehide",R),R=null),j&&(window.removeEventListener("unhandledrejection",j),j=null);else{const t=global.ErrorUtils;t&&T&&(t.setGlobalHandler(T),T=null),B&&(B.remove(),B=null)}q=!1}}var O=class extends s.Component{constructor(t){super(t),this.resetError=()=>{this.setState({hasError:!1,error:null,errorInfo:null})},this.state={hasError:!1,error:null,errorInfo:null}}static getDerivedStateFromError(t){return{hasError:!0,error:t}}componentDidCatch(t,e){this.setState({errorInfo:e});const{captureErrors:i=!0,onError:s}=this.props;if(i){const i=M.getInstance();i&&i.captureError(t,{componentStack:e.componentStack,source:"WatchErrorBoundary"})}s&&s(t,e)}render(){const{hasError:t,error:e,errorInfo:i}=this.state,{children:s,fallback:r}=this.props;return t&&e?r?n(r,{error:e,errorInfo:i,resetError:this.resetError}):null:s}};function L(t,e){const i=t.displayName||t.name||"Component",s=i=>n(O,{...e,children:n(t,{...i})});return s.displayName=`withWatchErrorBoundary(${i})`,s}export{C as BreadcrumbManager,$ as SDK_VERSION,A as Watch,M as WatchClient,O as WatchErrorBoundary,v as clearDeviceInfoCache,w as collectDeviceInfo,_ as createConsoleBreadcrumb,W as createCustomBreadcrumb,E as createHttpBreadcrumb,D as createNavigationBreadcrumb,b as createUIBreadcrumb,S as deriveReadableFile,g as getCachedDeviceInfo,y as getEnvironment,p as getPlatform,l as getSessionId,k as isUsableFilename,N as removeGlobalHandlers,d as resetSession,f as setSessionId,F as setupConsoleErrorCapture,z as setupGlobalHandlers,L as withWatchErrorBoundary};
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * CatDoes Watch SDK - React Error Boundary
5
+ *
6
+ * A React Error Boundary component that automatically captures
7
+ * errors and reports them to CatDoes Watch.
8
+ */
9
+
10
+ interface WatchErrorBoundaryProps {
11
+ /**
12
+ * The children to render.
13
+ */
14
+ children: React.ReactNode;
15
+ /**
16
+ * A fallback component to render when an error occurs.
17
+ */
18
+ fallback?: React.ComponentType<{
19
+ error: Error;
20
+ errorInfo: React.ErrorInfo;
21
+ resetError: () => void;
22
+ }>;
23
+ /**
24
+ * A callback invoked when an error is caught.
25
+ */
26
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
27
+ /**
28
+ * Whether to capture errors to CatDoes Watch.
29
+ * @default true
30
+ */
31
+ captureErrors?: boolean;
32
+ }
33
+ interface WatchErrorBoundaryState {
34
+ hasError: boolean;
35
+ error: Error | null;
36
+ errorInfo: React.ErrorInfo | null;
37
+ }
38
+ /**
39
+ * React Error Boundary that integrates with CatDoes Watch.
40
+ */
41
+ declare class WatchErrorBoundary extends React.Component<WatchErrorBoundaryProps, WatchErrorBoundaryState> {
42
+ constructor(props: WatchErrorBoundaryProps);
43
+ static getDerivedStateFromError(error: Error): Partial<WatchErrorBoundaryState>;
44
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
45
+ /**
46
+ * Resets the error state, allowing the children to be re-rendered.
47
+ */
48
+ resetError: () => void;
49
+ render(): React.ReactNode;
50
+ }
51
+ /**
52
+ * Higher-order component that wraps a component with WatchErrorBoundary.
53
+ */
54
+ declare function withWatchErrorBoundary<P extends object>(Component: React.ComponentType<P>, errorBoundaryProps?: Omit<WatchErrorBoundaryProps, "children">): React.ComponentType<P>;
55
+
56
+ export { WatchErrorBoundary, type WatchErrorBoundaryProps, withWatchErrorBoundary };