@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.
- package/README.md +181 -0
- package/dist/index.d.mts +486 -0
- package/dist/index.d.ts +486 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/dist/react.d.mts +56 -0
- package/dist/react.d.ts +56 -0
- package/dist/react.js +1 -0
- package/dist/react.mjs +1 -0
- package/package.json +74 -0
package/dist/react.d.ts
ADDED
|
@@ -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 };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var t=require("react"),e=require("react-native"),s=require("react/jsx-runtime");function i(t){return t&&t.t?t:{default:t}}var r=i(t),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')}),h=null;try{h=n("@react-native-async-storage/async-storage").default}catch{}function o(t){return"object"==typeof t&&null!==t}function a(t){return!!o(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(){h&&(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(h)for(await this.hydratePromise.catch(()=>{});this.persistCompletedVersion<this.persistRequestedVersion;){const t=this.persistRequestedVersion;await this.persistSnapshot(),this.persistCompletedVersion=t}}async persistSnapshot(){if(h)try{if(0===this.queue.length)return void await h.removeItem(this.storageKey);const t={v:1,savedAt:(new Date).toISOString(),apiKeyHash:this.apiKeyHash,events:this.queue};await h.setItem(this.storageKey,JSON.stringify(t))}catch(t){this.config.debug&&console.debug("[CatDoes Watch] Queue persistence failed:",t)}}async hydrateQueue(){if(!h)return;let t=null;try{t=await h.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(o(e)&&1===e.v&&Array.isArray(e.events)){const t=e.apiKeyHash;if(t&&t!==this.apiKeyHash){try{await h.removeItem(this.storageKey)}catch{}return}s=e.events.filter(a)}if(0===s.length)return void await h.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 h.removeItem(this.storageKey)}catch{}}}},u=null;function l(){const t=e.Platform.OS;return"ios"===t?"ios":"android"===t?"android":"web"}var f=null,d=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 p(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 y=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||("undefined"!=typeof __DEV__&&__DEV__?"development":"production"),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 d(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),h=this.config.beforeSend(n);h?this.transport.send(h):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,s){const i=(f||(f=function(){const t=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=l(),h={};try{const{width:t,height:s,scale:i}=e.Dimensions.get("window");h.screenWidth=Math.round(t),h.screenHeight=Math.round(s),h.screenScale=i}catch{}if(h.osName=e.Platform.OS,e.Platform.Version&&(h.osVersion=String(e.Platform.Version)),t&&"web"!==r)try{t.brand&&(h.brand=t.brand),t.manufacturer&&(h.manufacturer=t.manufacturer),t.modelName&&(h.modelName=t.modelName),t.deviceName&&(h.deviceName=t.deviceName),t.osName&&(h.osName=t.osName),t.osVersion&&(h.osVersion=t.osVersion),t.osBuildId&&(h.osBuildId=t.osBuildId),t.platformApiLevel&&(h.platformApiLevel=t.platformApiLevel),"boolean"==typeof t.isDevice&&(h.isDevice=t.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"===r&&t?.ios?.bundleIdentifier?h.bundleId=t.ios.bundleIdentifier:"android"===r&&t?.android?.package&&(h.bundleId=t.android.package)}catch{}if(i)try{i.locale&&(h.locale=i.locale),i.timezone&&(h.timezone=i.timezone)}catch{}if("web"===r&&"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}()),f),r={message:t.message||"Unknown error",stack:t.stack,timestamp:(new Date).toISOString(),environment:this.config.environment,platform:l(),sessionId:(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),deviceInfo:i,sdkVersion:"1.0.0",extra:{...this.context,...s,...this.user?{user:this.user}:{}},breadcrumbs:this.breadcrumbs.getAll()};if("development"!==this.config.environment&&t.stack){let e=t.stack.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?/);if(e||(e=t.stack.match(/^[^@]+@(.+?):(\d+):(\d+)$/m)),e){const t=function(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 p(e.slice(s+1))}try{const t=new URL(e).pathname.replace(/^\//,"");if(t)return t}catch{}return p(e)}(e[1]);!t||!(h=t)||h.includes("node_modules")||/\.bundle(\/|$|:)/.test(h)||/bundle(\.js|\.map)$/.test(h)||h.startsWith("[native code]")||h.startsWith("native ")||(r.filename=t),r.lineno=parseInt(e[2],10),r.colno=parseInt(e[3],10)}void 0===r.lineno&&"number"==typeof s?.lineno&&(r.lineno=s.lineno),void 0===r.colno&&"number"==typeof s?.colno&&(r.colno=s.colno)}var h;return r}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)}};y.instance=null;var w=y,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=w.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.WatchErrorBoundary=m,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/react.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"react";import{Platform as e,Dimensions as s}from"react-native";import{jsx as i}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')}),n=null;try{n=r("@react-native-async-storage/async-storage").default}catch{}function h(t){return"object"==typeof t&&null!==t}function o(t){return!!h(t)&&"string"==typeof t.message&&"string"==typeof t.timestamp}var a=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(){n&&(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(n)for(await this.hydratePromise.catch(()=>{});this.persistCompletedVersion<this.persistRequestedVersion;){const t=this.persistRequestedVersion;await this.persistSnapshot(),this.persistCompletedVersion=t}}async persistSnapshot(){if(n)try{if(0===this.queue.length)return void await n.removeItem(this.storageKey);const t={v:1,savedAt:(new Date).toISOString(),apiKeyHash:this.apiKeyHash,events:this.queue};await n.setItem(this.storageKey,JSON.stringify(t))}catch(t){this.config.debug&&console.debug("[CatDoes Watch] Queue persistence failed:",t)}}async hydrateQueue(){if(!n)return;let t=null;try{t=await n.getItem(this.storageKey)}catch{return}if(t)try{const e=JSON.parse(t);let s=[];if(Array.isArray(e))s=e.filter(o);else if(h(e)&&1===e.v&&Array.isArray(e.events)){const t=e.apiKeyHash;if(t&&t!==this.apiKeyHash){try{await n.removeItem(this.storageKey)}catch{}return}s=e.events.filter(o)}if(0===s.length)return void await n.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 n.removeItem(this.storageKey)}catch{}}}},c=null;function u(){const t=e.OS;return"ios"===t?"ios":"android"===t?"android":"web"}var l=null,f=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){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 p=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||("undefined"!=typeof __DEV__&&__DEV__?"development":"production"),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 a({endpoint:this.config.endpoint,apiKey:this.config.apiKey,maxBufferSize:this.config.maxBufferSize,flushInterval:this.config.flushInterval,debug:this.config.debug}),this.breadcrumbs=new f(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),h=this.config.beforeSend(n);h?this.transport.send(h):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,i){const n=(l||(l=function(){const t=function(){try{return r("expo-device")}catch{return null}}(),i=function(){try{return r("expo-constants").default}catch{return null}}(),n=function(){try{return r("expo-localization")}catch{return null}}(),h=u(),o={};try{const{width:t,height:e,scale:i}=s.get("window");o.screenWidth=Math.round(t),o.screenHeight=Math.round(e),o.screenScale=i}catch{}if(o.osName=e.OS,e.Version&&(o.osVersion=String(e.Version)),t&&"web"!==h)try{t.brand&&(o.brand=t.brand),t.manufacturer&&(o.manufacturer=t.manufacturer),t.modelName&&(o.modelName=t.modelName),t.deviceName&&(o.deviceName=t.deviceName),t.osName&&(o.osName=t.osName),t.osVersion&&(o.osVersion=t.osVersion),t.osBuildId&&(o.osBuildId=t.osBuildId),t.platformApiLevel&&(o.platformApiLevel=t.platformApiLevel),"boolean"==typeof t.isDevice&&(o.isDevice=t.isDevice)}catch{}if(i)try{const t=i.expoConfig||i.manifest;t?.version&&(o.appVersion=t.version),i.expoVersion&&(o.expoVersion=i.expoVersion),t?.name&&(o.appName=t.name),"ios"===h&&t?.ios?.bundleIdentifier?o.bundleId=t.ios.bundleIdentifier:"android"===h&&t?.android?.package&&(o.bundleId=t.android.package)}catch{}if(n)try{n.locale&&(o.locale=n.locale),n.timezone&&(o.timezone=n.timezone)}catch{}if("web"===h&&"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}()),l),h={message:t.message||"Unknown error",stack:t.stack,timestamp:(new Date).toISOString(),environment:this.config.environment,platform:u(),sessionId:(c||(c=`sess_${Date.now()}_${function(){const t="abcdefghijklmnopqrstuvwxyz0123456789";try{const e=r("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}()}`),c),deviceInfo:n,sdkVersion:"1.0.0",extra:{...this.context,...i,...this.user?{user:this.user}:{}},breadcrumbs:this.breadcrumbs.getAll()};if("development"!==this.config.environment&&t.stack){let e=t.stack.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?/);if(e||(e=t.stack.match(/^[^@]+@(.+?):(\d+):(\d+)$/m)),e){const t=function(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)}(e[1]);!t||!(o=t)||o.includes("node_modules")||/\.bundle(\/|$|:)/.test(o)||/bundle(\.js|\.map)$/.test(o)||o.startsWith("[native code]")||o.startsWith("native ")||(h.filename=t),h.lineno=parseInt(e[2],10),h.colno=parseInt(e[3],10)}void 0===h.lineno&&"number"==typeof i?.lineno&&(h.lineno=i.lineno),void 0===h.colno&&"number"==typeof i?.colno&&(h.colno=i.colno)}var o;return h}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)}};p.instance=null;var y=p,m=class extends t.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=y.getInstance();s&&s.captureError(t,{componentStack:e.componentStack,source:"WatchErrorBoundary"})}i&&i(t,e)}render(){const{hasError:t,error:e,errorInfo:s}=this.state,{children:r,fallback:n}=this.props;return t&&e?n?i(n,{error:e,errorInfo:s,resetError:this.resetError}):null:r}};function w(t,e){const s=t.displayName||t.name||"Component",r=s=>i(m,{...e,children:i(t,{...s})});return r.displayName=`withWatchErrorBoundary(${s})`,r}export{m as WatchErrorBoundary,w as withWatchErrorBoundary};
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@catdoes/watch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Error tracking and monitoring SDK for React Native and Expo apps - by CatDoes Inc.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./react": {
|
|
15
|
+
"types": "./dist/react.d.ts",
|
|
16
|
+
"import": "./dist/react.mjs",
|
|
17
|
+
"require": "./dist/react.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "cross-env NODE_ENV=production tsup",
|
|
26
|
+
"build:dev": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"error-tracking",
|
|
34
|
+
"monitoring",
|
|
35
|
+
"react-native",
|
|
36
|
+
"expo",
|
|
37
|
+
"catdoes",
|
|
38
|
+
"crash-reporting",
|
|
39
|
+
"error-boundary"
|
|
40
|
+
],
|
|
41
|
+
"author": "CatDoes <support@catdoes.com>",
|
|
42
|
+
"homepage": "https://catdoes.watch",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/catdoes/watch.git"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/catdoes/watch/issues"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@react-native-async-storage/async-storage": ">=1.0.0",
|
|
55
|
+
"expo-crypto": ">=13.0.0",
|
|
56
|
+
"react": ">=18.0.0",
|
|
57
|
+
"react-native": ">=0.72.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"expo-crypto": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@react-native-async-storage/async-storage": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/react": "^19.2.0",
|
|
69
|
+
"cross-env": "^7.0.3",
|
|
70
|
+
"terser": "^5.37.0",
|
|
71
|
+
"tsup": "^8.3.5",
|
|
72
|
+
"typescript": "~5.9.2"
|
|
73
|
+
}
|
|
74
|
+
}
|