Features

Three steps. Ninety seconds.
Nothing captured by default.

Every other bug-reporting tool starts from "capture everything, decide later." BugJar starts from the opposite end. What follows is the workflow, then each design choice and why we made it that way. Looking for prices? →

The flow

From bug to engineer's inbox in three clicks.

1

User clicks the trigger.

Your customer hits a bug. They click the BugJar button, which is visible, never hidden. A consent panel explains what's about to be recorded: DOM replay, optional microphone, optional drawing. They choose what to capture and click Record.

2

They reproduce, narrate, draw.

Up to 90 seconds. The user re-creates the bug, talks through what they expected, circles the offending UI. Sensitive fields (passwords, card numbers, SSN patterns) are masked in the browser as the recording happens. They never reach our servers.

3

User clicks Send.

They review the redacted replay. If anything is in there they don't want to share, they cancel and start over. Once they click Send, the report uploads directly from their browser to our Cloudflare R2 storage via presigned URLs — our Laravel server never sees the bytes. Your engineer opens the dashboard and watches the replay synced to console + network + audio. Slack dispatch ships today. Linear, GitHub Issues, Discord, and a generic webhook (Zapier / n8n) are on the roadmap.

The reasoning

Each design choice exists because the alternative is a liability we didn't want to ship.

What follows is six choices, the why behind each, and the trade we accepted in exchange. If you've ever sat in a security review and watched a vendor wave a hand at "we encrypt everything," this is the version where the answer is engineering, not slogan.

01 · Why nothing records until your user asks

Data you don't collect is data you can't lose.

Always-on capture is a liability disguised as a feature. Once the SDK is recording every session of every customer, you've taken on an obligation: defend that data from breaches, configure redaction correctly across every page, respond to every subpoena, and explain in security review how a screen recording of a doctor's portal ended up on a third-party server.

The simplest way to avoid those obligations is not to take them on. The SDK is dormant until the user clicks the trigger. Until then, there's a button on the page and lightweight console + fetch wrappers that fire NOTHING into a buffer — they only start capturing entries once the user is actively recording. Nothing else.

The trade: some bug reports go missing. Users who are too frustrated, embarrassed, or uninterested to click. We accept that loss in exchange for the property that we cannot leak data we never collected. The bug reports we do receive come from people actively trying to help; their reports are richer than passive recordings would have been.

Mechanic: SDK installs an in-page button + event listeners. Capture begins at trigger-click. Voice + screen permissions are requested at that moment, never pre-emptively.

02 · Why redaction happens in your customer's browser, not on our servers

Server-side redaction means the raw data already left.

The session-replay incumbents redact server-side: the browser uploads everything, the server applies your configured rules, the redacted blob is stored. By the time redaction runs, the raw data has crossed the network, transited a CDN, and lived in memory on a server you don't control. If something goes wrong (bad config, breach, subpoena), the data is there to lose.

BugJar runs the redaction pipeline before the upload buffer is filled. The browser is the trust boundary; nothing crosses it unredacted. The same regex set that protects the upload also protects the in-memory replay the user reviews before sending.

Why this is safer, not just different: a misconfigured server-side redaction leaks silently. The raw data sits in storage, you find out later, you write a breach notification. A misconfigured client-side redaction means a worse bug report. The user re-records. We tilt the failure mode toward the recoverable kind.

Mechanic: rrweb events pass through 17 default redaction categories (credit cards, SSNs, JWTs, API keys, bearer tokens, etc.) before joining the upload buffer. Per-project regex extensions for industry patterns are on the Team-plan roadmap. Elements you mark data-bugjar-block are excluded entirely; data-bugjar-mask shows layout but masks content.

Redaction scope: Credit cards, SSNs, JWTs, API keys, auth headers, and private keys are always masked. No override exists, even for support flows. Other categories (email addresses, phone numbers, IBANs, free-form sensitive field names) are masked by default but can be relaxed on a single-use capture invitation when the end user explicitly consents. For example: a support flow where the engineer needs to confirm which email address the user is submitting the form with.

The end user has the final say. The consent panel includes a "🛡️ Hide my data" toggle (default on) that lets the person being recorded opt OUT of sharing their page text — even when the project is configured for strict masking, the visitor can turn that off if they want to send the raw page so the developer can reproduce an exact-content bug. Pattern-redaction (cards, SSNs, JWTs, etc.) stays on regardless. No competitor lets the user being recorded make that call.

02b · Your customer's HTML carries no third-party CDN reference

The SDK ships from your dashboard. Not jsDelivr. Not unpkg. Not us-as-a-CDN.

Most JS SDKs in this space deliver via a third-party CDN. That puts an extra origin in your customer's Content-Security-Policy allowlist, expands the trust boundary (CDN compromise = your customer's data is in someone else's process), and shows up on supply-chain audits as an "unaccounted external resource."

BugJar serves the SDK from your project's URL on your BugJar dashboard (/sdk/{publicKey}.js). One origin to allowlist (us, the vendor you already trust for the report data). Same origin for the API calls. Same origin for the redaction config. Same trust boundary for everything.

Mechanic: the per-project URL also inlines the project's redaction posture into the response (window.__BUGJAR_CONFIG__) so the SDK doesn't have to round-trip a settings fetch on init. One request, full config, ready to record.

03 · Why our application servers never touch the blob

The narrowest threat surface is the one that doesn't include us.

Most SaaS architectures route uploads through application servers: the browser POSTs to your API, the API streams to storage. That's a fine architecture for most workloads, and a poor one for this one. Every byte that traverses our servers is a byte we have to defend, log responsibly, and keep out of error reports.

The simpler answer: presigned URLs. The browser asks our API for a 15-minute upload ticket and uploads directly to Cloudflare R2. Our application sees the metadata (report ID, project ID, timestamp, console + network manifests) but never the blob. Breach of our application can't expose blob content because our application doesn't have it.

Why R2 specifically: Cloudflare's storage tier is battle-tested at scale by people who do nothing else. Our Laravel servers are not. Outsourcing the durable-data layer to specialists is engineering humility. We keep the parts we're good at, hand off the parts they're better at.

Mechanic: 15-minute TTL presigned PUT URLs, content-length-range bounded. Plan-limit counter increments at issuance, not at finalize, so abandoned uploads still count.

04 · Why even we can't view your recordings

The threat model has to include us, or it isn't honest.

"Trust us, our employees won't look" is not a security property. It's a reassurance. The threat model has to include accidental access during a support session, a compromised admin account, a disgruntled employee. If the design lets us view content, all of those become customer-data incidents. If the design doesn't, they become bugs in someone else's incident report.

Our admin panel has zero ability to issue presigned GET URLs for blob content. The endpoint that issues those URLs hard-rejects admin-guard sessions; the rejection is itself audit-logged, and any attempt fires an alert email to the founder.

Why customers can verify, not just trust: the audit log is customer-readable on the Team plan. We cannot redact entries from it. If an admin tries something they shouldn't, the customer sees it before we explain it. That's a property the customer can test, not a promise they have to take on faith.

Mechanic: Filament admin uses a separate Laravel guard. Customer-team sessions on the dashboard guard can issue presigned GETs scoped to their team's reports; admin sessions cannot. Contract test asserts the rejection.

04b · Close the loop with the person who reported the bug

Optional email verification — so "we shipped the fix" actually reaches them.

Anonymous bug reports are good for privacy but bad for follow-up. The most actionable bug reports come from people you can write back to — "we shipped the fix, can you try again?" That re-engagement loop is half the value of bug reports in the first place.

Toggle Require verified email from reporter on a project, and the SDK collects an email + 6-digit code on the preview screen before the report finalizes. The code is mailed by us, validated by us, then the verified email is stamped on the report. The dashboard shows an "Email verified ✓" chip next to the address, so engineers know which addresses they can actually email back.

Different from HMAC user auth: HMAC proves the identifier came from your own server (good for "this is user_id 42 in our DB"). Email verification proves the inbox is reachable (good for "this address answers email"). Use either, both, or neither depending on what the bug-report flow needs.

Available on Solo + Team. Free returns no-verified-email reports — same as today.

04c · The triage view that doesn't ask you to watch the whole replay

Play-by-play — every click, navigation, error, and request in one scrollable feed.

The dashboard's report viewer ships with a play-by-play sidebar: the captured rrweb events, console logs, and network requests merged into a single chronological feed. Click any row — clicks, navigations, inputs, HTTP errors, console errors, redactions — and the synced replay player seeks to that exact moment.

Filter chips per event kind, case-insensitive search across labels and details, "currently here" highlight as the replay plays. The shape developers were already trying to recreate by scrubbing the timeline manually — now it's the first tab you land on when you open a report.

05 · Why we ship less, on purpose

Negative space is part of the product.

Things we deliberately don't do, and the reason for each:

06 · Compliance

GDPR + CCPA controls, built in.

We don't promise compliance — that depends on how you use the product. But the controls these regimes require are first-class here: consent is the gating mechanism for every capture, no analytics cookies, no third-party pixels, no cross-customer data sharing.

Two different kinds of "your data," two different paths:

  • Team-member access: sign in and download a JSON export of your profile + team memberships + audit-log entries + projects you own from /account. Reports captured FROM end users aren't your personal data — they're the end user's — so they're not in this export by design.
  • End-user data-subject requests (Art. 15 / 17): if you submitted a bug report on a site that uses BugJar and want a copy or deletion of your submission, email [email protected]. Verified requests are honored within 30 days; the receiving team's audit log records the action against their data.
  • Customer team migration / backup: team owners who need a full export of their account (projects + configs + reports + blobs) can email [email protected] to arrange one.

For security researchers, see security.txt for our disclosure address.

Pricing

Free for solo. $9 when you ship. $39 for the team.

No card on Free. No surprise overage. Cancel anytime.

Try it on this page

The button bottom-right is a real BugJar trigger.

It's wired to a sandbox project. Click it, record yourself for 30 seconds, send. You'll see the replay on the dashboard.