Unreal Ops Monitor

Quick start for Flags

One key per environment, one fetch at startup, and every check after that answered from memory. A flag check is never a network call.

Before you start

A key belongs to an environment, not to the project.

Create a key on the project's Flags settings screen, one per environment. That is a different shape from Monitor, where one DSN covers every environment and the environment is a label the SDK attaches at runtime: here the key is the decision. There is no environment switch inside the SDK to flip, so point a staging build at the staging key and a production build at the production key, and that is the entire configuration.

Fetch the rules

One request, at startup, for the whole rule set.

GET https://vps-7eadf7ac.vps.ovh.us/flags/<sdk key> answers with every flag in the key's environment:

a successful response
{
  "ok": true,
  "environment": "production",
  "data": [
    { "key": "new-checkout", "enabled": true, "rollout": 25 }
  ]
}

rollout is ignored while enabled is false. A flag check happens on render and inside loops, so answering it over the network would turn every one of those into a round trip, and would turn an outage here into an outage in your application. Fetching the whole set once and evaluating each check locally, against whatever was fetched last, is what keeps a flag check off the wire entirely.

The JS client

@unrealops/flags is not on npm yet.

once it is published
import { Flags } from "@unrealops/flags";

const flags = new Flags({
  host: "https://vps-7eadf7ac.vps.ovh.us",
  sdkKey: "uofl_...",
});

if (flags.isEnabled("new-checkout", user.id)) {
  // ...
}

Until then, this is what that client does under the fetch call, so it is what you write yourself:

today, with nothing installed
const host = "https://vps-7eadf7ac.vps.ovh.us";
const sdkKey = "uofl_...";

let rules = [];

async function refresh() {
  try {
    const response = await fetch(`${host}/flags/${sdkKey}`);
    if (!response.ok) return; // keep whatever rules we already have
    const body = await response.json();
    if (body.ok) rules = body.data;
  } catch {
    // Same rule on a network failure: change nothing already held.
  }
}

async function bucket(flagKey, distinctId) {
  const bytes = new TextEncoder().encode(`${flagKey}\0${distinctId}`);
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
  return ((digest[0] << 8) | digest[1]) % 100;
}

async function isEnabled(key, distinctId) {
  const flag = rules.find((candidate) => candidate.key === key);
  if (!flag || !flag.enabled) return false;
  if (flag.rollout >= 100) return true;
  if (flag.rollout <= 0) return false;
  return (await bucket(flag.key, distinctId)) < flag.rollout;
}

await refresh();
setInterval(refresh, 60_000);

isEnabled above is a promise, and the published client's will not be. crypto.subtle.digest is asynchronous, and a flag check on a hot path cannot be one, so the packaged client carries its own small, synchronous SHA-256 instead of this browser API, used for exactly this one calculation and nothing that treats it as security sensitive.

What a client does when it cannot reach us

Stated plainly, because a flag client that fails badly fails on every screen at once.

If the fetch fails, or answers with an error, keep whatever rule set was fetched last and change nothing. A client that cannot reach this endpoint behaves exactly as it did a minute ago, not as a project with no flags. Before the very first successful fetch, every flag reads as off, because there is nothing yet to answer from.

An unknown flag key is also off. There is no path in this system that turns a flag on for a key it does not recognise, which is what lets a feature check ship in the same release as the code it guards, before anybody has created the flag, and it is what makes a typo in a flag name fail closed rather than turning a feature on for everybody by accident.

Bucketing: the same person gets the same answer

A hash, not a coin flip, because a rollout that changed its mind on every screen would be worse than not shipping it.

Whether one person falls inside a rollout percentage is SHA-256 over the flag key, then a single zero byte, then the distinct id, read as the first two bytes, big endian, modulo 100. It is deterministic with no state stored anywhere: the same flag and the same distinct id land in the same bucket every time, on every screen, for as long as the rollout percentage does not change.

The flag key is part of what gets hashed so that one rollout's luck does not follow a person into every other rollout. Without it, the same slice of people would land inside every flag's rollout and the rest would land inside none of them; with it, somebody unlucky in a checkout experiment is not automatically unlucky in the next one.

Every change is audited

What a flag was, not only what it is.

Writing a flag records what it was as well as what it became, and who changed it, on the project's Flags history screen. "It is off now" does not tell an on-call engineer whether it was on ten minutes ago when the errors started, so every change states both sides of it rather than only the current value.

What to do next

  • Start every flag disabled, at 0% rollout, and raise it once the code path it guards has shipped. A flag that does not exist yet evaluates to off, so the safe order is to ship the check first and create the flag when it is time to turn it on.
  • Have a contact form on the same site? The Forms quick start is the other endpoint this platform runs.