Widget

A single IIFE script tag. Mounts in a shadow DOM so it can't affect your host page's CSS. Captures the host page, opens a recording panel, ships everything to /v1/feedback.

Install snippet

<script>
  (function () {
    var s = document.createElement('script');
    s.src = 'https://cdn.earshotbot.com/v1/earshot.iife.js';
    s.async = true;
    s.onload = function () {
      window.Earshot.init({
        projectId: 'prj_xxxxxxxxxxxx',
        apiKey: 'pk_xxxxxxxxxxxxxxxx',
      });
    };
    document.head.appendChild(s);
  })();
</script>

The wrapper (function(){ ... })() is intentional — it's one self-contained script element. Inline <script> tags inside framework heads (Next.js app/layout.tsx, SvelteKit, Nuxt) don't reliably preserve order relative to external <script src> tags, which is why the simpler two-tag form race-conditions in production builds.

Earshot.init(options)

Option Type Default Notes
projectId string required The prj_* id shown on your project settings page.
apiKey string required The pk_* public key. Safe to embed in client JS.
apiUrl string https://app.earshotbot.com Override for self-hosted or staging.
position `'bottom-right' 'bottom-left' 'top-right'
primaryColor string #09090b Hex color for the launcher and primary buttons.

Earshot.identify(identity)

Call after your auth flow loads the user. Idempotent — safe to call on every render.

Earshot.identify({
  userId: user.id,           // required, your stable user id
  email: user.email ?? null,
  name: user.name ?? null,
  traits: {                   // optional, any string/number/boolean
    plan: user.plan,
    signedUpAt: user.createdAt,
  },
});

Earshot backfills any feedback submitted earlier in the same session that lacked identity. The linked Linear issue and the submitter email both pick up the new identity automatically.

Earshot.reset()

Call on logout. Rotates the session cookie, drops the cached identity, and the next submission is anonymous again until the next identify().

Earshot.open()

Programmatic open. Useful for a custom "Send feedback" menu item:

<button onClick={() => Earshot.open()}>Send feedback</button>

Capture model

When the user clicks the launcher, the widget snapshots the host page before the panel paints:

  • A PNG screenshot via html2canvas (skips the Earshot root + any data-earshot-block element).
  • An rrweb full-snapshot of the DOM (with earshot-block and earshot-mask classes honored).
  • The last 50 lines of console output (level, message, stack, timestamp).
  • The last 10 network requests (URL, method, status, duration).
  • Browser, OS, device, viewport, language, timezone, device pixel ratio.
  • The current URL, document title, referrer, and any data-* custom attributes you wired.

When the user stops recording, we POST audio + payload to /v1/feedback once. The server returns 202 and the widget shows "Filed." immediately; transcription + LLM enrichment + ingest happen in a background worker. There are no follow-up rounds.

Privacy

  • The widget never reads cross-origin pages.
  • Add class="earshot-block" to any element you want hidden from the DOM snapshot.
  • Add class="earshot-mask" to mask text (keeps the element, replaces text with *).
  • Inputs are masked by default (rrweb's maskAllInputs: true).

For more, see Security.

Source

The widget is open source. Find it under apps/widget/ in the repo. Vite library build, IIFE format, terser-minified, 344 KB / 94 KB gzip.