Example · reference instrumentation
How SpeechLab instrumented
translate-ui-react in 80 lines.
The waveform editor you see when you translate audio at speechlab.ai is a Next.js + Zustand app with react-rnd drag handles, segment collision detection, and a frozen-clock playhead. Branch B (PR #1995 in the public repo) shipped a 6-layer test harness against 8 historical reopen-treadmill bugs.
CUIT instrumentation added one useEffect and one bridge file. Everything you see below is open source — fork it.
What we capture — and what we don't
The recorder bridge taps into translate-ui-react's existing window.__waveformDebug API — a debug surface Branch B shipped for the Playwright suite. It captures only what spec-gen consumes; no DOM mutation logs, no screenshots, no network traffic.
Captured
- Pointer events (down / move / up)
- Semantic selectors (
data-segment-id,data-testid) - State snapshots from
__waveformDebug.getState() - Drag/resize calls via
dispatchDrag/dispatchResize - Nav events (URL changes with the gate)
- Git SHA + branch (for replay-against-history)
Not captured
- Video frames or screenshots
- Console output
- Network traffic / API responses
- Full DOM mutation logs
- Keyboard or wheel events (v0.1)
- Production traffic — gated by
?cuitRecorder=1
The integration — every line, in order
This is the diff that lights up CUIT in translate-ui-react. Two files, ~80 lines total. Adapt to your framework by swapping the debug-API import.
Step 1 — drop in the bridge file
Available at @cuit/recorder/translate-ui-react-bridge. Re-declares the __waveformDebug shape locally so it has no compile-time dependency on translate-ui-react source.
tsx
// proof-of-concept/packages/recorder/src/translate-ui-react-bridge.ts (excerpt)
export function mountTranslateUiReactBridge(opts: {
apiUrl: string; // https://cuit-saas-pilot.fly.dev
tenantToken: string; // cuit_tk_...
sessionId: string; // `tur-${Date.now()}`
gitSha?: string;
gitBranch?: string;
onUpload?: (info: { events: number; serverId: string }) => void;
onError?: (err: Error) => void;
}): BridgeHandle {
// Poll for __waveformDebug to mount; once present, wrap its dispatch
// methods so existing Branch B Playwright runs ALSO feed the recorder.
const recorder = new Recorder({
sessionId: opts.sessionId,
vendor: 'cuit',
snapshotProvider: () => window.__waveformDebug?.getState() ?? null,
semanticSelectors: ['data-segment-id', 'data-testid', 'data-cuit-id', 'data-cy'],
});
recorder.start();
// ... wrap dispatchDrag / dispatchResize, capture snapshots, POST on stop ...
return { stop, size, peek };
}Step 2 — mount it from your root component
In our case, alongside the existing mountDebugAPI useEffect in WaveFormDub.tsx. Two gates: NODE_ENV !== 'production' AND ?cuitRecorder=1 in the URL. The dynamic import keeps the production bundle untouched — tree-shaken to zero.
tsx
// src/components/domain/WaveFormSegment/WaveFormDub.tsx
useEffect(() => {
if (process.env.NODE_ENV === 'production') return;
if (typeof window === 'undefined') return;
const url = new URL(window.location.href);
if (!url.searchParams.has('cuitRecorder')) return;
let cancelled = false;
let handle: { stop: () => Promise<string | null> } | null = null;
(async () => {
const mod = await import('@cuit/recorder/translate-ui-react-bridge');
if (cancelled) return;
handle = mod.mountTranslateUiReactBridge({
apiUrl: process.env.NEXT_PUBLIC_CUIT_API_URL ?? 'https://cuit-saas-pilot.fly.dev',
tenantToken: process.env.NEXT_PUBLIC_CUIT_TOKEN!,
sessionId: `tur-${Date.now()}`,
gitSha: process.env.NEXT_PUBLIC_GIT_SHA,
gitBranch: process.env.NEXT_PUBLIC_GIT_BRANCH,
onUpload: (info) => console.log('[cuit] uploaded', info),
onError: (err) => console.error('[cuit] error', err),
});
})();
return () => { cancelled = true; handle?.stop?.(); };
}, []);Step 3 — env vars + dev workflow
No bundler config to change. Just env vars for token + git context.
bash
# .env.local (gitignored)
NEXT_PUBLIC_CUIT_API_URL=https://cuit-saas-pilot.fly.dev
NEXT_PUBLIC_CUIT_TOKEN=cuit_tk_...
NEXT_PUBLIC_GIT_SHA=$(git rev-parse --short HEAD)
NEXT_PUBLIC_GIT_BRANCH=$(git branch --show-current)
# Then
pnpm dev
open "http://localhost:3000/projects/abc-123?cuitRecorder=1"
# Use the app normally. Sessions stream to the warehouse on close.Why __waveformDebug existed before CUIT did
Branch B (the 6-layer harness that became CUIT's core) shipped a debug surface for translate-ui-react's Playwright tests in May 2026. The CUIT recorder taps that same surface — meaning the integration is just the bridge file, not a refactor.
| Bug | Reopen count before | After CUIT spec lock-in |
|---|---|---|
| #1931 playhead defaults to first segment | 3 reopens | locked |
| #1921 resize handle flicker | 2 reopens | locked |
| #1927 seekTo frame offset | 2 reopens | locked |
| #1956 WaveSurfer instance leak | 1 reopen | locked |
| #1933 playhead clock drift under setClock | 2 reopens | locked |
| #1960 CSS z-index regression | 1 reopen | locked |
| #1964 touch dispatch on scroll-locked | 2 reopens | locked |
| #1833/#1952 drag-after-resize playhead | 4 reopens | locked |
Adapt to your framework
The bridge has three swappable pieces. Map them onto whatever you have:
Snapshot provider
SpeechLab: window.__waveformDebug.getState()
You: expose window.__cuitDebug.getState returning a flat record of your interesting state.
Dispatch wrappers
SpeechLab: dispatchDrag/Resize on react-rnd
You: not required — recorder captures pointer events directly. Wrappers help when your tests fire synthetic events.
Semantic selectors
SpeechLab: data-segment-id on each segment
You: any stable identifier you already have. data-testid, data-cy, aria-label all work. Recorder tries them in priority order.
Fork the SpeechLab pattern
The bridge file is 264 lines of TypeScript. The mount is 22 lines in WaveFormDub.tsx. Take them as your reference; we'll have a per-framework templated CLI shortly.