Spectodocs

Documentation

Record your first session in a few minutes, then make Specto fit your stack, your consent banner and your security policy.

Quickstart

1. Create a project (or use your workspace's default). Copy its publishable key from Settings → Install.

2. Add the snippet to your site's <head>:

<script src="https://specto.sh/specto.js"
        data-key="pk_your_key" defer></script>

…or, for bundler apps (React, Vue, Svelte, …), install the npm package instead — see npm package below.

3. In Settings → General, add your site's origin to allowed origins — requests from other origins are rejected.

4. Load your site. The session appears in the dashboard within seconds (Settings → Install shows live status).

Runnable examples for React, Vue, Astro, and plain HTML — script-tag and npm-package (@spectosh/browser) integrations — live in the repository's examples/ folder.

SDK reference

Script-tag attributes

data-key           required   your publishable key (pk_…)
data-privacy       optional   "strict" masks ALL text and URL query strings
data-consent       optional   "required" records nothing until Specto.consent(true)
data-sample-rate   optional   0–1, fraction of sessions recorded (e.g. "0.25")
data-console       optional   "false" skips console capture
data-details       optional   "false" skips network request details
data-feedback      optional   "true" adds a floating feedback button
data-host          optional   where the project lives, if not this script's host

Project settings in the dashboard (Settings → Privacy) apply on top of these: the SDK fetches them at start and the stricter value always wins, so the dashboard can pause recording, force strict privacy or lower the sample rate without a redeploy. Ingest enforces the same settings server-side.

Pinned versions (SRI)

/specto.js is the evergreen channel (5-minute cache) — it picks up fixes automatically. To pin, load an exact version: its URL is immutable and never changes under its integrity hash. Upgrade by changing the version.

<script src="https://specto.sh/sdk/0.2.5/specto.js"
        integrity="sha384-uur2zoQMYHlot7rdUWfdtjkRlAsY+1fnP3P5J8WNtF/q1RHuj0nldwj5A/gNWjWt"
        crossorigin="anonymous"
        data-key="pk_your_key" defer></script>

JavaScript API (script tag)

Specto.identify('user_42', { plan: 'pro' })   // attach your user id + flat traits
Specto.track('checkout_started', { items: 3 }) // custom event on the timeline
Specto.consent(true)    // consent mode: start recording after the visitor agreed
Specto.consent(false)   // withdraw: stop and forget the consent
Specto.optOut()         // stop and never record this browser again
Specto.optIn()          // undo optOut()
Specto.sessionUrl()     // link to this session's replay (e.g. for support tickets)
Specto.stop()           // stop capturing + restore patched APIs
Specto.captureError(error) // report an error your app caught itself
Specto.init({ key, privacy, sampleRate, consent, feedback })  // manual start

Identify with an opaque id (not an email) where you can — traits are visible to everyone in your workspace and searchable in Sessions.

npm package (bundler apps)

Same engine as the script tag, installed from npm — for React, Vue, Svelte, or any bundler. The key is all it needs — pk_…@host for a project in another region or on a self-hosted instance — and React apps get @spectosh/browser/react (SpectoProvider, error boundaries, hooks). Start it before your app renders:

npm install @spectosh/browser
import { startRecording, identify, track, grantConsent, optOut } from '@spectosh/browser'

const rec = startRecording({
  key: 'pk_your_key',
  privacy: 'strict',        // optional: mask ALL text, not just inputs
  sampleRate: 0.5,          // optional: record half of sessions
  consent: 'required',      // optional: wait for grantConsent()
  networkDetails: false,    // optional: skip request details
})

identify('user_42', { plan: 'pro' })
track('checkout_started', { items: 3 })
rec.sessionId   // current session id
rec.flushNow()  // force an upload now
rec.stop()      // stop + restore patched APIs (StrictMode-safe)

Start it once for the app's lifetime (root effect / before mount). Framework-specific wiring — a React hook, a Vue plugin, an Astro layout — lives in the repository's examples/ folder, and Settings → Install shows these snippets with your project's real key filled in.

Sessions and what is captured

A session is one visitor's continuous activity: it continues across page loads in the same tab and ends after 30 minutes without interaction (or after 4 hours). Sessions are what plans meter.

Captured: DOM snapshots + mutations (rrweb, with a fresh checkpoint every 5 minutes), console output, network request metadata (method, URL, status, timing, sizes, a short allowlist of response headers), JavaScript errors, rage clicks, SPA navigations, click positions and scroll depth (for heatmaps — a position, the viewport width and a tag/id/class selector, never text), and your identify/track calls. Request/response bodies, cookies and authorization headers are never captured. Batches upload every few seconds from a Web Worker with retry and backoff; the tail is delivered with sendBeacon when the page is hidden.

Finding sessions: Sessions → + Filter narrows the list by a page visited (the entry page or any navigation), a custom event from track(), an identify trait (plan=pro), a JavaScript error message, a request status (5xx, 4xx, failed or a code) or identified/anonymous visitors. Filters combine, and a set of them saves as a segment everyone on the project can apply.

Heatmaps: Analytics → Heatmaps shows, per page and device class, where visitors click — drawn on a recorded snapshot of the page — the most clicked elements, and how far down they scroll.

Privacy controls

• All form inputs are masked in the visitor's browser — masked values never leave the page.

• Sensitive attributes (data-*, title, alt, placeholder, aria-label), URL credentials and PII-looking text (emails, card numbers, tokens) are scrubbed before upload.

• class="specto-mask" masks an element's text; class="specto-block" drops the element entirely.

• data-privacy="strict" (or the project setting) masks all text on the page, drops console text and keeps only the path of recorded URLs — requests, navigations and the page address lose their query strings.

• Consent mode (data-consent="required") stores and records nothing until Specto.consent(true); Specto.optOut() gives visitors a permanent off switch.

• Do-Not-Track and Global Privacy Control are honored — recording is skipped for those visitors.

• The SDK sets no cookies: sessionStorage carries the session across page loads, localStorage holds a random visitor id and the consent/opt-out choice.

Alerts & webhooks

Issues groups JavaScript errors by message and the code that threw them. When a project hits an error it has never produced before, or a resolved issue comes back in a session that started after it was resolved, Specto emails the project's developers and admins one digest — by default at most every 15 minutes per project — and sends a webhook event per issue. Admins can turn alerts off for a project and anyone can opt out of the emails (Settings → Alerts). Ignored issues never alert.

Signal rules (Settings → Alerts) watch behaviour against the past week: an error spike or rage-click surge (the share of sessions in the last hour that hit a JavaScript error or a rage click) and a funnel drop (conversion through your Analytics funnel over the last 24 hours), each with a low, medium or high sensitivity. A rule alerts once when it trips and posts again when things are back to normal. Connect Slack under Settings → Integrations (an incoming-webhook URL) to get issue digests and signal alerts in a channel.

Weekly digest: every Monday morning, each member of a project gets last week's sessions (against the week before), how many hit an error, feedback, new issues and regressions and the pages with the most friction — with a short written summary when the instance has an LLM. Settings → Alerts turns it off for you and previews it.

Similar feedback: the Feedback Inbox's Similar tab groups open reports that describe the same problem, and a report shows its similar reports — matched on their words, so it works without an LLM. A session's AI summary is generated once and kept; issues filed from the replay include it.

Issue trackers: connect GitHub Issues, Linear, Jira or Symbiome in Settings → Integrations, then Send to… on a feedback item or a replay files it as an issue — title, the signals, the browser and a link back to the replay — once per tracker; the issue's key shows as a link afterwards. Share to Slack posts it to the project's channel with a button back to the replay.

Webhooks (Settings → Integrations) are POSTed as JSON to your endpoint. Events: feedback.created, issue.new, issue.regressed, signal.alert, signal.resolved and webhook.test (the "Send test event" button). Answer with a 2xx within 10 seconds; anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (a Retry-After header is honored), and project admins get an email if an event still fails. Every attempt goes to the URL configured at that moment. Delivery is at least once: the body's id (also in x-specto-delivery) stays the same across retries and redeliveries, so skip ids you have processed. Headers also carry x-specto-event and x-specto-attempt.

With a signing secret set, verify x-specto-signature: sha256=<hex> — an HMAC-SHA256 of the raw body:

// { "id": "whd_…", "event": "issue.new", "ts": "2026-10-01T12:00:00.000Z",
//   "data": { "project": { "key": "…", "name": "Shop" },
//             "issue": { "id": "d963b245a2c7", "title": "TypeError: Cannot read properties of undefined (reading '…')",
//                        "frame": "render (app.js)", "firstSeenAt": "…", "sessionId": "rec_…", "url": "…/#issues/…" } } }
import { createHmac, timingSafeEqual } from 'node:crypto'
const expected = 'sha256=' + createHmac('sha256', process.env.SPECTO_WEBHOOK_SECRET).update(rawBody).digest('hex')
const valid = header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected))

Self-hosting

Specto is open-core: the same build runs the hosted service and your own instance. Run it with Docker Compose on a single box, or on Kubernetes with the manifests in deploy/k8s/ (app + ingest deployments, Postgres, Redis, network policies, autoscaling, offsite backups); the repository's deploy/ folder has the runbooks, including a Cloudflare Tunnel setup. Probes: GET /api/live (liveness) and GET /api/health (readiness, checks the database).

Required: BETTER_AUTH_SECRET, BETTER_AUTH_URL, DATABASE_URL and an email transport (SMTP_URL + EMAIL_FROM). The server refuses to start in production with an unsafe configuration. Without billing (BILLING_PROVIDER=none) new organizations get unlimited usage. Every variable is documented in .env.example.