Tracing a Feature Flag Through a Codebase
Finding every reference to a feature flag across repos and config requires more than string search.

A feature flag is a conditional in the code that gates a path at runtime, letting a team ship code without releasing it to users yet. Tracing one, finding every definition, every place it's checked, every branch it guards, and every stale reference left behind, is a distinct engineering problem from just knowing what the flag does. Most teams treat those as the same question. They aren't, and the gap between them is where dead code goes to hide.
Where flags live in a codebase: definitions, evaluation points, guarded paths, and config
Every flag worth tracing breaks into four separate things, and a full trace has to find all four or it isn't done.
There's the definition itself: a string key sitting in a control plane or a config file, YAML, JSON, TOML, sometimes just an environment variable. Then there are the evaluation call sites, every spot in the code where the SDK gets asked to resolve that key, usually an if block deciding which way to go. Downstream of that sits the guarded code path, the actual feature logic, which might span several functions, a few modules, or more than one service. And separate from all of it: the configuration and targeting rules, percentage rollouts, segment conditions, scheduled changes, which often live outside the repo entirely, in a dashboard or a GitOps file somewhere.
Flags scatter across a codebase in ways that make locating all four painful. The same key might show up in a frontend service, a backend API, and a data pipeline, looking identical in source but behaving differently depending on which SDK resolves it. The evaluation call is usually one line, something like isEnabled("flag-key"), so a literal string search will find it. But knowing what that call actually controls means reading the logic around it: some teams build the key dynamically from a string template at runtime, and that slips past any static search. Wrapper functions make it worse: plenty of teams wrap the SDK call in their own utility, so a search for the SDK's method name misses every call that goes through the wrapper instead.
Config-as-code tools like Featurevisor and GO Feature Flag push flag definitions into Git, which is good for review and audit trails, but it also means the definition lives in one repo or directory while the call sites live somewhere else. OpenFeature's provider model adds its own wrinkle: application code calls the OpenFeature SDK once, and which backend actually serves the flag is decided at runtime. That makes the call site look the same no matter which vendor sits behind it, which simplifies search on one end, but it means the backend config still needs tracing on its own, separately.
Why a simple text search misses many references
Grep the flag key. Run "find all usages" in the IDE. That's the instinct, and it works, right up until it doesn't.
It catches literal string matches inside whatever repo happens to be open. It misses flags built from string fragments at runtime, flags passed around as variables, flags called through a wrapper function instead of the SDK directly, flags evaluated in some other microservice the IDE never indexed, and flags sitting in a config file the search tool doesn't parse. None of these are exotic. They're the default state of a codebase that's been touched by more than one team.
Scale just makes the failure more obvious. A search that works fine on a small project chokes on a codebase spread across dozens of repositories: "find all usages" either returns after a long wait or comes back empty because it only ever looked inside the one open project. No engineer carries a mental map of every service that touches a given flag key, and nobody should be expected to.
The dynamic isn't new. When Log4j's vulnerability hit, teams that could query their whole codebase in one shot remediated in days. Teams stuck grepping local clones one at a time spent far longer not knowing if they'd found everything. Flag cleanup runs into the identical wall, just quieter and with no security vulnerability forcing the issue.
Three specific gaps explain why. A string search finds the literal key but not the wrapper function sitting around it; a symbol search finds the wrapper but won't catch every guarded path downstream of it. Cross-repo flags need cross-repo search; a single-project tool just won't reach them. And targeting rules, percentage rollouts, none of that lives in source code at all, so it needs its own separate query against the control plane or the config store.
A step-by-step method for tracing a flag from its key to every affected code path
Start with the canonical key. Pull the exact string from the control plane or the config file, that's the anchor for everything downstream. Identify what type of toggle it is: release, experiment, ops, or permissioning, since that tells you whether finding it two years later is a red flag or just business as usual. Note its current state too, whether it's active, rolled out to 100%, or already sitting archived somewhere in the platform.
From there, find every SDK call site across every repo the flag touches. Start with a literal search on the key string, that covers the common case. Then search for the SDK method name itself, things like isEnabled, variation, or getBooleanValue, to surface any wrapper functions built around it. Trace each wrapper to its callers, since those callers are the real call sites, not the wrapper itself. If the key gets built dynamically, search on string fragments and look for the template pattern doing the assembling. At any real scale, this step needs cross-repo search, not whatever ships in an IDE.
Next, map what each call site actually guards. For every evaluation point, figure out which branch is the old path and which is the new one, then trace both far enough to see what they touch: which functions they call, which APIs they hit, which data schemas they depend on. Flag anything that reaches into another service or writes to a database schema that might be getting deprecated.
Then check the control plane and config files for targeting rules, the rollout percentage, the segment conditions, anything scheduled. A flag at 100% rollout still leaves both branches in the code, so knowing the rollout state matters before anyone touches the delete key.
Watch for the signals that say a flag has gone stale: it's outlasted the expected lifespan for its type (a release toggle still sitting there two months after launch is debt by definition), one branch of the conditional hasn't been reachable in production for a long stretch, or it references an API or access pattern that's already deprecated, which is a security concern as much as a cleanup one.
Last, document the blast radius before removing anything. List every file, service, and config location the flag touches. Roughly 80% of flag removals touch more than one file, so plan for a real multi-file change rather than a one-line delete, and route the pull request past the owners of every service it affects.
Why stale flags left untraced become a compounding liability
GrowthBook puts the healthy ceiling for stale flags at under 15% of a codebase's total. Most organizations sit well above 40%, some past 50%, simply because manual tracing can't keep pace with how fast flags get created.
The math behind why this matters is unforgiving. N independent boolean flags produce 2^N possible states, and no team tests all of them. Every stale flag left in place adds combinations nobody has ever run in production, and the worst bugs tend to live exactly there, in the states nobody thought to check.
There's a security angle too, not just a tidiness one. A flag parked at 100% rollout for a year still leaves the old code path sitting in the codebase, fully intact, and that retired logic can still get referenced through deprecated APIs or old access patterns. Stale flags left in place can expose retired code paths through deprecated APIs and old access patterns, creating unnecessary risk that should have been eliminated once the new path shipped.
Lifecycle models exist precisely to make this tractable rather than a permanent mess. LaunchDarkly's lifecycle model includes stages such as Live, Needs Code Removal, Archived, Deprecated, and Deleted, and comes with a recommendation to archive quarterly and a healthy time-to-archive window of 90 to 120 days. Unleash runs five stages, Define, Develop, Production, Cleanup, Archived, and treats flags stuck sitting in Cleanup as the clearest signal that debt is piling up.
AI coding tools have sped up the flag-creation side without doing much for cleanup. Sonar's State of Code Developer Survey found AI tools lifting individual developer productivity by 35%, yet developers still lose 23 to 25% of their workweek to toil. AI writes more flags, faster. It doesn't remove the stale ones on its own, not without tooling built specifically to close that loop.
Uber's own numbers show what happens once an organization takes the problem seriously. Its Polyglot Piranha tool generated nearly 5,000 pull requests over a six-month evaluation period, removing stale flags across codebases totaling more than 10 million lines. That's not a scale manual review was ever going to reach.
What tooling supports cross-repo flag tracing at scale
No single tool covers this end to end. The work splits across three layers: code search to find where a flag lives and what it touches, a flag management platform to track its lifecycle and targeting rules, and automated cleanup tooling to handle the removal itself once the tracing is done.
On the search side, the requirement is straightforward to state and hard to satisfy: literal, regex, and symbol search across every repo the organization owns, not just whatever project happens to be open in an editor. OpenGrok, open-source and web-based, has held up well in enterprise environments with large legacy archives, letting engineers explore code without a local build or a full IDE index. Its strength is holding steady in codebases with a lot of history behind them, which is exactly where flags tend to accumulate the longest. The Log4j lesson applies here directly: teams that can query the whole codebase at once remediate in days, while teams limited to local clones spend far longer still unsure they've caught everything.
The flag management layer is where lifecycle state and targeting rules actually surface, cutting down the manual audit work considerably. LaunchDarkly runs as a runtime control layer connecting to IDEs, CI/CD pipelines, and observability systems, with role-based access, audit logs, approval flows, and lifecycle stages built into the product, plus native SDKs across mobile, frontend, backend, and edge. Unleash is open-source and self-hostable through Docker or Kubernetes at no cost, with an enterprise tier starting at $75 per seat per month with a five-seat minimum, and its five-stage lifecycle puts the Cleanup stage front and center as the debt indicator. GrowthBook combines feature management with experimentation, plugs into existing data warehouses, is self-hostable, and had crossed 7,000 GitHub stars as of early 2026. GO Feature Flag ships as a single binary with no database required, MIT-licensed and GitOps-friendly, storing flags as YAML, JSON, or TOML, with native OpenFeature providers across Go, Node, Python, Java,.NET, PHP, Ruby, Swift, and Kotlin, plus React and Angular; Grafana runs it in production, and there's no paywalled enterprise tier. Featurevisor takes a "flags as code" approach, with definitions living in Git, a CLI that builds static datafiles published to a CDN, and no server or database at all, just Git review and CI standing in as the control plane; it ships official OpenFeature providers across its SDKs. Flagsmith is open-source too, running in the cloud, self-hosted, or in a private cloud, with remote config management alongside flags, A/B and multivariate testing, and granular access control with audit logs and feature-specific approval flows. And flagd, the CNCF's reference OpenFeature backend, is a minimal, config-driven engine reading from files, HTTP, or gRPC sync sources, with no database and no admin UI, built more for local development and learning the standard than for running a large org's flag estate.
OpenFeature matters here beyond just being one more spec to know. Flag evaluation code scattered across services, all written against one vendor's proprietary SDK, means touching every single call site the moment anyone wants to switch backends. OpenFeature decouples the API from the backend: write against the SDK once, and swap providers later without rewriting application code. As of 2026, GO Feature Flag's own analysis treats OpenFeature support as a first-class selection criterion for any new tool, not a bonus feature. A 2026 State of Open Source report found 68% of organizations naming vendor lock-in avoidance as a reason for choosing open-source tooling, up from 55% previously, which tracks with how much friction proprietary call sites create down the line.
The cleanup layer is the least crowded of the three. Uber's Polyglot Piranha stands as the clearest working example, a general-purpose multi-language code transformation tool used at scale specifically for stale flag removal, and the nearly 5,000 pull requests it generated across 10 million-plus lines of code is the reference point for what automated tracing-to-removal actually looks like in practice. At smaller scale, teams lean on IDE refactoring tools or custom scripts keyed to SDK method signatures, but none of that works without a complete call-site inventory first. Without the tracing, the cleanup tool has nothing reliable to act on.


