@reopt-ai/data-sdk-devtool
v0.3.0
Published
reopt-data SDK devtools — records what the browser SDK sends and shows it in a floating panel; off in production by default
Readme
@reopt-ai/data-sdk-devtool
A devtools panel for the reopt-data browser SDK: sanitized batches the SDK sent, the identity lifecycle the browser is tracked under, and any tabs your app adds.
Off in production by default. createDevtools() follows NODE_ENV; a production bundle carries an inert instance — the page's own fetch, an empty store, a component that renders null. Pass enabled: true to force it on for a demo or a staging deployment.
Small when inactive. No CSS import, UI library, or React context. The ESM entry is a ~0.8 KB gzip gate and the React entry is ~0.4 KB; the CommonJS compatibility entries are also guarded by explicit budgets. When disabled they keep the page's transport, render null, and request no recorder or UI chunks. When enabled, the recorder and status bar load lazily; the expanded panel loads only on the first click.
| Entry | What |
| ---------------------------------- | --------------------------------------------------------------------- |
| @reopt-ai/data-sdk-devtool | createDevtools() — a recording fetch and the store. No framework. |
| @reopt-ai/data-sdk-devtool/react | <ReoptDevtools />, useDevtoolsState() — "use client" |
Install
pnpm add -D @reopt-ai/data-sdk-devtoolPeer: @reopt-ai/data-sdk-client (and react for the panel).
Use
// lib/devtools.ts — one instance, shared by the SDK config and the panel
import { createDevtools } from "@reopt-ai/data-sdk-devtool";
export const devtools = createDevtools();
// createDevtools({ enabled: true }) to show it in production as well// the client boundary that creates the SDK client
"use client";
import { ReoptProvider } from "@reopt-ai/data-sdk-client/next";
import { ReoptDevtools } from "@reopt-ai/data-sdk-devtool/react";
import { devtools } from "@/lib/devtools";
export function Analytics({ children }) {
return (
<ReoptProvider config={{ writeKey, baseUrl: "/ingest", fetch: devtools.fetch, observe: devtools.observe }}>
{children}
<ReoptDevtools devtools={devtools} />
</ReoptProvider>
);
}fetch records the wire batch without intercepting or patching globals and reduces signed-session request/response data to a boolean. Sensitive headers, common personal fields, URLs, responses, and identity facts are redacted before they enter the store. observe adds enqueue-time lifecycle, identity, consent, tracking, and sanitized configuration facts, so an event appears immediately and advances from queued through sending to accepted, failed, or dropped. A row ingest hands back at the quota (deferred[]) returns to queued with the reason, because the SDK keeps it and sends it again once its quota hold has passed; the batch records how many came back as deferred. If the recorder chunk cannot load, fetch sends through the plain transport unrecorded and tries the chunk again on the next request — the devtool never blocks delivery. The status bar shows anonymous or signed in independently from session pending, session active, or no session, without exposing either identifier. The same pair works next to init() without a provider.
The default transport forwards the SDK's abort signal, credential policy and keepalive option in both enabled and disabled modes. Request deadlines and replay cancellation on consent withdrawal therefore still reach the browser's fetch implementation.
The Events tab uses low-contrast phase backgrounds instead of card outlines. Newly inserted rows reveal from the top with a short motion, while prefers-reduced-motion disables that animation.
Options
createDevtools({ enabled, maxBatches, maxEvents, maxTimeline, captureRaw, fetch })
enabled— defaultprocess.env.NODE_ENV !== "production".maxBatches— batches kept in memory, default 40.maxEvents— event lifecycle rows kept in memory, default 250.maxTimeline— identity, consent, and tracking rows kept in memory, default 100.captureRaw— retain exact diagnostic values, defaultfalse. Use only on a trusted local page.fetch— the transport to delegate to, default the page'sfetch.
<ReoptDevtools devtools panels title bottomOffset console expose />
panels— extra tabs:[{ id, label, render }]. The reopt-data example app uses one for its SDK option switches.bottomOffset— pixels to reserve below the tool for a host-owned footer.console—{ origin, projectId }; adds project and accepted-event links through the console's stable/open/projects/...resolver.expose— installwindow.__reoptDevtoolson mount (defaulttrue) so an end-to-end test can read the store:
const events = await page.evaluate(() => window.__reoptDevtools?.state().batches.flatMap((batch) => batch.events));Reading the store yourself
import { useDevtoolsState } from "@reopt-ai/data-sdk-devtool/react";
const { batches, totals, identity } = useDevtoolsState(devtools);The recorder redacts credentials and common personal fields before values enter memory, matching key names by word in any case style (userEmail, user_email and user-email alike, also in URL query parameters), so the UI and window.__reoptDevtools are sanitized by default. Property names the recorder does not recognize can still carry sensitive application data; keep diagnostics scoped to development, staging, or an intentionally sanitized demo. captureRaw: true is an explicit local-only escape hatch.
Replay requests appear as transport batches with status, duration and request ID. Successful replay uploads preserve the observed analytics session state. DOM events are never retained in the devtool event list. Replay upload grants and credential headers stay redacted even with captureRaw: true; the SDK still receives the original response. Capture diagnostics are not a substitute for the project's replay console.
Diagnostics use the server's error code to distinguish exhausted event or replay quotas from rate limiting; a 200 batch with deferred rows is reported as a quota warning. A session_pending response means replay is waiting for an analytics session; check analytics delivery if it persists. HTTP 409 alone does not indicate a contract mismatch. Enable replay in the project's settings when the server reports replay_disabled.
