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? →
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.
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.
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.
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.
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.
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.
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.
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.
"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.
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.
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.
Things we deliberately don't do, and the reason for each:
Dormancy is a property. An SDK that records when asked is auditable; an SDK that records always is a liability that requires constant configuration to be safe. We picked auditable.
A pixel from us means a pixel from someone we can't audit on your behalf. Zero pings, zero telemetry, zero reads of "anonymized" usage.
You can't train on data you can't see. The architecture is the policy. There's no clause we can change later that puts us in a position to reverse this.
Different product category. We do bug reports for engineers, not conversion optimization for marketers. Adding either would dilute both.
Not because we don't want healthcare customers. BAAs require operational discipline (HHS audits, breach-notification playbooks, encryption-at-rest specifics) that we haven't built and won't pretend to have. Out of ICP, on purpose.
Each team's storage prefix is isolated. Multi-tenancy is enforced via global query scope on every model. Escape hatches are grep-able for review.
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:
/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.
For security researchers, see security.txt for our disclosure address.
No card on Free. No surprise overage. Cancel anytime.
It's wired to a sandbox project. Click it, record yourself for 30 seconds, send. You'll see the replay on the dashboard.