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.

Next.js 16React 19Zustandreact-rndNODE_ENV-gated?cuitRecorder=1

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.

BugReopen count beforeAfter CUIT spec lock-in
#1931 playhead defaults to first segment3 reopenslocked
#1921 resize handle flicker2 reopenslocked
#1927 seekTo frame offset2 reopenslocked
#1956 WaveSurfer instance leak1 reopenlocked
#1933 playhead clock drift under setClock2 reopenslocked
#1960 CSS z-index regression1 reopenlocked
#1964 touch dispatch on scroll-locked2 reopenslocked
#1833/#1952 drag-after-resize playhead4 reopenslocked

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.