Baseline · Reproducible Build · Canary · Runtime Guards Last

The risk in adopting obfuscation is not the transform. It is switching everything on at once.

Protection failures in production almost never trace back to a transform being wrong. They trace back to a team enabling twenty options and three runtime guards in a single release, then discovering something is broken with no way to work out which change did it. The fix is ordering, and it costs a few extra releases rather than any extra tooling.

The Short Version

Order beats effort

Four stages, each one leaving the next one debuggable.

Reproducible before strongA recorded seed means the same inputs regenerate the same output, so a difference is a signal instead of noise.
One bundle, then widenThe first target should be something you can turn off on its own without a total outage.
Guards last, and aloneThe default action when a runtime guard decides something is wrong is to throw, so it needs its own release.
Why Rollouts Go Wrong

Three failure patterns, and all of them are really about attribution

None of these are exotic. They are what happens when a protection step is treated as a switch to be flipped rather than a change to be released, and each one removes information you will want later.

Everything at maximum on day one

Turning on the full option set plus the runtime guards changes how the code is written, how large it is, how long it takes to parse, and what it does when it suspects interference, all in one release. If anything then misbehaves you have dozens of candidate causes and no way to rank them. Teams in this position usually end up bisecting options by hand under pressure, which is slow and tends to end with the whole thing switched off.

No baseline was recorded

Nobody wrote down the bundle size, the load timing or the error rate before the change, so every number afterwards becomes an argument rather than a measurement. This is the cheapest mistake to avoid and one of the most expensive to live with, because the missing numbers are unrecoverable once the change has shipped. Ten minutes before the first release saves a week of debate after it.

No rollback smaller than off

If the only lever is enabled or disabled, then one bad interaction with one option costs you the entire programme, and the decision gets made at the worst possible moment by whoever happens to be on call. Granular, reviewable configuration is what keeps a small problem small, and it is worth setting up before you need it rather than during an incident.

Stage One

Make the build reproducible before you make it strong

This stage ships no protection at all to users. It exists so that every later stage produces evidence instead of impressions, and it is the part teams are most tempted to skip.

Record a seed

Passing a fixed seed makes the naming and layout decisions deterministic, so the same source with the same options and the same seed produces byte-identical output. That turns it works on my machine into something you can check, and it means a hash that changes unexpectedly is genuinely telling you something. Store the seed with the release record rather than in someone's shell history.

Generate the manifest and keep it

The manifest records each file with a digest of its source and a digest of its output, which is what lets you answer later which source produced the artefact a customer is running. Verifying a delivered file against the manifest is a fast check with an unambiguous result, and it is the thing that resolves a whole class of arguments about whether the file in production is the file you built.

Wire the check into CI now

A release check that runs in your pipeline and fails the build is worth far more than the same check run by hand occasionally, because the manual version stops happening the first busy week. Add it while nothing depends on it and it will be uncontroversial. Add it after an incident and it becomes a negotiation. See the build integration guide for where it slots into common pipelines.

Stage Two

One bundle, a conservative option set, no runtime guards

The goal of this stage is to learn how your own code reacts, on a target where being wrong is survivable and reversible.

Pick a low blast radius target

An internal admin bundle, a single lazily loaded route, or one feature chunk. What makes a good first target is that it has real users, so you learn something real, and that it can be reverted on its own without taking the application with it. Your main entry bundle fails both tests and should be one of the last things you widen to, not the first.

Start with renaming and string handling

These change how the code is written without changing the execution model, which makes them the most predictable place to begin and the easiest to reason about when something looks odd. Leave the structural transforms and anything that alters control flow for a later stage, once you have seen a clean canary and know your reserved-name list is right.

Reserve anything reached by name

The reliable predictor of breakage is code that looks names up as strings at runtime rather than referencing them directly: dependency injection tokens, framework lifecycle hooks, anything read out of a data attribute, serialised keys that cross a network boundary, and members another team's script calls. Put these in the reserved list deliberately, and treat the list as documentation of your own dynamic surface rather than as a workaround.

Stage Three

Canary, and compare against the baseline you recorded

Ship the protected bundle to a slice of traffic and read four numbers. Choose them before you ship, because choosing afterwards is how a bad result gets reinterpreted as a good one.

Watch one error signature, not total volume

Overall error counts move every day for reasons that have nothing to do with you, so a small real regression hides inside the noise. What you are looking for is a specific new signature appearing in the canary group and not in the control group. That is a clean signal, and it is usually visible within an hour rather than needing a week of data.

Compare size and timing to the recorded figures

Both will move, and the point is to know by how much rather than to be surprised. Size is the easy one. The more useful measurement is the time from script fetch to your first meaningful application milestone, because that is what a user experiences, and the size question is treated separately.

Run the suite against the protected artefact

A suite pointed at source proves the source is fine, which was never the question. Point it at the built file instead and you learn whether the artefact behaves. Tests that reach into internals by name will fail first, and that is useful information rather than an obstacle, as the testing guide covers in more detail.

Expect the noise to come from third parties

Tag managers, analytics, session replay and monitoring agents all reach into a page that is no longer shaped the way they expect, and they generate a disproportionate share of first-canary reports. Knowing this in advance stops an integration problem being misread as a transform problem. That interaction has its own write-up.

Stage Four

Runtime guards go last, and in a release of their own

This is the stage with user-visible failure modes, and the one most worth being deliberate about. It is also entirely optional, and plenty of teams stop after stage three.

The default action is to throw

When a runtime guard concludes that something is wrong, the configured response decides what your user sees, and if you have not configured one the engine uses throw. That is a defensible default for a guard and a surprising one for a team that enabled the option without reading it, so decide this explicitly rather than inheriting it. Of the available actions, degrade is the only one that does not raise.

Understand the timing behaviour first

Debug protection works by comparing elapsed time against a threshold derived from its interval, using wall-clock deltas. Browsers throttle timers in hidden tabs aggressively, so a tab left in the background can cross that threshold without anyone touching a developer tool. This is worth reading about before enabling it rather than after the first support ticket, and it is documented in full.

Ship them alone so attribution stays cheap

If guards and transforms arrive in the same release, an intermittent error report has two unrelated explanations and you will burn the incident separating them. Shipped separately, the same report is immediately attributable to one change you made this week, and reverting is a decision about one option rather than about the whole programme.

What To Write Down

The record that makes the next release boring

Everything expensive about running this long term comes from decisions that were made once, by one person, and never written anywhere.

The option set, in version control

A checked-in configuration file means the protection settings get reviewed like any other change, show up in history with an author and a reason, and can be reverted by a commit. Options passed as ad hoc command-line flags in a CI job have none of those properties and tend to drift quietly between environments until someone notices two builds do not match.

The rollback plan, decided in advance

Write down what you would turn off first, who is allowed to make that call, and how long it takes to reach users. This takes fifteen minutes when nothing is wrong. It takes an hour of argument when something is, and the version produced under pressure is always worse than the version written calmly.

A per-release record

The seed, the manifest, the option set and the resulting digests, kept per release. This is the same material that answers audit and customer-security questions later, so the work is not single-purpose, and assembling it once means it is already there the first time somebody asks.

Frequently Asked Questions

Questions teams ask before the first release

How long should a phased rollout take?

For a team shipping weekly, four releases is a realistic shape: one to make the build reproducible and wire the check into CI, one to protect a single low-risk bundle, one to widen coverage after the canary is clean, and one for runtime guards if you want them at all. Compressing that into a single release is what produces the incidents, because every symptom then has four candidate causes and no way to separate them. Teams that ship daily can move faster, but the ordering still matters more than the calendar.

Which bundle should we protect first?

Something with real users but a small blast radius, and ideally something you can turn off independently. An internal admin bundle, a single lazily loaded route, or one feature chunk are all good first candidates. Your main entry bundle is the worst possible starting point, because a failure there is total rather than partial and it is loaded on every page by every visitor. The point of the first stage is to learn how your pipeline behaves, not to maximise coverage.

What should we measure during the canary?

Pick the metrics before you ship, because deciding afterwards invites motivated reasoning. Four are usually enough: the rate of one specific error signature rather than total error volume, bundle size against the figure you recorded beforehand, the time from script fetch to your first application milestone, and the completion rate of one business flow that exercises the protected code. Total error volume moves for unrelated reasons every day and will not tell you anything on its own.

Do we need to run our test suite against protected output?

Yes, and it is the single highest-value check in the whole process. A suite that runs against source proves your source is correct, which was not the thing in doubt. What you need to know is whether the emitted artefact behaves the same, and that requires pointing the suite at the built file. Tests that reach into internals by name are the ones that will fail first, and they fail for a reason worth knowing about before your users find it.

Why should runtime guards be enabled separately from the transforms?

Because they have a different failure mode. The transforms change how code is written and are either correct or produce an immediate, obvious break. The runtime guards make decisions while your application is running, and the default action when one of them decides something is wrong is to throw. Mixing both into one release means a report of an intermittent error in production has two very different candidate explanations, and you will spend the incident separating them instead of fixing it.

What does a rollback actually look like here?

Ideally it is a configuration change that ships through your normal release path, not a code change. If your option set lives in a checked-in config file and your build reads it, then reverting is a single commit that reviewers can read. What you want to avoid is a rollback whose only available granularity is off, because that turns a small problem with one option into a decision about the entire programme, usually taken under time pressure by whoever is on call.

Related Guides

Where to read next

Build integration

Where the protection step and its release check belong in common pipelines, and what each one needs to cache.

Testing protected output

Why the suite has to point at the built artefact, and which kinds of test break first when it does.

Runtime defense options

What each guard checks, what it does when it decides something is wrong, and which action avoids raising.

Next Step

Start with the stage that ships nothing

The first release in this plan changes nothing your users can see, which is exactly why it is the one worth doing first. Record the baseline, fix the seed, keep the manifest, and every decision after that has evidence behind it instead of an opinion.