Feature flags and the bug you can't reproduce
A tester files a bug: the checkout total shows NaN on https://staging.example.com/cart. A developer opens the same URL, on the same build, with the same test data, and sees a correct total. The ticket gets closed as "works for me". Two weeks later the same bug reaches production.
Nothing was flaky. The two people were running different code. A flag called new-pricing-engine was on for one account and off for the other.
Short version
- A feature flag is a switch that turns code paths on or off without a new deploy.
- With flags, the build number no longer tells you which code ran.
- Flag state belongs in every bug report, next to browser and URL.
- You can read the current flags from the network response, the SDK object, or storage.
- Test the risky flag pairs, not all combinations. Ten flags already make 1,024 of them.
What a feature flag actually is
A feature flag, sometimes called a feature toggle, is a named switch stored outside your code. The application asks a service "is new-pricing-engine on for this user?" and runs one branch or the other based on the answer.
Teams use flags for three things:
- Release control. Ship code that is turned off, then turn it on later.
- Gradual rollout. Turn it on for 5% of users, then 50%, then everyone.
- Targeting. Turn it on only for internal accounts, one customer, or one country.
The value of the flag can depend on the user, so two people on the same page can genuinely get different behaviour.
Why flags break reproduction
Before flags, "same build plus same data plus same browser" meant "same behaviour". Flags remove that guarantee.
Three specific traps cause most "cannot reproduce" tickets:
Targeting by user. The tester is [email protected] and sits in an internal group with the flag on. The developer logs in as their own account and gets the old path.
Percentage rollout. The flag is on for 20% of users. Which 20% is decided by hashing a user ID, so the result is stable per user and random across people. Nobody is lying, and nobody can reproduce the other's result.
Evaluation timing. Flags load over the network after the page starts. If the value arrives late, the first render uses the default and then the page changes. Bugs that appear only "on first load" are often this.
There is a fourth trap that is worse: the frontend and the backend can disagree. The browser gets new-pricing-engine: true while the API still evaluates it as false, so the new UI sends a request the old endpoint does not understand. You then see errors such as TypeError: Cannot read properties of undefined (reading 'discount') in the console with a perfectly healthy-looking network tab.
Make flag state a required bug report field
Treat flags like browser version. Nobody argues about including "Chrome 141". Flags deserve the same status.
Add one required field to your bug template:
Flags: new-pricing-engine=on, checkout-v3=off, promo-banner=variant-b
Report only the flags that touch the affected area, plus any flag switched in the last two weeks. A dump of 200 flags is noise.
| Bad | Good |
|---|---|
| "Cart total is wrong" | "Cart total shows NaN with new-pricing-engine=on" |
| "Flags: default" | "Flags: new-pricing-engine=on, checkout-v3=off" |
| "Happens sometimes" | "Happens on 3 of 3 tries as [email protected], never as [email protected]" |
| No user named | User and flag values both listed |
The second column costs about fifteen seconds and removes the most common cause of a closed-and-reopened ticket.
Collecting this by hand is the part that fails, because it means leaving the page mid-bug and copying values into a form. Reporting tools that run in the browser help here: Crosscheck captures console output, network requests, and environment details from the page as you file the report, so the evaluated flag payload is already in the ticket instead of in someone's memory.
How to read the current flags in your browser
You do not need access to the flag dashboard. The values are in the page. Try these in order.
1. The network response. Open DevTools, go to the Network tab, and filter by the vendor name. LaunchDarkly requests contain launchdarkly, PostHog requests contain /decide or /flags, and custom services often use a path such as /api/flags. Reload the page, click the request, and open the Response tab. You get the evaluated values for the current user, which is exactly what you want.
2. The SDK object on window. In the Console tab, type the vendor's global. Common ones:
// PostHog
posthog.featureFlags.getFlags()
// LaunchDarkly, if the client was stored globally
ldclient.allFlags()
If neither exists, type window and expand it, then look for a name matching your vendor.
3. Storage. Many SDKs cache the last known flags. Open the Application tab, then Local Storage, and look for keys containing flag, ld:, or ph_. Treat this as a cached value, not the truth, since it may be stale.
4. A debug parameter. Many teams support an override such as ?ff_new-pricing-engine=true. Ask your developers whether one exists, because it turns flag testing from guesswork into a switch you control.
Write down which of these four works for your product and put it in the team wiki. New testers waste hours rediscovering it.
Overriding a flag so you can test both paths
Reading flags is half the job. You also need to force a value.
The safe options, best first:
- A built-in override. A query parameter or an internal settings page. Nothing else affects other users.
- A targeting rule for your test account. Ask for
[email protected]to be added to a rule in the flag dashboard. Scoped to you, easy to undo. - A local override in the SDK. Some SDKs support a developer mode that forces values in your browser only.
Do not flip a percentage rollout up and down on a shared environment to test. Everyone else on that environment silently changes code paths, and their in-progress test runs become invalid.
After any override, reload the page and confirm the flag actually changed using the network response method above. An override that silently failed produces a passing test on the wrong code path, which is worse than no test.
Testing flag combinations without testing all of them
Every extra flag doubles the possible states. Five flags give 32 combinations. Ten give 1,024. You cannot test them all, and you do not need to.
Use this order:
- Test each new flag on and off. Two runs per flag. This is the minimum and it catches most problems.
- Test the pairs that share a screen. If
checkout-v3andnew-pricing-engineboth change the cart, test all four combinations of those two. Flags that never touch the same code rarely interact. - Test the "half deployed" states. Frontend on with backend off, and the reverse, if your system can reach those states.
- Test the default path. What a brand-new user with no flags sees. Teams forget this because their own accounts always have flags on.
A short worked example. You are releasing checkout-v3 while new-pricing-engine is at 20% rollout.
| checkout-v3 | new-pricing-engine | What you are checking |
|---|---|---|
| off | off | Today's production behaviour still works |
| on | off | The new checkout with old prices |
| off | on | Old checkout with new prices, the state 20% of users are in |
| on | on | The intended end state |
Four runs, and it covers the realistic combinations. Anything beyond this belongs in automated tests, where you can run the same suite twice with different flag values.
Clean up flags, or the problem grows
Every flag left in the code after a full rollout is a permanent branch that someone must reason about, and a permanent line in your test matrix.
Two habits keep it under control:
- Give each flag an owner and an expected removal date when it is created.
- Review flags older than 90 days each month, and delete the ones that are at 100% or 0% everywhere.
QA has a direct interest here. Flag cleanup is the only thing that makes the combination problem shrink instead of grow.
Frequently asked questions
How do I know whether a bug is flag-related at all?
Try the same steps with a different account, especially a fresh one. If the behaviour differs and the build is the same, flags or user permissions are the likely cause. Then compare the flag payload between the two accounts.
Which flags should I list in a bug report?
The ones touching the affected feature, plus any flag changed in the last two weeks. If you are unsure, attach the full evaluated payload as a file and name the two or three you think matter in the description.
Can I trust the flag values shown in local storage?
Only as a hint. Storage holds the last cached values and may be stale after a rule change. The response from the flag endpoint on the current page load is the reliable source.
Should automated tests run with flags on or off?
Both, for flags that are actively rolling out. Run the main suite in the default state and a smaller targeted suite with the new flag forced on, so a regression on either path fails the build.
What if the flag service is down during testing?
The SDK falls back to a default value, usually the off state or the last cached value. Test that path deliberately by blocking the flag domain in DevTools, since users will hit it during any outage.




