@debugkit/sdk 0.0.0-stage → 0.2.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.
Files changed (57) hide show
  1. package/README.md +228 -2
  2. package/dist/chunk-3WRI3DI2.js +41 -0
  3. package/dist/chunk-3WRI3DI2.js.map +1 -0
  4. package/dist/chunk-5C7KJJNO.js +651 -0
  5. package/dist/chunk-5C7KJJNO.js.map +1 -0
  6. package/dist/chunk-EGCXLOFH.cjs +663 -0
  7. package/dist/chunk-EGCXLOFH.cjs.map +1 -0
  8. package/dist/chunk-HO3JW55K.js +79 -0
  9. package/dist/chunk-HO3JW55K.js.map +1 -0
  10. package/dist/chunk-NJ6DTUVX.cjs +83 -0
  11. package/dist/chunk-NJ6DTUVX.cjs.map +1 -0
  12. package/dist/chunk-YELZOB7J.cjs +45 -0
  13. package/dist/chunk-YELZOB7J.cjs.map +1 -0
  14. package/dist/client-ekzlpWIR.d.cts +220 -0
  15. package/dist/client-ekzlpWIR.d.ts +220 -0
  16. package/dist/expo-router.cjs +13 -0
  17. package/dist/expo-router.cjs.map +1 -0
  18. package/dist/expo-router.d.cts +10 -0
  19. package/dist/expo-router.d.ts +10 -0
  20. package/dist/expo-router.js +11 -0
  21. package/dist/expo-router.js.map +1 -0
  22. package/dist/expo.cjs +97 -0
  23. package/dist/expo.cjs.map +1 -0
  24. package/dist/expo.d.cts +14 -0
  25. package/dist/expo.d.ts +14 -0
  26. package/dist/expo.js +47 -0
  27. package/dist/expo.js.map +1 -0
  28. package/dist/index.cjs +40 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +11 -0
  31. package/dist/index.d.ts +11 -0
  32. package/dist/index.js +3 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/node.cjs +140 -0
  35. package/dist/node.cjs.map +1 -0
  36. package/dist/node.d.cts +20 -0
  37. package/dist/node.d.ts +20 -0
  38. package/dist/node.js +100 -0
  39. package/dist/node.js.map +1 -0
  40. package/dist/react-native.cjs +38 -0
  41. package/dist/react-native.cjs.map +1 -0
  42. package/dist/react-native.d.cts +51 -0
  43. package/dist/react-native.d.ts +51 -0
  44. package/dist/react-native.js +5 -0
  45. package/dist/react-native.js.map +1 -0
  46. package/dist/react.cjs +25 -0
  47. package/dist/react.cjs.map +1 -0
  48. package/dist/react.d.cts +50 -0
  49. package/dist/react.d.ts +50 -0
  50. package/dist/react.js +4 -0
  51. package/dist/react.js.map +1 -0
  52. package/expo/package.json +6 -0
  53. package/expo-router/package.json +6 -0
  54. package/node/package.json +6 -0
  55. package/package.json +121 -3
  56. package/react/package.json +6 -0
  57. package/react-native/package.json +6 -0
package/README.md CHANGED
@@ -1,3 +1,229 @@
1
- # Temporary Holding Version
1
+ # @debugkit/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Log errors and events from your app into **your own** cloud. This is the client half of
4
+ [deBug](https://github.com/Abantu-Digital/deBug): events go to an ingest function you deploy
5
+ in Firebase or AWS, and a dashboard you run yourself tells you what is failing and why.
6
+
7
+ - About 6 KB gzipped, with no runtime dependencies in your bundle.
8
+ - Works with React, React Native (bare and Expo), Expo Router, TanStack Start/Router, Next.js
9
+ and plain Node.
10
+ - **Works on its own once initialised:** crashes, failed requests, sessions and the steps
11
+ leading up to each error are recorded without any extra code. See
12
+ [What it records by itself](#what-it-records-by-itself).
13
+ - Batches, retries, rate-limits, and queues events while offline.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @debugkit/sdk
19
+ ```
20
+
21
+ Pick the entry point for your platform. Each one adds the right device context and error
22
+ hooks, and they all share the same `deBug` client.
23
+
24
+ | You have | Import from | Also install |
25
+ |---|---|---|
26
+ | Expo | `@debugkit/sdk/expo` | `npx expo install expo-device expo-application` |
27
+ | Expo Router | `@debugkit/sdk/expo-router`, plus the above | |
28
+ | Bare React Native | `@debugkit/sdk/react-native` | `react-native-device-info` (optional, for model and app version) |
29
+ | React on the web (Vite and others) | `@debugkit/sdk/react` | |
30
+ | TanStack Start / Router | `@debugkit/sdk/react` for the browser, `@debugkit/sdk/node` for the server | |
31
+ | Next.js, Remix, Express, any Node server | `@debugkit/sdk/node` | |
32
+ | Anything else (workers, edge, vanilla JS) | `@debugkit/sdk` | |
33
+
34
+ Your ingest endpoint and key come from your deBug deployment. During development, point at a
35
+ dashboard running in demo mode: `http://localhost:4400/api/ingest` with key `pk_local`.
36
+
37
+ ## What it records by itself
38
+
39
+ Calling `init()` (or `initExpo`, `initReactNative`, `initNode`) turns on everything in this
40
+ table. The manual API below is for what only you know: which part of the app a step belongs to,
41
+ and whether it succeeded.
42
+
43
+ | | Browser | React Native / Expo | Node |
44
+ |---|---|---|---|
45
+ | Uncaught errors and unhandled rejections | ✓ | ✓ (fatal crashes marked critical) | ✓ (flushes, then exits like Node) |
46
+ | Render errors | with `DebugErrorBoundary` | with `DebugErrorBoundary` | |
47
+ | **Failed requests** (5xx, timeouts, network failures) as errors, every request as a breadcrumb | fetch, XHR | fetch, XHR (axios too) | fetch, `node:http` (axios, SDKs), plus 5xx your server sends |
48
+ | **Sessions**: crash-free and error-free rates per release | one per page load | new session after 30 min in the background | one per process |
49
+ | Navigation breadcrumbs | any router (History API) | add `useDebugExpoRouter` or `trackReactNavigation` | |
50
+ | Clicks on buttons and links, by their label | ✓ | | |
51
+ | `console.warn` / `console.error` as breadcrumbs | ✓ | ✓ | off by default |
52
+ | Foreground/background, online/offline | ✓ | foreground/background | |
53
+ | Device, OS, app version, network type on every event | ✓ | ✓ | ✓ |
54
+
55
+ Every error carries the last 30 breadcrumbs, so the dashboard shows what happened just
56
+ before it: the screen, the click, the request that failed.
57
+
58
+ **Privacy.** Query strings and fragments are dropped from every URL before anything is
59
+ recorded, and ids in paths become `:id` (`/v1/orders/8812?token=…` becomes `/v1/orders/:id`).
60
+ Request and response bodies, headers and what users type are never read. Use `beforeSend` to
61
+ scrub anything else.
62
+
63
+ **Noise control.** At most 60 errors a minute are sent (`maxErrorsPerMinute`), and one repeating
64
+ error gets at most a sixth of that. Breadcrumbs that repeat are collapsed into one. `sampleRate`
65
+ keeps a fraction of errors and events. Sessions and fatal crashes are never sampled out, so
66
+ crash-free rates stay accurate.
67
+
68
+ ### Tuning it
69
+
70
+ ```ts
71
+ deBug.init({
72
+ endpoint, ingestKey, app: { id: "your-app", version: "2.4.0" },
73
+ autoCapture: { console: false, clicks: false }, // or `false` to turn everything off
74
+ network: {
75
+ flows: [["/v2/auth/", "auth.login"], [/\/payments\//, "checkout.payment"]], // tag requests by URL
76
+ ignoreUrls: ["analytics.example.com"],
77
+ isFailure: (status) => status >= 500 || status === 429, // default: 500 and above
78
+ slowMs: 3000, // slower requests get a warning breadcrumb
79
+ },
80
+ sampleRate: 1,
81
+ maxErrorsPerMinute: 60,
82
+ });
83
+ ```
84
+
85
+ Requests to your ingest endpoint and to the Metro and Expo dev servers are never recorded.
86
+
87
+ ## Expo
88
+
89
+ ```tsx
90
+ // src/debug.ts
91
+ import { initExpo } from "@debugkit/sdk/expo";
92
+
93
+ initExpo({
94
+ endpoint: "https://<your-ingest-url>",
95
+ ingestKey: "pk_live_yourapp",
96
+ app: { id: "your-app" }, // version and build are read from expo-application
97
+ });
98
+ ```
99
+
100
+ ```tsx
101
+ // src/app/_layout.tsx
102
+ import "@/debug";
103
+ import { DebugErrorBoundary } from "@debugkit/sdk/expo";
104
+ import { useDebugExpoRouter } from "@debugkit/sdk/expo-router";
105
+
106
+ export default function RootLayout() {
107
+ useDebugExpoRouter(); // a breadcrumb for every screen change
108
+ return (
109
+ <DebugErrorBoundary flow="app.root" fallback={({ reset }) => <CrashScreen onRetry={reset} />}>
110
+ <Stack />
111
+ </DebugErrorBoundary>
112
+ );
113
+ }
114
+ ```
115
+
116
+ In Expo Go, the app version reported is Expo Go's own. Development and production builds
117
+ report yours.
118
+
119
+ ## Bare React Native
120
+
121
+ ```tsx
122
+ import DeviceInfo from "react-native-device-info";
123
+ import { initReactNative, DebugErrorBoundary, trackReactNavigation } from "@debugkit/sdk/react-native";
124
+
125
+ initReactNative({ endpoint, ingestKey, app: { id: "your-app" }, deviceInfo: DeviceInfo });
126
+
127
+ const nav = useNavigationContainerRef();
128
+ <NavigationContainer ref={nav} {...trackReactNavigation(nav)}>…</NavigationContainer>
129
+ ```
130
+
131
+ Without `react-native-device-info` you still get the OS, OS version and JS engine. On Android
132
+ you also get the model and brand.
133
+
134
+ React Native also gets:
135
+
136
+ - **Crashes** through React Native's global error handler.
137
+ - **Unhandled promise rejections** (Hermes). In production only by default, so the yellow-box
138
+ warning still works in development. Pass `captureUnhandledRejections: true` to capture them
139
+ in development too.
140
+ - **A flush when the app goes to the background.** Pass AsyncStorage as `storage` to keep
141
+ unsent events across restarts.
142
+
143
+ ## React (web)
144
+
145
+ ```tsx
146
+ import { deBug, DebugErrorBoundary } from "@debugkit/sdk/react";
147
+
148
+ deBug.init({ endpoint, ingestKey, app: { id: "your-web-app", version: "1.4.0" } });
149
+
150
+ createRoot(el).render(
151
+ <DebugErrorBoundary fallback={<p>Something went wrong.</p>}>
152
+ <App />
153
+ </DebugErrorBoundary>,
154
+ );
155
+ ```
156
+
157
+ Uncaught errors and unhandled rejections are captured from `window`. The OS, browser, screen
158
+ and network type are detected automatically.
159
+
160
+ ## TanStack Start / Router
161
+
162
+ ```tsx
163
+ // src/router.tsx: the browser side
164
+ import { deBug, trackTanStackRouter } from "@debugkit/sdk/react";
165
+
166
+ if (typeof window !== "undefined") deBug.init({ endpoint, ingestKey, app: { id: "web" } });
167
+
168
+ export function getRouter() {
169
+ const router = createRouter({ routeTree });
170
+ if (typeof window !== "undefined") trackTanStackRouter(router);
171
+ return router;
172
+ }
173
+ ```
174
+
175
+ ```ts
176
+ // src/server/debug.ts: server only
177
+ import { deBug, initNode } from "@debugkit/sdk/node";
178
+
179
+ export const serverDebug = () =>
180
+ deBug.initialized ? deBug : initNode({ endpoint, ingestKey, app: { id: "web", environment: "server" } });
181
+ ```
182
+
183
+ ```ts
184
+ // src/server/fns.ts
185
+ import { createServerFn } from "@tanstack/react-start";
186
+ import { withDebug } from "@debugkit/sdk/node";
187
+ import { serverDebug } from "./debug";
188
+
189
+ export const charge = createServerFn({ method: "POST" }).handler(
190
+ withDebug("checkout.payment", async ({ data }) => {
191
+ serverDebug(); // initialise lazily, inside the handler, so none of this reaches the client bundle
192
+ /* … */
193
+ }),
194
+ );
195
+ ```
196
+
197
+ When the browser calls a server function that is wrapped in `withDebug`, record only the
198
+ outcome there, so the error isn't counted twice:
199
+ `deBug.track("checkout.payment", "charge", () => charge(), { captureError: false })`.
200
+
201
+ ## Node
202
+
203
+ `initNode()` captures uncaught exceptions and unhandled rejections. It flushes events and then
204
+ exits the way Node would. Pass `captureProcessErrors: false` to manage those hooks yourself,
205
+ for example inside a dev server.
206
+
207
+ On edge runtimes such as Cloudflare Workers, timers aren't allowed at startup. Use the base
208
+ entry with `flushIntervalMs: 0`, and `await deBug.flush()` before each response returns.
209
+
210
+ ## The API
211
+
212
+ ```ts
213
+ deBug.error(err, { flow: "checkout.payment", step: "create_intent" });
214
+ deBug.event("signup_abandoned", { flow: "onboarding.signup", outcome: "abandoned" });
215
+ await deBug.track("auth.login", "submit_otp", () => api.verifyOtp(code)); // success or failure, plus the error
216
+ deBug.breadcrumb("navigation", "Login → OtpScreen");
217
+ deBug.setUser("user_123");
218
+ deBug.setContext({ app: { environment: "staging" } });
219
+ await deBug.flush();
220
+ ```
221
+
222
+ `flow` names the part of your app, in dotted lowercase like `auth.login`. It's the main axis
223
+ of the dashboard, so keep it consistent.
224
+
225
+ To scrub personal data before anything leaves the device, use `beforeSend`:
226
+
227
+ ```ts
228
+ deBug.init({ …, beforeSend: (e) => (e.type === "event" && e.name === "secret" ? null : e) });
229
+ ```
@@ -0,0 +1,41 @@
1
+ import { deBug, __publicField } from './chunk-5C7KJJNO.js';
2
+ import { Component, useEffect } from 'react';
3
+
4
+ var DebugErrorBoundary = class extends Component {
5
+ constructor() {
6
+ super(...arguments);
7
+ __publicField(this, "state", { error: null });
8
+ __publicField(this, "reset", () => this.setState({ error: null }));
9
+ }
10
+ static getDerivedStateFromError(error) {
11
+ return { error };
12
+ }
13
+ componentDidCatch(error, info) {
14
+ var _a, _b, _c;
15
+ const client = (_a = this.props.client) != null ? _a : deBug;
16
+ if (info.componentStack) {
17
+ client.breadcrumb("ui", "React render error", { componentStack: info.componentStack.trim().slice(0, 1500) }, "error");
18
+ }
19
+ client.error(error, { flow: this.props.flow, handled: false, tags: { source: "error-boundary" } });
20
+ void client.flush();
21
+ (_c = (_b = this.props).onError) == null ? void 0 : _c.call(_b, error, info);
22
+ }
23
+ render() {
24
+ const { error } = this.state;
25
+ if (!error) return this.props.children;
26
+ const { fallback } = this.props;
27
+ return typeof fallback === "function" ? fallback({ error, reset: this.reset }) : fallback != null ? fallback : null;
28
+ }
29
+ };
30
+ function useDebugNavigation(path, client = deBug) {
31
+ useEffect(() => {
32
+ if (path) client.navigated(path);
33
+ }, [path, client]);
34
+ }
35
+ function trackTanStackRouter(router, client = deBug) {
36
+ return router.subscribe("onResolved", ({ toLocation }) => client.navigated(toLocation.pathname));
37
+ }
38
+
39
+ export { DebugErrorBoundary, trackTanStackRouter, useDebugNavigation };
40
+ //# sourceMappingURL=chunk-3WRI3DI2.js.map
41
+ //# sourceMappingURL=chunk-3WRI3DI2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.tsx"],"names":[],"mappings":";;;AAqBO,IAAM,kBAAA,GAAN,cAAiC,SAAA,CAA0C;AAAA,EAA3E,WAAA,GAAA;AAAA,IAAA,KAAA,CAAA,GAAA,SAAA,CAAA;AACL,IAAA,aAAA,CAAA,IAAA,EAAA,OAAA,EAAe,EAAE,OAAO,IAAA,EAAK,CAAA;AAgB7B,IAAA,aAAA,CAAA,IAAA,EAAA,OAAA,EAAQ,MAAM,IAAA,CAAK,QAAA,CAAS,EAAE,KAAA,EAAO,MAAM,CAAA,CAAA;AAAA,EAAA;AAAA,EAd3C,OAAO,yBAAyB,KAAA,EAAqB;AACnD,IAAA,OAAO,EAAE,KAAA,EAAM;AAAA,EACjB;AAAA,EAEA,iBAAA,CAAkB,OAAc,IAAA,EAAiB;AA5BnD,IAAA,IAAA,EAAA,EAAA,EAAA,EAAA,EAAA;AA6BI,IAAA,MAAM,MAAA,GAAA,CAAS,EAAA,GAAA,IAAA,CAAK,KAAA,CAAM,MAAA,KAAX,IAAA,GAAA,EAAA,GAAqB,KAAA;AACpC,IAAA,IAAI,KAAK,cAAA,EAAgB;AACvB,MAAA,MAAA,CAAO,UAAA,CAAW,IAAA,EAAM,oBAAA,EAAsB,EAAE,gBAAgB,IAAA,CAAK,cAAA,CAAe,IAAA,EAAK,CAAE,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA,IAAK,OAAO,CAAA;AAAA,IACtH;AACA,IAAA,MAAA,CAAO,KAAA,CAAM,KAAA,EAAO,EAAE,IAAA,EAAM,KAAK,KAAA,CAAM,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,IAAA,EAAM,EAAE,MAAA,EAAQ,gBAAA,IAAoB,CAAA;AACjG,IAAA,KAAK,OAAO,KAAA,EAAM;AAClB,IAAA,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,IAAA,CAAK,KAAA,EAAM,OAAA,KAAX,IAAA,GAAA,MAAA,GAAA,EAAA,CAAA,IAAA,CAAA,EAAA,EAAqB,KAAA,EAAO,IAAA,CAAA;AAAA,EAC9B;AAAA,EAIA,MAAA,GAAS;AACP,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,IAAA,CAAK,KAAA;AACvB,IAAA,IAAI,CAAC,KAAA,EAAO,OAAO,IAAA,CAAK,KAAA,CAAM,QAAA;AAC9B,IAAA,MAAM,EAAE,QAAA,EAAS,GAAI,IAAA,CAAK,KAAA;AAC1B,IAAA,OAAO,OAAO,QAAA,KAAa,UAAA,GAAa,QAAA,CAAS,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,CAAK,KAAA,EAAO,CAAA,GAAK,QAAA,IAAA,IAAA,GAAA,QAAA,GAAY,IAAA;AAAA,EAChG;AACF;AAGO,SAAS,kBAAA,CAAmB,IAAA,EAA0B,MAAA,GAAsB,KAAA,EAAO;AACxF,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,IAAA,EAAM,MAAA,CAAO,SAAA,CAAU,IAAI,CAAA;AAAA,EACjC,CAAA,EAAG,CAAC,IAAA,EAAM,MAAM,CAAC,CAAA;AACnB;AAYO,SAAS,mBAAA,CAAoB,MAAA,EAAoB,MAAA,GAAsB,KAAA,EAAO;AACnF,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,YAAA,EAAc,CAAC,EAAE,UAAA,EAAW,KAAM,MAAA,CAAO,SAAA,CAAU,UAAA,CAAW,QAAQ,CAAC,CAAA;AACjG","file":"chunk-3WRI3DI2.js","sourcesContent":["import { Component, useEffect, type ErrorInfo, type ReactNode } from \"react\";\nimport { deBug, type DebugClient } from \"./index\";\n\nexport interface DebugErrorBoundaryProps {\n children?: ReactNode;\n /** What to render after a crash. A function gets the error and a reset callback. */\n fallback?: ReactNode | ((props: { error: Error; reset: () => void }) => ReactNode);\n /** Part of the app this boundary protects, e.g. \"checkout\". */\n flow?: string;\n client?: DebugClient;\n onError?: (error: Error, info: ErrorInfo) => void;\n}\n\ninterface State {\n error: Error | null;\n}\n\n/**\n * Catches render errors that React would otherwise swallow (or turn into a white/red screen)\n * and reports them. Works in React DOM and React Native.\n */\nexport class DebugErrorBoundary extends Component<DebugErrorBoundaryProps, State> {\n state: State = { error: null };\n\n static getDerivedStateFromError(error: Error): State {\n return { error };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n const client = this.props.client ?? deBug;\n if (info.componentStack) {\n client.breadcrumb(\"ui\", \"React render error\", { componentStack: info.componentStack.trim().slice(0, 1500) }, \"error\");\n }\n client.error(error, { flow: this.props.flow, handled: false, tags: { source: \"error-boundary\" } });\n void client.flush();\n this.props.onError?.(error, info);\n }\n\n reset = () => this.setState({ error: null });\n\n render() {\n const { error } = this.state;\n if (!error) return this.props.children;\n const { fallback } = this.props;\n return typeof fallback === \"function\" ? fallback({ error, reset: this.reset }) : (fallback ?? null);\n }\n}\n\n/** Adds a navigation breadcrumb whenever `path` changes. Works with any router. */\nexport function useDebugNavigation(path: string | undefined, client: DebugClient = deBug) {\n useEffect(() => {\n if (path) client.navigated(path);\n }, [path, client]);\n}\n\n/** Minimal shape of a TanStack Router instance (avoids a hard dependency). */\ninterface RouterLike {\n subscribe(event: \"onResolved\", fn: (e: { toLocation: { pathname: string; href?: string } }) => void): () => void;\n}\n\n/**\n * Records a breadcrumb for every navigation in a TanStack Router app:\n * const router = createRouter({ routeTree }); trackTanStackRouter(router);\n * Returns an unsubscribe function.\n */\nexport function trackTanStackRouter(router: RouterLike, client: DebugClient = deBug) {\n return router.subscribe(\"onResolved\", ({ toLocation }) => client.navigated(toLocation.pathname));\n}\n\nexport { deBug } from \"./index\";\n"]}