How-to

How to Reproduce Bugs From Vague Support Tickets (Without Pinging the User Five Times)

May 14, 2026 · 11 min read

It's Tuesday. You have a PR to review, a deploy to babysit, and three Slack threads going. Then a ticket lands in your queue:

"Hey, the export button isn't working. Can you fix it? Thanks!"

— Sarah, sent from iPhone

That's it. No browser. No screenshot. No timestamp. No mention of which export. You have four exports in the product.

You could ping Sarah. You could ask the five questions you always end up asking. But Sarah's in a different timezone and won't reply for six hours. By then you'll have context-switched into something else and lost the thread. So you do what most engineers do: you guess. You open the app, click around, hope it breaks, and when it doesn't, you mark the ticket "needs more info" and move on.

That's not reproduction. That's avoidance.

This post is the playbook for actually reproducing bugs from tickets like Sarah's — without bouncing five emails back and forth, and without giving up after twenty minutes of clicking. We'll walk through a real method, with real commands, and then talk about when to stop doing this by hand.

The bug report you can't act on

Here's a ticket pulled out of a real support inbox (anonymized, but only barely):

Subject: broken

Tried to invite my teammate this morning and it says "something went wrong" but doesn't say what. I'm on the Pro plan so I should be able to add people. This is really frustrating because we have a client demo today. Please fix asap.

— Marcus

You read that and you immediately have questions. Which "invite" — the workspace invite or the project-level invite? What email did Marcus try? Was the teammate already a user in another workspace? Did the error appear on submit, or after a delay? Was it a 500, a 422, a network timeout? Pro plan as in your "Pro" tier or the legacy "Pro" tier you renamed last quarter?

None of this is in the ticket. And asking Marcus all of it makes you look like you don't know your own product. So you have to fill in the gaps yourself.

The good news: most "vague" tickets aren't actually vague. They're just missing the same five pieces every time. Once you know what those five pieces are, you can extract them yourself, or know exactly what to ask for in one (not five) follow-up.

The five missing pieces in every vague ticket

Almost every irreproducible bug fails on one of these axes. If you can fill them in, you can almost always reproduce.

Environment. OS, browser, browser version, device type, screen size, network conditions. A bug in Safari 17 on an iPad with a slow 4G connection is a different bug than the one in Chrome 121 on a 27-inch monitor on office wifi.

Auth state. Logged in or out. Which user. Which role. Which workspace. Whether their session is fresh or stale. Whether they're impersonating someone. Whether their token has the right scopes.

Data state. What's in their account. How many records. What plan tier. What feature flags are enabled. Whether they've hit a quota. Whether they have one of the seventeen weird historical data shapes you've accumulated since launch.

Sequence. The exact clicks, in order. Not "I tried to invite someone" — "I clicked Settings, then Team, then Invite, then typed an email, then clicked Send." Most bugs require a specific path. Many require a specific path and a specific state from a previous session.

Expected vs. actual. What the user thought would happen. What actually happened. These are not always the same thing as the bug. Sometimes the actual behavior is correct and the user's mental model is wrong — that's a docs bug, not a code bug, and you handle it differently.

Now, the playbook.

A manual playbook for systematic reproduction

Step 1: Parse the ticket for facts

Before you touch the app, read the ticket twice and split it into three columns: explicit, implicit, missing.

Take Marcus's ticket again. Here's what you get:

Explicit:

  • Action: invite a teammate
  • Error message: "something went wrong"
  • Plan: Pro
  • Time: this morning

Implicit:

  • Sent from email, so probably desktop (most "from iPhone" footers are explicit)
  • Has at least one teammate already, otherwise the framing is weird
  • Probably hit submit (he saw an error message, not a missing button)
  • The error is generic enough that it's probably your fallback error toast, not a specific validation error

Missing:

  • Browser, OS, viewport
  • Email they typed
  • Whether the teammate already exists in another workspace
  • Network tab response
  • Whether this is the workspace invite flow or the project invite flow
  • Their workspace ID and user ID

Notice that "implicit" did real work. You went from "I don't know anything" to "I have a decent hypothesis: he hit submit on the workspace invite flow and got the fallback toast." That's enough to start.

If the ticket came in through a CRM or a tool that captures user agent and session metadata automatically, pull that now. A lot of teams have this data and forget it exists. Check your support tool's metadata panel. If you're piping tickets into your own dashboard (or aggregating support signals across channels), the environment metadata is usually attached.

Step 2: Match the user's environment

Now you set up the conditions. Don't skip this. A bug that doesn't reproduce in Chrome on macOS will sometimes reproduce instantly in Safari on iOS, and you'll waste an hour before you realize the user was on a different platform than you assumed.

If you have the user agent, parse it. If you don't, default to the most common environment for your user base, then add the user's likely conditions.

For a desktop SaaS like Marcus's product, this means opening a fresh Chrome window with no extensions, then opening DevTools and setting the viewport to something realistic. If you suspect mobile, use the iOS Simulator or BrowserStack. If you suspect slow network, throttle in DevTools:

``` DevTools -> Network tab -> Throttling: "Slow 3G" ```

For Safari specifically, you need a real device or a real Mac. Safari's quirks (storage partitioning, ITP, autoplay restrictions) don't show up in Chrome's "Safari user agent" toggle.

If your bug involves cookies, sessions, or third-party auth, open an incognito window. This kills 30% of "can't reproduce" cases on its own. Stale localStorage from your last debugging session is a hell of a drug.

Step 3: Replicate the user's data state

This is the step most engineers skip. You log in as your own account, click the thing, it works, and you close the ticket. But your account has one workspace, three users, and no feature flags. The user's account has eleven workspaces, a custom SSO config, and the legacy "Pro" plan from before the rename.

You need to be the user — or close enough that the data shape matches.

If your app supports admin impersonation, use it. It's the fastest path. Most well-built SaaS dashboards have a "Sign in as user" button on the admin panel:

```bash

$ psql $DATABASE_URL -c "select id, name, plan from workspaces where id = 'ws_8f2c'"

id | name | plan ---------+----------------+------------ ws_8f2c | Acme Inc | pro_legacy ```

Note `pro_legacy`. That's not your current Pro plan. That's the one you renamed. If your invite quota logic has a branch on `plan === 'pro'`, you just found the bug without even reproducing it.

If you don't have impersonation, the next best thing is to seed a test account that matches. Write a script or a SQL block that creates a user with the same plan, the same number of existing teammates, the same feature flags. Keep these scripts around — you'll use them again.

```bash

$ pnpm tsx scripts/seed-repro.ts
--plan pro_legacy
--teammates 4
--flags new_invite_flow=false ```

For data-shape bugs (the user has 10,000 records, you have 12), seed at scale. Don't try to reproduce a pagination bug with three records.

Step 4: Walk the sequence — don't shortcut

You know the bug. You're tempted to skip to the action. Don't.

Bugs that survive into production usually require a specific path. The user opened the app, navigated to settings, opened a modal, closed it, opened a different modal, then clicked the thing. Some state from step 3 is still in memory when step 5 fires.

So walk the whole sequence. Open DevTools first. Clear console. Open the Network tab. Then:

  1. Log in (don't use a magic link or saved session — log in the way Marcus did).
  2. Land on whatever page the app defaults to.
  3. Navigate to Settings.
  4. Click Team.
  5. Click Invite.
  6. Type an email that does not already exist in your test workspace.
  7. Click Send.

Watch the Network tab. Watch the console. Watch the DOM. Note the request, the response, the status code, the headers.

If you don't see the error: try the variations. An email that's already a member. An email with a `+` in it. An email at a domain you SSO-restrict. An email in uppercase. An empty submit. A submit while offline. A submit twice in rapid succession.

Each variation takes 30 seconds. Doing twenty of them is ten minutes — less time than waiting for Marcus to reply.

If it still doesn't reproduce, check the things outside the happy path. Open a second tab. Log in there too. Now submit the invite in the first tab. Race conditions on session refresh are an underrated source of "something went wrong."

Step 5: Document what you saw (even when it doesn't reproduce)

Most engineers don't document failed reproduction attempts. They should. A ticket that says "tried in Chrome 121, Safari 17, with pro_legacy plan, with 4 existing teammates — did not reproduce" is far more useful in two weeks when the bug comes back than a ticket that says "couldn't repro."

Write down:

  • The exact environment you tested (browser, OS, viewport).
  • The exact account state (plan, flags, data counts).
  • The exact sequence you walked.
  • What you observed at each step.
  • Console output, network responses, screenshots if anything looked weird.

If you did reproduce it, capture everything. Console errors copy-pasted in full. Network request including headers and body. A screen recording. The stack trace from your server logs filtered to that user's request ID. The browser's `navigator.userAgent` string. A timestamp.

A good repro doc looks like a small case file. It should be the kind of thing a different engineer could read on Monday and immediately know what's happening, without asking you a single question. That's the bar. If your repro doc requires verbal explanation, it's not done.

This is, by the way, exactly the problem most small teams hit when they don't have a dedicated QA function — repro docs become tribal knowledge instead of artifacts.

When to give up and ask the user

There's an honest threshold here. Don't burn 90 minutes guessing.

Set yourself a timer. 25 minutes of structured reproduction is roughly the right budget for a normal-priority ticket. If you've walked the playbook, varied the inputs, checked the obvious environment differences, and you still don't have it — stop. Send the user one well-formed message.

A good follow-up to the user is precise and includes one ask, not five. Bad:

Hey, can you tell me what browser you're using, what OS, what plan, what email you tried to invite, was the teammate already in another workspace, and can you send a screenshot?

Good:

Hi Marcus — I can't reproduce this on my end. To narrow it down, could you open the browser console (F12, then the Console tab), retry the invite, and paste any red text you see? That'll tell me exactly which path is failing.

One ask. Specific. The user can do it in 60 seconds. You're treating them like a competent collaborator, not a hostile witness. This is the bigger pattern behind "can't reproduce" tickets — the user almost always has the missing piece; you just have to make it cheap for them to share it.

When to give up and automate it

Here's the harder honesty: if you're doing this playbook five times a week, you're spending a half-day a week on triage.

For a solo founder or a three-engineer team, that's significant. The math gets ugly fast: ten tickets a week, twenty minutes each, plus context-switching tax. You're losing a day of build time to ticket reproduction. And most of that work is the same five steps over and over — parse, match environment, replicate data state, walk sequence, document.

That's a script's job, not a human's.

FixFirstly does exactly this. It pulls bug reports from your Gmail, Slack, Zendesk, Intercom, Linear, CSV, or API, clusters the duplicates so you're not reproducing the same bug four times, then dispatches an AI agent that signs into your staging app, walks the sequence the user described, and files a GitHub issue with the repro steps, a session replay, and the console logs already attached.

You wake up. The ticket is already verified or already ruled out. You open the GitHub issue, you have everything you need, and you go fix it. That's the whole pitch. Here's how the agent loop works if you want the details.

The bar isn't "do this perfectly." The bar is "do this faster than a human can, on tickets that don't deserve a human's first pass." Vague tickets are exactly the kind of work that should be automated, because the first 15 minutes of reproduction are mechanical.

What good documentation looks like

Let's close with a before-and-after. Same bug, two artifacts.

Before — what landed in your inbox:

Subject: broken

Tried to invite my teammate this morning and it says "something went wrong" but doesn't say what. I'm on the Pro plan so I should be able to add people. This is really frustrating because we have a client demo today. Please fix asap.

— Marcus

After — what should be in your GitHub issue:

Title: Invite flow returns 500 for `pro_legacy` workspaces when invitee email is already a member of any workspace

Environment: Chrome 121.0.6167.85 on macOS 14.3, viewport 1440x900, network normal

Account state: Workspace `ws_8f2c` (Acme Inc), plan `pro_legacy`, 4 existing teammates, no relevant feature flags

Repro steps:

  1. Sign in as workspace admin
  2. Navigate to /settings/team
  3. Click "Invite teammate"
  4. Enter an email that exists in any other workspace in the system
  5. Click Send

Expected: Invite is sent, or a clear validation error appears ("This user is already a member of another workspace — they'll be added on accept")

Actual: Generic "something went wrong" toast. Network tab shows `POST /api/invites` returning `500` with body `{"error":"plan_check_failed"}`. Server logs show: `TypeError: Cannot read property 'invite_quota' of undefined at billing.ts:142`

Root cause hypothesis: `billing.ts:142` looks up quota by `plan === 'pro'`, but the legacy plan key is `pro_legacy`. Quota lookup returns undefined, downstream invite logic crashes.

Attachments: session replay (link), full console log, HAR file

Reported by: Marcus, 3 other users (clustered)

That second artifact is a ticket you can act on in fifteen minutes. You don't need to ping Marcus. You don't need to guess. The fix is probably one line.

That's the whole game. Get from artifact one to artifact two as fast as possible. Manually with a playbook, or with an agent doing the playbook for you. The playbook is the playbook either way — the only question is who's running it.


FixFirstly runs the playbook for you. Pulls the ticket, matches the environment, walks the sequence on your staging app, and files the verified GitHub issue. Free tier is 5 agent runs a month — enough to see if it actually saves you the half-day. Join the waitlist.

Verify your next bug in 24 seconds, not 4 days

FixFirstly reads bug reports from your inbox, reproduces them on staging, and files verified GitHub issues. Free during early access.

Join the waitlist