ProdBeat Docs
One script from our CDN. Every framework. Session replay, heatmaps, errors, performance, and product analytics — with privacy masking built in.
Reading with an AI agent? These docs are available as markdown: /llms.txt (index) · /llms-full.txt (full content).
Welcome to ProdBeat
ProdBeat records what your users actually experience — pixel-accurate session replays with the network requests, console output, errors, and Web Vitals behind them — and turns that into heatmaps, funnels, and product analytics.
There is exactly one thing to install: a single script served from our CDN. No npm package, no build-step integration, no framework adapters to keep in sync. The same bundle works in React, Next.js, Vue, Nuxt, Angular, Svelte, Rails, Django, WordPress — anything that renders HTML in a browser.
Sensitive data is masked in the browser before anything is sent: passwords, tokens, credit cards, emails, and API secrets never leave the page. Masking is always on — there is no privacy configuration to get wrong.
Quickstart — two minutes, honestly
1. Create a project in the ProdBeat dashboard. Your SDK API key (pk_live_…) is generated with it and shown once — copy it somewhere safe.
2. Paste the snippet below before the closing </body> tag of your site. Replace pk_live_xxx with your key. That's the whole install.
3. Deploy, open your site, click around, then check Sessions in the dashboard — your first replay appears within a minute.
Paste before </body> — works on any site or framework
<script src="https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js" async></script>
<script>
window.addEventListener("load", function () {
if (window.SessionRecorder) {
window.SessionRecorder.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
}
});
</script>API keys
Your API key is the only credential the SDK needs. Your project and organization are derived from it on our servers — you never set a project id or tenant id by hand.
Keys are created with the project and shown once, at creation or rotation. After that the dashboard only shows a masked version (pk_live_••••), so store the raw key in your secrets manager or deploy config when you first see it.
Lost a key, or worried it leaked? Rotate it from Settings → Projects → your project. Rotation invalidates the old key immediately; update the snippet with the new key and redeploy.
- The key identifies your project — it is not a secret that grants dashboard access, but treat it like configuration, not something to share in public repos.
- One key per project. Use separate projects (and keys) for staging and production so test traffic never pollutes real analytics.
Script tag — any site
The plain script tag is the canonical install, and for most sites it's also the best one. It loads async (never blocks your page), initializes after the load event, and works regardless of what framework — if any — rendered the page.
Before </body>
<script src="https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js" async></script>
<script>
window.addEventListener("load", function () {
if (window.SessionRecorder) {
window.SessionRecorder.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
}
});
</script>React
Inject the script once on mount from your root component. The id guard makes the hook safe under StrictMode double-mounting and hot reloads.
useSessionRecorder.js — call once in your root component
import { useEffect } from "react";
const SDK_URL = "https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js";
export function useSessionRecorder() {
useEffect(() => {
if (document.getElementById("session-recorder-script")) return;
const s = document.createElement("script");
s.id = "session-recorder-script";
s.src = SDK_URL;
s.async = true;
s.onload = () =>
window.SessionRecorder?.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
document.body.appendChild(s);
}, []);
}Next.js
Use next/script in a client component and render it in your root layout. strategy="afterInteractive" keeps it off the critical path; the component is SSR-safe because the script only ever runs in the browser.
app/session-recorder.tsx
"use client";
import Script from "next/script";
export function SessionRecorder() {
return (
<Script
src="https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js"
strategy="afterInteractive"
onLoad={() =>
window.SessionRecorder?.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
})
}
/>
);
}
// Render <SessionRecorder /> inside <body> in app/layout.tsxVue
Append the script from your app entry. The typeof window guard makes the same file safe if you later adopt SSR.
main.ts
if (typeof window !== "undefined") {
const s = document.createElement("script");
s.src = "https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js";
s.async = true;
s.onload = () =>
window.SessionRecorder?.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
document.body.appendChild(s);
}Nuxt
Create a client-only plugin — the .client suffix keeps it out of server-side rendering entirely.
plugins/session-recorder.client.ts
export default defineNuxtPlugin(() => {
const s = document.createElement("script");
s.src = "https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js";
s.async = true;
s.onload = () =>
window.SessionRecorder?.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
document.body.appendChild(s);
});Angular
Load the script in ngOnInit of your root component, guarded by isPlatformBrowser so Angular Universal SSR never touches it.
app.component.ts
import { Component, OnInit, Inject, PLATFORM_ID } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
@Component({ /* ... */ })
export class AppComponent implements OnInit {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngOnInit() {
if (!isPlatformBrowser(this.platformId)) return;
const s = document.createElement("script");
s.src = "https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js";
s.async = true;
s.onload = () =>
(window as any).SessionRecorder?.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
document.body.appendChild(s);
}
}Svelte / SvelteKit
Load the script in onMount of your root layout — onMount only runs in the browser, so it's SSR-safe by construction.
src/routes/+layout.svelte
<script>
import { onMount } from "svelte";
onMount(() => {
const s = document.createElement("script");
s.src = "https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js";
s.async = true;
s.onload = () =>
window.SessionRecorder?.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
document.body.appendChild(s);
});
</script>
<slot />Rails, Django, Laravel, PHP — any server-rendered stack
If your backend renders HTML, you don't need a JavaScript build at all — the script tag goes in your base layout template and every page inherits it. The SDK runs entirely in the visitor's browser; your server language is irrelevant to it.
Rails: app/views/layouts/application.html.erb. Django: your base.html. Laravel: resources/views/layouts/app.blade.php. Plain PHP: your shared footer include. In every case it's the same snippet, before </body>:
In your base layout, before </body>
<script src="https://pub-60b02f367ac941aab7bfc8cec136001a.r2.dev/session-recorder.umd.js" async></script>
<script>
window.addEventListener("load", function () {
if (window.SessionRecorder) {
window.SessionRecorder.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
});
}
});
</script>WordPress & hosted CMS
Any platform that lets you add custom HTML or a footer script can run ProdBeat — WordPress, Webflow, Shopify, Wix, Squarespace, Framer.
WordPress: add the snippet with your theme's footer.php, a code-snippets plugin, or your theme's 'custom scripts' panel. Webflow/Shopify/etc.: paste it into the site-wide footer code setting. It's the same script-tag snippet from the Quickstart — nothing platform-specific.
Configuration
Everything is passed to SessionRecorder.init({ … }). Only apiKey is required. Errors, performance, forms, engagement, and gestures are captured by default; network and console capture are the two you opt into explicitly.
- apiKey (required) — your project key; project + organization are derived from it.
- enableSessionGate (recommended: true) — registers the session with our servers before recording starts.
- recordNetwork (default false) — capture fetch/XHR requests, status codes, and sanitized bodies.
- recordConsole (default false) — capture console.log/info/warn/error (sanitized).
- recordErrors (default true) — uncaught errors, promise rejections, failed resource loads.
- recordPerformance (default true) — Web Vitals (LCP/INP/CLS/FCP/TTFB), navigation timing, long tasks.
- recordForms (default true) — form focus/submit/abandon; field names only, never values.
- recordEngagement (default true) — scroll depth, visibility, resize, selection length, copy.
- recordGestures (default true) — mobile swipe/pinch/double-tap, rapid-tap rage, rage-keyboard.
- version — explicit release version (e.g. a git SHA) for deploy attribution; auto-detected if omitted.
- userId / anonymousId — attach identity at init, or call identify() after login.
- idleTimeoutMs (default 30 min) — inactivity window after which a fresh session id is minted.
A fully specified init (defaults shown where they matter)
window.SessionRecorder.init({
apiKey: "pk_live_xxx",
enableSessionGate: true,
recordNetwork: true,
recordConsole: true,
version: "v2.4.0", // optional — release attribution
// everything below is already the default:
// recordErrors: true, recordPerformance: true,
// recordForms: true, recordEngagement: true, recordGestures: true,
});track(name, props?)
Record an app-defined event at a meaningful moment — a signup completed, a checkout started, a feature used. Events power the Events explorer and funnels, and every event links back to the session replay where it happened.
Name events object_action style (test_created, plan_upgraded). Props take up to 20 keys; values are coerced to string/number/boolean and truncated at 200 characters.
Fire on success, not on click
const res = await api.signup(form);
if (res.ok) {
window.SessionRecorder.track("sign_up_completed", {
plan: res.user.plan,
referrer: "pricing_page",
});
}Safe wrapper — the SDK loads async, so guard your calls
export function trackEvent(name, props) {
try { window.SessionRecorder?.track?.(name, props); } catch {}
}identify(userId, traits?)
Tie the current anonymous visitor to a known user, right after login. Everything from that moment on — events, sessions, funnels — is attributed to your user id instead of an anonymous visitor.
Use a stable, opaque internal id rather than an email where you can: you control what the identifier reveals.
Right after login succeeds
window.SessionRecorder.identify("user_123", {
role: "teacher",
plan: "pro",
});Session replay
Every session is a pixel-accurate DOM recording — not a video — so it's small, sharp at any zoom, and searchable. Scrub the timeline and watch exactly what the user saw, alongside everything the page was doing underneath.
The replay view shows the activity timeline (clicks, navigation, rage/dead clicks, errors) next to the network and console panes, all synchronized to the playhead. When a user reports 'it broke', you watch it break — with the failing request and the stack trace on the same screen.
- Filter sessions by page, user, signal (rage clicks, dead clicks, errors), or the custom events they contain.
- Live mode: watch an in-progress session in real time — invaluable for support calls.
- Every error, funnel drop-off, and heatmap hotspot deep-links to the exact sessions behind it.
Heatmaps
Click heatmaps are rendered over a real snapshot of your page, split by device — mobile and desktop get their own maps, because they're different UIs.
ProdBeat separates clicks by what happened after them: a dead click produced no response within ~500ms ('is this broken?'), a rage click is three or more rapid clicks in the same spot (definitely frustrated). Both rank above raw click volume, and both link to the replays where they happened.
Compare mode puts two versions of a page side by side — before and after a release — and tells you what changed: which elements gained or lost clicks, where new frustration appeared, and whether the difference is statistically meaningful or just noise.
Errors
Uncaught JS errors, unhandled promise rejections, and failed resource loads (broken images, scripts) are captured automatically and grouped by fingerprint, so a thousand occurrences of one bug is one row, not a thousand.
Each error group shows its trend, affected pages and browsers, and — the part that actually helps — links to session replays where it fired. You don't reproduce the bug; you watch it happen.
Performance
Web Vitals — LCP, INP, CLS, FCP, TTFB — plus navigation timing and long tasks, captured from real users (not lab runs), with percentile summaries and trends per page.
Because vitals live in the same system as replays, a slow-LCP page isn't just a number: open the sessions behind the p90 and watch what those users actually experienced while it loaded.
Events & funnels
Everything you track() lands in the Events explorer: counts, unique users, trend sparklines, and a property breakdown — slice sign_up_completed by plan, or test_created by subject, without writing a query.
Funnels chain events into an ordered sequence and show where users leak. Order matters (step 2 counts only after step 1), the window is yours (1h to 30d), and you can count by user or by session.
The step that drops 75% of your users is a fact; why they drop is in the replays — every funnel step links to the sessions that made it and the sessions that gave up there.
Automatic PII masking
Masking runs inside the SDK, in the browser, before any data is transmitted — sensitive values never reach our servers in the first place. It's always on: there is no flag to disable it, so there's no configuration mistake that leaks data.
What gets masked, automatically: every input value (passwords never leave the field); PII rendered into the page by your APIs — credit cards, emails, phone numbers, national IDs — even in plain text; network request and response bodies (sensitive keys like password/token/apiKey, recognizable secrets like JWTs and API keys, and unknown high-entropy strings all become [REDACTED:…]); auth headers like Authorization and Cookie, which are dropped outright; secrets in URL query strings; console logs; and WebSocket messages. Canvas is never recorded at all.
Masking is surgical, not destructive: in a JSON body, {"password":"hunter2","plan":"pro"} is captured as {"password":"[REDACTED]","plan":"pro"} — you lose the secret, you keep the debugging context. In page text, only the matched characters become *, so replays stay readable.
Excluding your own sensitive regions
Pattern matching catches structured PII on its own, but it can't know that 'Jane Doe, 42 Baker Street' is a customer record. For those, add one attribute to a wrapper element — it cascades to everything inside, and needs nothing in init():
- data-ps-block / .ps-block — drop the element and all children from the recording. Use for payment widgets, signature pads, sensitive dashboards.
- data-ps-mask / .ps-mask — keep the structure, mask all text. Use for names, addresses, free-text PII.
- .ps-ignore — don't record interaction on an input.
- They're plain HTML attributes/classes — identical in React (className), Vue, Angular, Svelte, or server templates.
Three levels of exclusion
<!-- Never captured at all — replay shows a same-size placeholder -->
<div data-ps-block>
<iframe src="/payment-widget"></iframe>
</div>
<!-- Layout kept, all text inside becomes ***** -->
<div data-ps-mask>
<p>Jane Doe</p>
<p>42 Baker Street</p>
</div>
<!-- Element visible, but typing/changes on it are not recorded -->
<input class="ps-ignore" />