When a bug affects only one customer account, the cause is almost always something that’s different about that account: its data, configuration, flags, plan, integrations or the customer’s environment. Bug reproduction then becomes a comparison. Find what differs from an account that works, show which difference matters, and rebuild it in a test account engineering can use.
The usual dead end is trying the steps in your own account, seeing everything work, and replying “can’t reproduce”. Your account is the control, not the test. This guide covers where account-specific bugs come from, the order to gather evidence, a worksheet, and what a good handoff contains.
If the symptom is a missing button or a 403, start with debugging customer permission issues instead. That guide covers roles, grants and entitlement checks in depth. This one covers everything else.
Why does a bug affect only one customer account?
Because the code path is shared but its inputs aren’t. Every account feeds the same code different data and settings, and one combination hits a case nobody tested. These are the usual suspects:
| Cause | What’s different about the account | Where to look |
|---|---|---|
| Data shape | A null field, a legacy record, unusual characters, a duplicate | The specific records the failing page or job reads |
| Data volume | 40,000 items where most accounts have 400 | Row counts, payload sizes, pagination and timeout logs |
| Feature flags | A rule targets this user, tenant, locale or cohort | Flag evaluation logs and the context the flag was evaluated with |
| Plan or entitlement | A legacy plan, add-on, trial or grace period | Billing record compared with the app’s derived entitlements |
| Tenant configuration | Custom fields, workflow rules, SSO or region settings | Settings diffed against a working tenant |
| Locale and timezone | Date boundaries, number formats, right-to-left text | User and tenant locale settings, server versus browser time |
| Integrations | An expired third-party token, a stuck sync, a webhook failing | Integration status, the last successful sync, error logs |
| Customer environment | Browser version, extensions, corporate proxy, blocked scripts | Browser console and network errors from the customer’s machine |
Feature flags deserve extra care. A flag’s result can depend on who is asking and when. The OpenFeature specification (opens in a new tab) describes flag evaluation context as data about the user, the application or the host, such as an email address, locale or time of day. Your admin account and the customer’s account can get different answers from the same flag.
What evidence should you collect first?
Collect the cheapest, least invasive evidence first and stop as soon as one source explains the bug. A good order:
- The exact report. Who, which account, which action, when (with timezone), what they expected and what happened. Ask for a screenshot of the error, not a description of it.
- Logs and request IDs. Find the failing request by account and timestamp. A request ID links every event from one interaction. OWASP’s Logging Cheat Sheet (opens in a new tab) calls this an “interaction identifier” and recommends recording it so related events don’t need to be reconstructed later. A stack trace with an account ID often ends the investigation here.
- A configuration diff. Compare the affected account with a working account on the same plan and region. More on this below.
- Replay and browser diagnostics, if you have them. A recording of the session can show what happened on screen. It can’t show server-side state, so read when session replay isn’t enough before relying on it alone.
- A synthetic reproduction. Build a test account that copies the differences you found.
- Approved access to the current account. Only when the earlier steps leave a gap that only the live account can fill.
Each step is a cheaper experiment than the next, and each one narrows the search. Skipping straight to step 6 is tempting. It’s also the step most likely to change things, expose unrelated customer data, or show you a state that no longer matches the report.
How do you compare the account with one that works?
Pick a control account that should behave the same: same plan, same region, similar size, ideally one that uses the same feature successfully. Then diff the inputs, not the screens.
Export both accounts’ settings, flag evaluations for the relevant flags, entitlements and integration states as structured data, and compare them side by side. Add counts and shapes for the records involved: how many items, the oldest record’s date, and fields that are null in one account and not the other.
Here’s a fictional example. Customer Larch reports that their invoices page shows a blank screen. Your control account loads it fine. The logs show a TypeError while rendering one invoice. The diff shows Larch has one invoice, imported years ago, with no due date, while every invoice in the control account has one. That’s a data-shape hypothesis you can test without opening Larch’s account at all.
Keep the diff out of the ticket if it contains customer data. Store it where access is controlled, and put the request IDs and field names in the ticket instead.
How do you reproduce the bug without the customer’s account?
Rebuild the difference in a test account, using synthetic data that copies the shape but not the content. In the Larch example, create a test tenant with one invoice whose due date is null and load the page. If it fails the same way, you have a reproduction that engineering can run as often as they like.
Change one input at a time. If you copy a large dataset and three settings at once and the bug appears, you still don’t know which input caused it. Mozilla’s bug-writing guidelines (opens in a new tab) call precise steps to reproduce “the most important part of any bug report”. The same guidelines ask reporters to say whether a bug reproduces in a new browser profile and, if not, which settings or extensions it needs. Ask customers the same thing: have them try a private window or a clean profile with extensions turned off before you go looking for account causes.
Then prove the cause works in both directions. These are pass conditions to check, not results we’ve run.
| Check | Pass condition |
|---|---|
| Test account with the suspected difference | Fails the same way as the customer’s account |
| Same test account without the difference | Works |
| Control account after you add the difference | Starts failing |
| Fix applied in staging, difference still present | Works, and the original error is gone from logs |
| Other accounts with the same difference | Found by a query, so you know the true blast radius |
The last check matters. “Only one customer” often means “only one customer who reported it”.
When should you look at the customer’s live account?
When the evidence points at something you can’t copy: a record you aren’t allowed to export, an integration’s live state, or a configuration combination you can’t reconstruct. At that point a short, approved look at the account is the right tool. Without one, you’re guessing.
Keep it narrow. Write down the one question the session must answer, use read-only access where possible, and end it when you have the answer. Don’t change the customer’s role or settings to “see if it helps”, because that destroys the state you’re investigating. Remember that what you see is the account as it is now. It won’t recreate last week’s flag rollout or the customer’s laptop.
Devora handles this step. A team member picks the customer and requests a scope, duration and reason (opens in a new tab). An approver or admin in your organization reviews the request, and Devora keeps the requests, approvals and session records for audit. Read-only scope is enforced by a backend guard on your API routes (opens in a new tab), not only in the UI. Recording is optional (opens in a new tab) and off by default. Devora doesn’t find root causes for you. It controls the access step at the end of the investigation.
Use a worksheet for each investigation
One worksheet per ticket keeps the investigation honest. Leave a field blank rather than filling it with a guess.
| Field | What to enter |
|---|---|
| Report | Account, user, action, time with timezone, expected and actual result |
| Request evidence | Request IDs, status codes, error messages, log links |
| Control account | Which account and why it’s comparable |
| Differences found | Data shape and volume, flags, plan, configuration, integrations, locale, environment |
| Hypothesis | The one difference you think causes the bug |
| Synthetic reproduction | Test account or fixture, steps, result |
| Live-account access | Not needed, or the approved question, scope and outcome |
| Blast radius | Other accounts with the same difference |
| Status | Confirmed, hypothesis, or not tested |
What should a bug reproduction handoff include?
A reproducible case, not a story. Engineering should be able to run your steps against your fixture and see the failure without contacting the customer.
Include the completed worksheet, the exact steps, the test account or fixture, and the expected versus actual result. Name the difference from the control account and the evidence for it. Say plainly what’s confirmed, what’s a hypothesis and what you didn’t test. Link logs by request ID instead of pasting customer records, tokens or screenshots full of personal data.
If the worksheet still has a gap that only the live account can fill, that’s when controlled access earns its place. To see how an approved, read-only session fits your stack, create a Devora account and try one staging session.
Sources
- OpenFeature specification: Evaluation context (opens in a new tab)
- OWASP Logging Cheat Sheet (opens in a new tab)
- Mozilla: Bug writing guidelines (opens in a new tab)
- Devora: Overview for teams (opens in a new tab)
- Devora: Scope and security (opens in a new tab)
- Devora: Recording privacy & activity logs (opens in a new tab)