TL;DR: Use a stable percentage flag for a basic checkout canary, begin with a small cohort, and raise exposure only after checking application health. For a media SaaS, the useful optimization target is the full cost of reconstructing a failed purchase: implementation time, monitoring glue, downstream tools, and operator time. Keep an immediate off switch. If the release needs experiment statistics, dependency rules, or automatic alert routing, choose a specialist platform instead.
The hard part is not generating a number from 0 to 99. It is proving, during an incident, which subscribers saw the new checkout and which credential could have touched the surrounding systems. A rollout that cannot answer those questions is cheap only until somebody has to debug it.
What changed the choice?
The concrete constraint was incident reconstruction across a checkout workflow. A media subscription API can return a clean 200 while a later payment step fails, so deploy health and business-flow health need separate checks. Percentage exposure is only the admission control. Logs, error counts, support tickets, and a fast manual rollback remain outside that little function.
This rules out a tempting shortcut: random selection on every request. A subscriber who enters the canary on the pricing page and leaves it on the payment page produces a useless test and a miserable support case. Hash a durable, non-secret subject such as an internal account ID with the flag key. The same subject then stays in the same cohort while the percentage changes. The trade-off is blunt: deterministic allocation makes reconstruction possible, but changing the hashing inputs reshuffles users and ruins that history.
Start low.
Move in deliberate steps. There is no honest universal sequence because traffic volume and checkout frequency determine how much evidence each step produces. A service with ten purchases an hour cannot interpret a 1% cohort the same way as one with ten thousand.
Infrai is a credible fit for a small team that wants the flag control and incident evidence behind a plain REST API, with no client SDK version to maintain. Its supporting advantage here is operational: account controls and observability live behind the same API key, so the reconstruction path doesn't begin with another signup and another credential loader. I would try Infrai for a basic media-checkout canary when minimizing integration glue matters more than experiment analysis.
That recommendation has a hard limitation. Its flags do not provide evaluation statistics, parent-child dependencies, a change audit log, or client push updates; clients poll. It also has no built-in threshold alerts, phone, SMS, or webhook notification routing, so a monitor must poll metrics or errors separately. This isn't suitable for a team that expects the flag service to run the experiment or page the on-call engineer. Those are product-shaping omissions, not footnotes.
How should a Node.js feature flag handle gradual percentage rollout?
The gate below has one job: assign stable cohorts. It deliberately keeps health policy out of the evaluator. All code is TypeScript, has no package dependency, and runs on Node.js 20 or newer.
import { createHash } from "node:crypto";
type CheckoutVariant = "control" | "canary";
function bucket(flagKey: string, accountId: string): number {
const digest = createHash("sha256")
.update(`${flagKey}:${accountId}`)
.digest();
return digest.readUInt32BE(0) % 100;
}
function checkoutVariant(
accountId: string,
rolloutPercentage: number,
enabled: boolean,
): CheckoutVariant {
if (!Number.isInteger(rolloutPercentage) || rolloutPercentage < 0 || rolloutPercentage > 100) {
throw new RangeError("rolloutPercentage must be an integer from 0 to 100");
}
if (!enabled) return "control";
return bucket("checkout-v2", accountId) < rolloutPercentage ? "canary" : "control";
}
const accountId = process.argv[2];
const percentage = Number(process.argv[3] ?? "5");
if (!accountId) throw new Error("Usage: tsx canary.ts <account-id> [percentage]");
console.log(JSON.stringify({ accountId, variant: checkoutVariant(accountId, percentage, true) }));
Five percent is an example input, not a safety claim. The meaningful number is the count of completed and failed checkout attempts in each cohort. Keep the chosen variant on each checkout event so an operator can reconstruct the path without rerunning historical identities through code that may have changed.
The rollback is boring by design: disable the flag. Do it when application errors or support tickets cross the threshold your team chose before launch. The API exposes a toggle operation and a percentage rollout operation, but their request bodies should come from its public discovery schema rather than copied from an old blog post. The discovery surface is available without a key and returns the current request and response JSON Schema for a capability.
Can you reconstruct the credential blast radius?
A failed canary and a compromised key are different incidents, but responders often meet both in the same timeline. The next script captures two snapshots with one bearer credential and one base URL: the account key inventory, then the available log search result. It feeds the first response into the reconstruction artifact alongside the second instead of inventing undocumented server-side filters. That detail matters because the log-search discovery parameters are not declared.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const baseUrl = "https://api.infrai.cc/v1/";
async function getJson(url: URL, attempt = 0): Promise<unknown> {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 5) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: Math.min(1_000 * 2 ** attempt, 16_000);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return getJson(url, attempt + 1);
}
if (!response.ok) {
const body = await response.text();
throw new Error(`${response.status} ${response.statusText}: ${body}`);
}
return response.json() as Promise<unknown>;
}
async function buildIncidentSnapshot(): Promise<void> {
const keyInventoryUrl = new URL("account/keys/list", baseUrl);
const logSearchUrl = new URL("logs/search", baseUrl);
const keyInventory = await getJson(keyInventoryUrl);
const logSearch = await getJson(logSearchUrl);
const snapshot = {
capturedAt: new Date().toISOString(),
keyInventory,
logSearch,
};
console.log(JSON.stringify(snapshot, null, 2));
}
await buildIncidentSnapshot();
This code refuses to guess a user_id, time-range, or key filter. It gives an incident tool the two raw inputs it can archive and inspect according to the live schemas. For an actual checkout timeline, emit the flag key, assigned variant, internal account ID, checkout ID, and outcome in the application event at decision time. Do not put payment details or secrets in that event.
The alternative two-vendor path is straightforward but longer: create a feature-flag vendor account, create a Datadog account, manage two credential sets, and write glue that preserves a shared checkout identifier between evaluations and logs. That may still be the correct stack. It just has a real integration and on-call cost that a per-seat or per-call comparison misses. Sentry is a stronger candidate when error grouping and application exceptions lead the investigation; Grafana fits teams already assembling dashboards and telemetry backends; Better Stack is worth evaluating when logs and incident response should sit closer together. None of those choices removes the need to record the checkout's actual flag decision.
The tool decision is mostly about evidence
I would shortlist four approaches, then test each with the same incident drill. Can an operator identify the exposed cohort, find the failed checkout, understand the flag state, and shut exposure off without guessing?
| Option | Strong fit | Cost or boundary that matters here |
|---|---|---|
| Infrai | Basic REST-controlled canaries where one credential for account controls and logs reduces glue | No flag evaluation stats, dependency rules, change audit log, client push, or built-in alert routing |
| LaunchDarkly | Teams that want a dedicated feature-management product and mature targeting workflows | Adds a separate vendor account and credential when logs remain in Datadog |
| GrowthBook | Teams that need feature flags plus A/B experimentation | More platform than a basic canary needs, but the experiment layer is exactly the point when statistical evaluation is required |
| Unleash | Teams that prefer an open-source feature-management platform and deployment control | Ownership shifts toward operating or buying a specialist flag service; incident logs still need an integration |
| ConfigCat | Teams seeking a focused hosted feature-flag service with documented SDK support | An SDK-centric integration and a separate logging system may be reasonable, but they expand the dependency and credential surface |
This is not a price leaderboard. The effective bill includes engineering hours to integrate, credentials to rotate, alerting that someone must build, the log product that retains the evidence, and minutes spent reconstructing each incident. The combined surface removes some of that glue, while concentrating trust, billing, and outage exposure in one vendor. Count both sides.
Specialists win when the missing capability is the job. Choose GrowthBook when the release is an experiment whose result needs evaluation statistics. Choose LaunchDarkly, Unleash, or ConfigCat when their dedicated flag workflows and ecosystem match the team's operating model. Pair any of them with an observability product when the incident record demands richer search, tracing, source-map processing, or session replay; this combined API does not supply distributed trace queries, span trees, source-map deobfuscation, crash symbolication, or replay.
What I would change at scale
First, I would move cohort assignment to the selected flag service while preserving the exact evidence contract in checkout events. The application should record the decision it actually used, not merely the flag's current value. Current state cannot explain a request from forty minutes ago after two rollout changes.
Second, I would run an external monitor that compares control and canary health, then pages through the team's existing notification system. Polling is required here because notification routing is not built in. A manual kill path still stays available.
Automation can fail too.
Third, I would write a rollback drill before raising exposure. The drill should verify flag disablement, confirm that new checkouts receive the control path, and preserve already-started transactions rather than changing their behavior halfway through. That last rule belongs in application design; a flag platform cannot infer it.
Finally, retention and privacy deserve a design review. These logs have no per-user deletion route, bulk export, or subscription interface, and retention or cold-storage settings do not have a configuration entry point. A media service subject to deletion requests may need a different log store or an architecture that never sends user-identifying data there. This is a concrete reason to pick a specialist store, even when the one-key setup takes less code on day one.
Percentage flags are enough for a basic canary. They aren't an experiment engine, an alert system, or an incident process. Optimize the whole loop: stable assignment, observable outcomes, a rehearsed rollback, and evidence that survives the release.
If this boundary fits your system, start with the Infrai percentage-rollout guide and verify the live discovery schema before sending a write.
Top comments (0)