Back to Docs

Narrated Feedback

Let people record themselves using your app and talking through what they see. Simple Product works out what "this button" meant and files it as feedback your team, and your coding agent, can act on.

How it works

  1. The person clicks record, uses your app, and talks. We record the microphone only, never the screen.
  2. Alongside the audio, the SDK keeps a timeline of what the cursor pointed at: the element's text, where it sits on the page, and a few styles like font size and padding.
  3. When they stop, they can add a written note, then send or discard.
  4. We transcribe the audio and delete it, match each "this" and "that" to what the cursor was on, and file one feedback doc linked to the identified user: a section per issue they raised, with their words and the elements it's about, then the full narration.

Requires @simple-product/sdk 0.2.0 or later and a publishable key (pk_ws_*) from Settings → API Keys. See Feedback Collection for the other ways to collect feedback.

Use our UI

One call shows a small "Recording" bar with Stop and Cancel. After Stop, a review card offers an optional note, then Send or Discard.

import SimpleProduct from '@simple-product/sdk';

const sp = new SimpleProduct({ apiKey: 'pk_ws_...' });
sp.identify({ userId: user.id, email: user.email });

const { sent } = await sp.feedback.record();

In React:

import { useRecordFeedback } from '@simple-product/sdk/react';

function FeedbackButton() {
  const record = useRecordFeedback();
  if (!record) return null; // outside SimpleProductProvider
  return <button onClick={() => record()}>Record feedback</button>;
}

Restyle it

The default look is neutral and follows light and dark mode. Every color and radius is a CSS variable on the widget:

[data-sp-feedback] {
  --sp-bg: #0b0b0c;          /* surface */
  --sp-fg: #fafafa;          /* text */
  --sp-muted: #a1a1aa;       /* secondary text */
  --sp-border: #27272a;
  --sp-subtle: #18181b;      /* hover backgrounds */
  --sp-accent: #4f46e5;      /* Stop / Send */
  --sp-accent-fg: #ffffff;
  --sp-live: #ef4444;        /* recording dot, errors */
  --sp-radius: 4px;          /* the bar and card */
  --sp-radius-inner: 2px;    /* buttons and note box */
}

To reshape a piece, use ::part() with panel, dot, time, hint, stop, cancel, note, send, or discard:

[data-sp-feedback]::part(panel) {
  left: auto; right: 24px; transform: none;
  font-family: Inter, sans-serif;
}

Build your own UI

The recorder works without any UI of ours. You get the controls and its state, and draw whatever fits your app.

const rec = sp.feedback.recorder();
rec.subscribe(({ status, elapsedMs, error }) => render(status, elapsedMs, error));

await rec.start();              // asks for the mic, starts recording
rec.stop();                     // stops; nothing is sent yet
await rec.send({ note: '…' });  // or rec.discard()

status moves idle → requesting-mic → recording → stopped → sending → sent, or to error with error set to:

  • unsupported: this browser or webview can't record audio
  • mic-blocked: the person, the browser, or the OS refused the mic
  • busy: another recording is already running on the page
  • send-failed: the upload failed; the recording is kept, so call send() again

In React:

import { useFeedbackRecorder } from '@simple-product/sdk/react';

function RecordButton() {
  const rec = useFeedbackRecorder();
  if (!rec.available) return null;
  if (rec.status === 'recording')
    return <button data-sp-ignore onClick={rec.stop}>Stop ({Math.round(rec.elapsedMs / 1000)}s)</button>;
  if (rec.status === 'stopped')
    return <button data-sp-ignore onClick={() => rec.send()}>Send feedback</button>;
  return <button data-sp-ignore onClick={rec.start}>Record feedback</button>;
}

Put data-sp-ignore on your own controls so clicking Record and Stop doesn't end up in the timeline.

What's captured and kept

  • Audio stays in the browser's memory until they send. Discarding it means it never leaves their machine. Once sent, it's transcribed (the transcription provider doesn't keep it or train on it) and deleted as soon as the transcript is saved.
  • Never the screen. The timeline records elements the cursor rested on or clicked, text selections, scrolls, and page changes. Form field values are never captured.
  • data-sp-mask on an element keeps its text out entirely. Use it for anything sensitive.
  • Emails and long numbers are scrubbed from the transcript and note before anything is filed.

Limits and usage

  • Recordings stop at 10 minutes. Pass { maxSeconds } to stop sooner; longer values are capped.
  • Each workspace can send 20 recordings an hour. Past that the API answers 429.
  • Transcription and analysis count toward your workspace's credits, like other AI features. A one-minute recording is typically a handful of credits.

Desktop apps (Tauri, Electron)

The SDK is plain DOM, so it runs in any webview. The one extra step is microphone permission, which the operating system controls:

  • macOS: add NSMicrophoneUsageDescription to your app's Info.plist. If the app is sandboxed or uses the hardened runtime (typical for notarized apps), also add the com.apple.security.device.audio-input entitlement.
  • Tauri: the webview has to pass mic requests through to the page. Recent versions do. Check with a bare navigator.mediaDevices.getUserMedia({ audio: true }) first: if that works, the recorder will.
  • Electron: on macOS, call systemPreferences.askForMediaAccess('microphone') from the main process, and allow the media permission in your session's permission handler.

If start() goes straight to mic-blocked, it's almost always one of these. WebKit-based webviews (Safari, Tauri on macOS) record audio/mp4. That's handled for you.