@reopt-ai/data-sdk-client 0.1.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 +168 -0
- package/dist/chunk-47EC5ATF.js +106 -0
- package/dist/chunk-47EC5ATF.js.map +1 -0
- package/dist/chunk-4MTDZBRS.js +57 -0
- package/dist/chunk-4MTDZBRS.js.map +1 -0
- package/dist/chunk-BIR7OFR3.js +1575 -0
- package/dist/chunk-BIR7OFR3.js.map +1 -0
- package/dist/client-g75DV57n.d.ts +180 -0
- package/dist/exceptions-NQHZUDYO.js +9 -0
- package/dist/exceptions-NQHZUDYO.js.map +1 -0
- package/dist/index.cjs +1873 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +1880 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.ts +34 -0
- package/dist/next.js +61 -0
- package/dist/next.js.map +1 -0
- package/dist/react.cjs +1841 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.ts +76 -0
- package/dist/react.js +26 -0
- package/dist/react.js.map +1 -0
- package/dist/tracing-LC3NZND7.js +91 -0
- package/dist/tracing-LC3NZND7.js.map +1 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# @reopt-ai/data-sdk-client
|
|
2
|
+
|
|
3
|
+
Browser SDK for [reopt-data](https://github.com/reopt-ai/reopt-data). Three entry points, one client:
|
|
4
|
+
|
|
5
|
+
| Entry | Use it from |
|
|
6
|
+
| --------------------------------- | --------------------------------------------------------------- |
|
|
7
|
+
| `@reopt-ai/data-sdk-client` | a `<script>` tag, a plain SPA, any framework — no directive |
|
|
8
|
+
| `@reopt-ai/data-sdk-client/react` | React client components (`"use client"`) |
|
|
9
|
+
| `@reopt-ai/data-sdk-client/next` | Next.js App Router — importing it from a server component is OK |
|
|
10
|
+
|
|
11
|
+
Server-side tracking (route handlers, server components, workers) lives in `@reopt-ai/data-sdk-server`. Importing that package from a client component fails at build time on purpose.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @reopt-ai/data-sdk-client
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Next.js App Router
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// app/layout.tsx — a server component
|
|
23
|
+
import { ReoptProvider, ReoptPageView, ReoptWebVitals } from "@reopt-ai/data-sdk-client/next";
|
|
24
|
+
import { getBootstrap } from "@/lib/reopt"; // @reopt-ai/data-sdk-server — optional
|
|
25
|
+
|
|
26
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
27
|
+
const bootstrap = await getBootstrap(); // null when the server knows nothing about this visitor
|
|
28
|
+
return (
|
|
29
|
+
<html>
|
|
30
|
+
<body>
|
|
31
|
+
<ReoptProvider
|
|
32
|
+
config={{ writeKey: process.env.NEXT_PUBLIC_REOPT_WRITE_KEY!, baseUrl: "/ingest" }}
|
|
33
|
+
bootstrap={bootstrap}
|
|
34
|
+
>
|
|
35
|
+
<ReoptPageView />
|
|
36
|
+
<ReoptWebVitals />
|
|
37
|
+
{children}
|
|
38
|
+
</ReoptProvider>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `ReoptPageView` sends `$pageview` on every navigation — pathname _and_ query string — and a `$pageleave` with time-on-page for the page it replaces. It is a **sibling** of your content, not a wrapper, so a navigation does not rerender your tree. It carries its own `<Suspense>`, so it will not knock a prerendered route into client rendering.
|
|
46
|
+
- `ReoptWebVitals` forwards Next's Core Web Vitals as `$web_vitals`.
|
|
47
|
+
- `baseUrl: "/ingest"` assumes the `reoptProxy` from `@reopt-ai/data-sdk-server/proxy` rewrites `/ingest/*` to your reopt-data deployment. Without the proxy, pass the deployment origin.
|
|
48
|
+
- `bootstrap` is optional. With it, the first render already agrees with the server about who the visitor is and what they consented to.
|
|
49
|
+
|
|
50
|
+
Then, anywhere in a client component:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
"use client";
|
|
54
|
+
import { useTrack, useIdentify } from "@reopt-ai/data-sdk-client/next";
|
|
55
|
+
|
|
56
|
+
export function BuyButton() {
|
|
57
|
+
const track = useTrack();
|
|
58
|
+
return <button onClick={() => track("checkout_started", { plan: "pro" })}>Buy</button>;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## React (any router)
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { ReoptProvider } from "@reopt-ai/data-sdk-client/react";
|
|
66
|
+
|
|
67
|
+
<ReoptProvider config={{ writeKey, baseUrl: "https://data.example.com" }}>
|
|
68
|
+
<App />
|
|
69
|
+
</ReoptProvider>;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The provider sends the first `$pageview` itself (`initialPageView={false}` to opt out). Call `usePageView()` on route changes; there is no `history.pushState` patching.
|
|
73
|
+
|
|
74
|
+
Hooks: `useReopt`, `useReoptClient`, `useTrack`, `useIdentify`, `usePageView`, `useConsent`, `useTrackOnMount`, `usePageViewOnMount`.
|
|
75
|
+
|
|
76
|
+
The client is created **during render** and kept on `window` per write key, so a child's effect can use it on first mount, StrictMode's double render shares one instance, and two copies of this package on one page do not create two devices. It is not closed on unmount; call `client.close()` if you need to.
|
|
77
|
+
|
|
78
|
+
## Vanilla
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { init, track, identify, pageView } from "@reopt-ai/data-sdk-client";
|
|
82
|
+
|
|
83
|
+
init({ writeKey: "wk_…", baseUrl: "https://data.example.com" });
|
|
84
|
+
track("signup_completed", { plan: "pro" });
|
|
85
|
+
identify("cus_123", { email: "a@b.c" });
|
|
86
|
+
pageView(); // sent automatically on init(); call this on client-side navigation
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
init({
|
|
93
|
+
writeKey: "wk_…",
|
|
94
|
+
baseUrl: "https://data.example.com", // or "/ingest" behind the proxy
|
|
95
|
+
bootstrap: null, // from @reopt-ai/data-sdk-server getBootstrap()
|
|
96
|
+
identity: {
|
|
97
|
+
storage: "auto", // cookie → localStorage → memory. Cookies survive Safari ITP; localStorage does not
|
|
98
|
+
cookieDomain: undefined, // ".example.com" to share across subdomains
|
|
99
|
+
cookieMaxAgeSeconds: 400 * 24 * 3600,
|
|
100
|
+
},
|
|
101
|
+
capture: {
|
|
102
|
+
pageview: true, // first $pageview on init (the Next entry sets false — <ReoptPageView /> owns it)
|
|
103
|
+
pageleave: true, // $pageleave with `duration` (seconds) and `scroll_depth`
|
|
104
|
+
scrollDepth: true,
|
|
105
|
+
exceptions: false, // $exception for uncaught errors and unhandled rejections; loads as its own chunk when on
|
|
106
|
+
},
|
|
107
|
+
tracingHeaders: false, // opt-in: add reopt-device-id to same-origin fetch/XHR (true) or to listed hosts; loads as its own chunk
|
|
108
|
+
normalizePath: (pathname) => pathname, // see below; applied to every path the SDK stamps itself
|
|
109
|
+
fetch: undefined, // transport override for tests
|
|
110
|
+
consent: { categories: ["analytics", "marketing"], defaultConsent: true },
|
|
111
|
+
batch: { size: 100, intervalMs: 1000, maxBytes: 400_000 },
|
|
112
|
+
retry: { maxRetries: 3, baseDelay: 1000, maxDelay: 30000, jitter: 0.1 },
|
|
113
|
+
circuitBreaker: { failureThreshold: 5, recoveryTimeout: 60000 },
|
|
114
|
+
debug: false,
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Runtime-resolved keys, manual page views, external consent
|
|
119
|
+
|
|
120
|
+
The three things a multi-tenant host needs, in the shape it needs them:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Server component: the write key comes from your database, per tenant, per request.
|
|
124
|
+
const writeKey = await resolveBrandWriteKey(brandId);
|
|
125
|
+
<AnalyticsMount config={{ writeKey, baseUrl: "/ingest", capture: { pageview: false }, consent: { persist: false } }} />;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- **`capture.pageview: false`** and call `pageView({ path, properties })` yourself — for example only when the page carries the markers that make it a real published page. There is no automatic page view you have to suppress, and `init()` is idempotent per write key, so a remount or a soft navigation never creates a second client.
|
|
129
|
+
- **`consent.persist: false`** when a consent banner already owns the decision: sync it with `setConsent("analytics", allowed)` before tracking, and the SDK keeps no cookie of its own that could disagree with the banner's. An undecided visitor is allowed (opt-out model).
|
|
130
|
+
- **`normalizePath`** rewrites high-cardinality paths (`/workspace/8f3…/customers/2a1…` → `/workspace/:id/customers/:id`) and can lift the removed ids into properties for breakdowns. It runs wherever the SDK stamps a path itself — the default `$pageview` path, `$pageleave`, `$web_vitals`, `$exception` — and **not** on a `path` you pass to `pageView()` explicitly, so use the same function in both places. Synchronous, pure; if it throws, the raw path is used.
|
|
131
|
+
- **`getDeviceId()`** returns the id this page is tracked under, so your own server call can carry it explicitly (`submitForm(body, { deviceId })`) and the server can record the conversion with `deviceId` on the event — the browser session and the server-side conversion then land on the same device without any header magic. `null` before `init()` or when analytics is disabled.
|
|
132
|
+
- **`properties`** in `init()` sets those same global properties from the very first event — web vitals can fire the instant the client exists, before an effect gets to call `register()`. Give it the page context the server already knows.
|
|
133
|
+
- **`register(properties)`** attaches properties to every event from then on — including the automatic ones. This is how a host puts its own breakdown axis (`page_id`, a tenant) on `$web_vitals` and `$pageleave`, which the SDK otherwise stamps with only a path. An event's own properties win; `reset()` clears them.
|
|
134
|
+
- **Fail-open.** A missing `writeKey` or `baseUrl` logs a warning and yields a disabled client whose every call is a no-op; nothing throws. Analytics must never be why a page fails.
|
|
135
|
+
- **`fetch`** injects the transport. A test can hand in a recording function and assert on the exact payload the SDK built, instead of intercepting the network.
|
|
136
|
+
|
|
137
|
+
## Bootstrap and caching (Next.js)
|
|
138
|
+
|
|
139
|
+
`bootstrap` comes from `getBootstrap()` in `@reopt-ai/data-sdk-server`, which reads `cookies()`. Three ways that goes wrong:
|
|
140
|
+
|
|
141
|
+
1. With `cacheComponents: true`, calling it inside a `"use cache"` function (or a helper that function calls) **passes `next build` and fails at `next start`** with `next-request-in-use-cache` — on dynamic routes only when the route runs, so it gets past your gate. Read it outside the cache, after `await connection()`, and pass the value down.
|
|
142
|
+
2. Passing the `cookies()` promise as a prop into a cached component does not error; the build **hangs** or the prerender times out.
|
|
143
|
+
3. Without `cacheComponents` (older apps, `unstable_cache`, fetch caching) the value really does get cached — **every visitor gets the same device id**. In those apps do not pass `bootstrap`; rely on the proxy's cookie seeding and the client's own identity.
|
|
144
|
+
|
|
145
|
+
## Size
|
|
146
|
+
|
|
147
|
+
The production bundle is checked on every build (`scripts/check-size.mjs`): each entry is bundled as a consumer would (minified, `NODE_ENV=production`, framework externals) and must stay under **10 KB gzip** for the vanilla entry (10.5 / 10.75 KB for `./react` / `./next`) and carry no zod. Opt-in features (tracing headers, exception capture) are separate chunks. Event validation in production is a hand-written mirror of the contract; the zod schema runs in development only and is dropped by the consumer's bundler.
|
|
148
|
+
|
|
149
|
+
## What it does that you would otherwise have to remember
|
|
150
|
+
|
|
151
|
+
- **Identity in a cookie.** `reopt_<writeKey>_device`, `SameSite=Lax`, `Secure` on https, 400 days. The server SDK and the proxy read the same cookie, so server-side events land on the same device. An id from the previous SDK generation (`localStorage` `reopt_device_id`) is migrated on first load.
|
|
152
|
+
- **Consent in a cookie** (`reopt_<writeKey>_consent`), so the server stops sending when the browser does. Refusing `analytics` stops everything; a decision made in the browser beats the server's bootstrap.
|
|
153
|
+
- **`reset()` becomes a new device.** Log out on a shared computer and the next person is not attributed to the previous one.
|
|
154
|
+
- **Byte-aware batches.** Requests stay under the server's 512 KB cap; unload flushes stay under fetch's 64 KB keepalive limit; an event that could never fit is refused at `track()` with `reason: "payload_too_large"` instead of being dropped by the server later.
|
|
155
|
+
- **Offline queue** in `localStorage`, restored once by the next page.
|
|
156
|
+
- **Corrected clock.** With a `bootstrap`, a device clock more than 30 s off the server's is corrected before events are stamped.
|
|
157
|
+
- **Event ids are UUIDv7**, so they sort in creation order.
|
|
158
|
+
|
|
159
|
+
## Events it sends
|
|
160
|
+
|
|
161
|
+
Names and property keys are exported from `@reopt-ai/data-contract/events` (`AUTO_EVENT_NAMES`, `AUTO_EVENT_PROPERTIES`); the low-cardinality subset suitable as rollup dimensions is `AUTO_EVENT_ROLLUP_KEYS`. Derive catalogue entries from those rather than retyping them.
|
|
162
|
+
|
|
163
|
+
| Event | Properties |
|
|
164
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
165
|
+
| `$pageview` | `path`, `origin`, `title`, `referrer`, `utm_*`, `search` (Next, when present), plus anything `normalizePath` lifts |
|
|
166
|
+
| `$pageleave` | `path`, `origin`, `duration` (s), `scroll_depth` (0–100) |
|
|
167
|
+
| `$web_vitals` | `metric_name`, `metric_id`, `value`, `delta`, `rating`, `navigation_type`, `path` |
|
|
168
|
+
| `$exception` | `$exception_type`, `$exception_message`, `$exception_stack`, `$exception_source`, `$exception_handled`, `path` |
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOrCreateClient
|
|
3
|
+
} from "./chunk-BIR7OFR3.js";
|
|
4
|
+
|
|
5
|
+
// src/react/index.tsx
|
|
6
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
var ReoptContext = createContext(null);
|
|
9
|
+
var NOT_AVAILABLE = { eventId: "", queued: false, reason: "tracking_paused" };
|
|
10
|
+
var NO_FLUSH = { status: "idle", sent: 0, failed: 0, pending: 0 };
|
|
11
|
+
function ReoptProvider({ children, config, bootstrap, initialPageView = true }) {
|
|
12
|
+
const client = typeof window === "undefined" ? null : getOrCreateClient(
|
|
13
|
+
{ ...config, bootstrap: bootstrap !== void 0 ? bootstrap : config.bootstrap ?? null },
|
|
14
|
+
{ pageview: initialPageView }
|
|
15
|
+
);
|
|
16
|
+
const value = useMemo(() => bindClient(client), [client]);
|
|
17
|
+
return /* @__PURE__ */ jsx(ReoptContext.Provider, { value, children });
|
|
18
|
+
}
|
|
19
|
+
function bindClient(client) {
|
|
20
|
+
const call = (fn, fallback) => client ? fn(client) : fallback;
|
|
21
|
+
return {
|
|
22
|
+
client,
|
|
23
|
+
track: (options) => call((c) => c.track(options), NOT_AVAILABLE),
|
|
24
|
+
identify: (options) => call((c) => c.identify(options), NOT_AVAILABLE),
|
|
25
|
+
increment: (options) => call((c) => c.increment(options), NOT_AVAILABLE),
|
|
26
|
+
decrement: (options) => call((c) => c.decrement(options), NOT_AVAILABLE),
|
|
27
|
+
pageView: (options) => call((c) => c.pageView(options), NOT_AVAILABLE),
|
|
28
|
+
screenView: (name, properties) => call((c) => c.screenView(name, properties), NOT_AVAILABLE),
|
|
29
|
+
captureWebVital: (metric) => call((c) => c.captureWebVital(metric), NOT_AVAILABLE),
|
|
30
|
+
captureException: (error, properties) => call((c) => c.captureException(error, properties), NOT_AVAILABLE),
|
|
31
|
+
register: (properties) => call((c) => c.register(properties), void 0),
|
|
32
|
+
unregister: (...keys) => call((c) => c.unregister(...keys), void 0),
|
|
33
|
+
setProfileId: (id) => call((c) => c.setProfileId(id), void 0),
|
|
34
|
+
getProfileId: () => call((c) => c.getProfileId(), null),
|
|
35
|
+
getDeviceId: () => call((c) => c.disabled ? null : c.getDeviceId(), null),
|
|
36
|
+
reset: () => call((c) => c.reset(), void 0),
|
|
37
|
+
flush: () => call((c) => c.flush(), Promise.resolve(NO_FLUSH)),
|
|
38
|
+
setConsent: (category, allowed) => call((c) => c.setConsent(category, allowed), void 0),
|
|
39
|
+
getConsent: (category) => call((c) => c.getConsent(category), false),
|
|
40
|
+
setAllConsent: (allowed) => call((c) => c.setAllConsent(allowed), void 0),
|
|
41
|
+
pauseTracking: () => call((c) => c.pauseTracking(), void 0),
|
|
42
|
+
resumeTracking: () => call((c) => c.resumeTracking(), void 0)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function useReopt() {
|
|
46
|
+
const context = useContext(ReoptContext);
|
|
47
|
+
if (!context) {
|
|
48
|
+
throw new Error("useReopt must be used within a ReoptProvider");
|
|
49
|
+
}
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
52
|
+
function useReoptClient() {
|
|
53
|
+
return useReopt().client;
|
|
54
|
+
}
|
|
55
|
+
function useTrack() {
|
|
56
|
+
const { track } = useReopt();
|
|
57
|
+
return useCallback((name, properties) => track({ name, properties }), [track]);
|
|
58
|
+
}
|
|
59
|
+
function useIdentify() {
|
|
60
|
+
const { identify } = useReopt();
|
|
61
|
+
return useCallback(
|
|
62
|
+
(profileId, properties) => identify({ profileId, properties }),
|
|
63
|
+
[identify]
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
function usePageView() {
|
|
67
|
+
return useReopt().pageView;
|
|
68
|
+
}
|
|
69
|
+
function useConsent() {
|
|
70
|
+
const { setConsent, getConsent, setAllConsent } = useReopt();
|
|
71
|
+
return useMemo(() => ({ setConsent, getConsent, setAllConsent }), [setConsent, getConsent, setAllConsent]);
|
|
72
|
+
}
|
|
73
|
+
function useLatest(value) {
|
|
74
|
+
const ref = useRef(value);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
ref.current = value;
|
|
77
|
+
});
|
|
78
|
+
return ref;
|
|
79
|
+
}
|
|
80
|
+
function useTrackOnMount(name, properties) {
|
|
81
|
+
const track = useTrack();
|
|
82
|
+
const latest = useLatest({ track, properties });
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
latest.current.track(name, latest.current.properties);
|
|
85
|
+
}, [name, latest]);
|
|
86
|
+
}
|
|
87
|
+
function usePageViewOnMount(options) {
|
|
88
|
+
const pageView = usePageView();
|
|
89
|
+
const latest = useLatest({ pageView, options });
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
latest.current.pageView(latest.current.options);
|
|
92
|
+
}, [latest]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
ReoptProvider,
|
|
97
|
+
useReopt,
|
|
98
|
+
useReoptClient,
|
|
99
|
+
useTrack,
|
|
100
|
+
useIdentify,
|
|
101
|
+
usePageView,
|
|
102
|
+
useConsent,
|
|
103
|
+
useTrackOnMount,
|
|
104
|
+
usePageViewOnMount
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=chunk-47EC5ATF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/index.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport type { ReoptBootstrap } from \"@reopt-ai/data-contract/identity\";\nimport type {\n ConsentCategory,\n DecrementOptions,\n FlushResult,\n IdentifyOptions,\n IncrementOptions,\n PageViewOptions,\n QueueResult,\n TrackEventOptions,\n} from \"@reopt-ai/data-sdk-core\";\nimport type { WebVitalMetric } from \"../capture/web-vitals.js\";\nimport type { ReoptClient } from \"../client.js\";\nimport type { ReoptClientConfig } from \"../config.js\";\nimport { getOrCreateClient } from \"../registry.js\";\n\nexport interface ReoptContextValue {\n /** `null` during server rendering and before hydration. */\n client: ReoptClient | null;\n track: (options: TrackEventOptions) => QueueResult;\n identify: (options: IdentifyOptions) => QueueResult;\n increment: (options: IncrementOptions) => QueueResult;\n decrement: (options: DecrementOptions) => QueueResult;\n pageView: (options?: PageViewOptions) => QueueResult;\n screenView: (screenName: string, properties?: Record<string, unknown>) => QueueResult;\n captureWebVital: (metric: WebVitalMetric) => QueueResult;\n captureException: (error: unknown, properties?: Record<string, unknown>) => QueueResult;\n register: (properties: Record<string, unknown>) => void;\n unregister: (...keys: string[]) => void;\n setProfileId: (profileId: string | number | null) => void;\n getProfileId: () => string | number | null;\n getDeviceId: () => string | null;\n reset: () => void;\n flush: () => Promise<FlushResult>;\n setConsent: (category: ConsentCategory, allowed: boolean) => void;\n getConsent: (category: ConsentCategory) => boolean;\n setAllConsent: (allowed: boolean) => void;\n pauseTracking: () => void;\n resumeTracking: () => void;\n}\n\nconst ReoptContext = createContext<ReoptContextValue | null>(null);\n\n/** What a call returns when there is no client (server render). Never queued, never an error. */\nconst NOT_AVAILABLE: QueueResult = { eventId: \"\", queued: false, reason: \"tracking_paused\" };\nconst NO_FLUSH: FlushResult = { status: \"idle\", sent: 0, failed: 0, pending: 0 };\n\nexport interface ReoptProviderProps {\n children: ReactNode;\n /** `bootstrap` may also be passed here; the prop below wins. */\n config: Omit<ReoptClientConfig, \"bootstrap\"> & { bootstrap?: ReoptBootstrap | null };\n /** From the server SDK's `getBootstrap()`. `null` = the server knows nothing about this visitor. */\n bootstrap?: ReoptBootstrap | null;\n /**\n * Send `$pageview` when the client is created. Default `true`. Set to\n * `false` when a router-aware component owns page views (the Next entry\n * does this for you).\n */\n initialPageView?: boolean;\n}\n\n/**\n * Creates the client **during render**, not in an effect. React runs effects\n * children-first, so a child's `useEffect` (a consent banner, a\n * `useTrackOnMount`) would otherwise run before the provider's effect had\n * created anything. Creation is idempotent per write key — StrictMode's\n * double render and a remount reuse the same instance.\n *\n * The client is never closed on unmount: it is page-scoped, and a provider\n * that unmounts during a StrictMode simulation or a layout swap must not\n * tear down the queue. Call `client.close()` yourself if you need to.\n */\nexport function ReoptProvider({ children, config, bootstrap, initialPageView = true }: ReoptProviderProps) {\n const client =\n typeof window === \"undefined\"\n ? null\n : getOrCreateClient(\n { ...config, bootstrap: bootstrap !== undefined ? bootstrap : (config.bootstrap ?? null) },\n { pageview: initialPageView }\n );\n\n const value = useMemo<ReoptContextValue>(() => bindClient(client), [client]);\n return <ReoptContext.Provider value={value}>{children}</ReoptContext.Provider>;\n}\n\nfunction bindClient(client: ReoptClient | null): ReoptContextValue {\n const call = <T,>(fn: (client: ReoptClient) => T, fallback: T): T => (client ? fn(client) : fallback);\n return {\n client,\n track: (options) => call((c) => c.track(options), NOT_AVAILABLE),\n identify: (options) => call((c) => c.identify(options), NOT_AVAILABLE),\n increment: (options) => call((c) => c.increment(options), NOT_AVAILABLE),\n decrement: (options) => call((c) => c.decrement(options), NOT_AVAILABLE),\n pageView: (options) => call((c) => c.pageView(options), NOT_AVAILABLE),\n screenView: (name, properties) => call((c) => c.screenView(name, properties), NOT_AVAILABLE),\n captureWebVital: (metric) => call((c) => c.captureWebVital(metric), NOT_AVAILABLE),\n captureException: (error, properties) => call((c) => c.captureException(error, properties), NOT_AVAILABLE),\n register: (properties) => call((c) => c.register(properties), undefined),\n unregister: (...keys) => call((c) => c.unregister(...keys), undefined),\n setProfileId: (id) => call((c) => c.setProfileId(id), undefined),\n getProfileId: () => call((c) => c.getProfileId(), null),\n getDeviceId: () => call((c) => (c.disabled ? null : c.getDeviceId()), null),\n reset: () => call((c) => c.reset(), undefined),\n flush: () => call((c) => c.flush(), Promise.resolve(NO_FLUSH)),\n setConsent: (category, allowed) => call((c) => c.setConsent(category, allowed), undefined),\n getConsent: (category) => call((c) => c.getConsent(category), false),\n setAllConsent: (allowed) => call((c) => c.setAllConsent(allowed), undefined),\n pauseTracking: () => call((c) => c.pauseTracking(), undefined),\n resumeTracking: () => call((c) => c.resumeTracking(), undefined),\n };\n}\n\nexport function useReopt(): ReoptContextValue {\n const context = useContext(ReoptContext);\n if (!context) {\n throw new Error(\"useReopt must be used within a ReoptProvider\");\n }\n return context;\n}\n\n/** The underlying client, or `null` on the server. */\nexport function useReoptClient(): ReoptClient | null {\n return useReopt().client;\n}\n\nexport function useTrack() {\n const { track } = useReopt();\n return useCallback((name: string, properties?: Record<string, unknown>) => track({ name, properties }), [track]);\n}\n\nexport function useIdentify() {\n const { identify } = useReopt();\n return useCallback(\n (profileId: string | number, properties?: Record<string, unknown>) => identify({ profileId, properties }),\n [identify]\n );\n}\n\nexport function usePageView() {\n return useReopt().pageView;\n}\n\nexport function useConsent() {\n const { setConsent, getConsent, setAllConsent } = useReopt();\n return useMemo(() => ({ setConsent, getConsent, setAllConsent }), [setConsent, getConsent, setAllConsent]);\n}\n\n/**\n * The latest value of `value`, readable from an effect that must not rerun\n * when it changes. Stands in for `useEffectEvent`, which needs React 19.2\n * while this package supports 18.\n */\nfunction useLatest<T>(value: T) {\n const ref = useRef(value);\n useEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\n/** Tracks `name` once per mount (and again if `name` changes). Property changes alone do not re-track. */\nexport function useTrackOnMount(name: string, properties?: Record<string, unknown>) {\n const track = useTrack();\n const latest = useLatest({ track, properties });\n useEffect(() => {\n latest.current.track(name, latest.current.properties);\n }, [name, latest]);\n}\n\n/** Sends one `$pageview` on mount with the options current at that moment. */\nexport function usePageViewOnMount(options?: PageViewOptions) {\n const pageView = usePageView();\n const latest = useLatest({ pageView, options });\n useEffect(() => {\n latest.current.pageView(latest.current.options);\n }, [latest]);\n}\n\nexport type { ReoptBootstrap, ReoptClient, ReoptClientConfig, WebVitalMetric };\nexport type {\n ConsentCategory,\n DecrementOptions,\n FlushResult,\n IdentifyOptions,\n IncrementOptions,\n PageViewOptions,\n QueueResult,\n TrackEventOptions,\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/capture/exceptions.ts
|
|
2
|
+
var MAX_STACK_CHARS = 8e3;
|
|
3
|
+
function describeError(value, source) {
|
|
4
|
+
const path = typeof location !== "undefined" ? location.pathname : "";
|
|
5
|
+
if (value instanceof Error) {
|
|
6
|
+
return {
|
|
7
|
+
$exception_type: value.name || "Error",
|
|
8
|
+
$exception_message: value.message,
|
|
9
|
+
...value.stack ? { $exception_stack: value.stack.slice(0, MAX_STACK_CHARS) } : {},
|
|
10
|
+
$exception_source: source,
|
|
11
|
+
$exception_handled: false,
|
|
12
|
+
path
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
$exception_type: typeof value === "object" && value !== null ? "UnknownError" : typeof value,
|
|
17
|
+
$exception_message: safeString(value),
|
|
18
|
+
$exception_source: source,
|
|
19
|
+
$exception_handled: false,
|
|
20
|
+
path
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function safeString(value) {
|
|
24
|
+
try {
|
|
25
|
+
return typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function installExceptionCapture(report) {
|
|
31
|
+
if (typeof window === "undefined") return () => {
|
|
32
|
+
};
|
|
33
|
+
const onError = (event) => {
|
|
34
|
+
try {
|
|
35
|
+
report(describeError(event.error ?? new Error(event.message), "window.onerror"));
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const onRejection = (event) => {
|
|
40
|
+
try {
|
|
41
|
+
report(describeError(event.reason, "unhandledrejection"));
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
window.addEventListener("error", onError);
|
|
46
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener("error", onError);
|
|
49
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
describeError,
|
|
55
|
+
installExceptionCapture
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=chunk-4MTDZBRS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/capture/exceptions.ts"],"sourcesContent":["export type ExceptionProperties = {\n $exception_type: string;\n $exception_message: string;\n $exception_stack?: string;\n $exception_source: \"window.onerror\" | \"unhandledrejection\";\n $exception_handled: boolean;\n path: string;\n};\n\nconst MAX_STACK_CHARS = 8_000;\n\nexport function describeError(value: unknown, source: ExceptionProperties[\"$exception_source\"]): ExceptionProperties {\n const path = typeof location !== \"undefined\" ? location.pathname : \"\";\n if (value instanceof Error) {\n return {\n $exception_type: value.name || \"Error\",\n $exception_message: value.message,\n ...(value.stack ? { $exception_stack: value.stack.slice(0, MAX_STACK_CHARS) } : {}),\n $exception_source: source,\n $exception_handled: false,\n path,\n };\n }\n return {\n $exception_type: typeof value === \"object\" && value !== null ? \"UnknownError\" : typeof value,\n $exception_message: safeString(value),\n $exception_source: source,\n $exception_handled: false,\n path,\n };\n}\n\nfunction safeString(value: unknown): string {\n try {\n return typeof value === \"string\" ? value : (JSON.stringify(value) ?? String(value));\n } catch {\n return String(value);\n }\n}\n\n/**\n * Reports uncaught errors and unhandled promise rejections. Errors thrown by\n * the reporter itself are swallowed — an analytics SDK must never turn one\n * crash into two.\n */\nexport function installExceptionCapture(report: (properties: ExceptionProperties) => void): () => void {\n if (typeof window === \"undefined\") return () => {};\n const onError = (event: ErrorEvent) => {\n try {\n // Some environments deliver the message but not the Error object.\n report(describeError(event.error ?? new Error(event.message), \"window.onerror\"));\n } catch {\n // see above\n }\n };\n const onRejection = (event: PromiseRejectionEvent) => {\n try {\n report(describeError(event.reason, \"unhandledrejection\"));\n } catch {\n // see above\n }\n };\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n return () => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n };\n}\n"],"mappings":";AASA,IAAM,kBAAkB;AAEjB,SAAS,cAAc,OAAgB,QAAuE;AACnH,QAAM,OAAO,OAAO,aAAa,cAAc,SAAS,WAAW;AACnE,MAAI,iBAAiB,OAAO;AAC1B,WAAO;AAAA,MACL,iBAAiB,MAAM,QAAQ;AAAA,MAC/B,oBAAoB,MAAM;AAAA,MAC1B,GAAI,MAAM,QAAQ,EAAE,kBAAkB,MAAM,MAAM,MAAM,GAAG,eAAe,EAAE,IAAI,CAAC;AAAA,MACjF,mBAAmB;AAAA,MACnB,oBAAoB;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,iBAAiB,OAAO,UAAU,YAAY,UAAU,OAAO,iBAAiB,OAAO;AAAA,IACvF,oBAAoB,WAAW,KAAK;AAAA,IACpC,mBAAmB;AAAA,IACnB,oBAAoB;AAAA,IACpB;AAAA,EACF;AACF;AAEA,SAAS,WAAW,OAAwB;AAC1C,MAAI;AACF,WAAO,OAAO,UAAU,WAAW,QAAS,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;AAAA,EACnF,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AAOO,SAAS,wBAAwB,QAA+D;AACrG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,UAAU,CAAC,UAAsB;AACrC,QAAI;AAEF,aAAO,cAAc,MAAM,SAAS,IAAI,MAAM,MAAM,OAAO,GAAG,gBAAgB,CAAC;AAAA,IACjF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,cAAc,CAAC,UAAiC;AACpD,QAAI;AACF,aAAO,cAAc,MAAM,QAAQ,oBAAoB,CAAC;AAAA,IAC1D,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,iBAAiB,SAAS,OAAO;AACxC,SAAO,iBAAiB,sBAAsB,WAAW;AACzD,SAAO,MAAM;AACX,WAAO,oBAAoB,SAAS,OAAO;AAC3C,WAAO,oBAAoB,sBAAsB,WAAW;AAAA,EAC9D;AACF;","names":[]}
|