Record exposures

An exposure is the analytics event recorded when a real user is served a flag. It's what powers everything worth having: per-flag usage (checks/hr, pass rate, staleness), always-on impact (how each variant moves your metrics), and experiments (lift, significance). The raw evaluation is always free; the exposure is the billable analytics event, so you pay to measure, never to serve.

Exposures are on by default

When you evaluate a flag remotely (the standard OpenFeature OFREP provider POSTs one request per check), Flagon logs the exposure for you automatically. There is nothing to wire up. It is deduped per user, flag, and variant per hour, so a user reloading a hot page bills once an hour, not once per request. Disable it per key with the Auto exposures toggle on the client key, or per request with the X-Flagon-Exposure: off header. The rest of this page is for cached evaluation, where the SDK fetches the whole config and resolves flags locally, so Flagon never sees each check and you log exposures yourself.

Send the served variant and the targetingKey with each exposure and Flagon attributes it to the arm the unit saw (the targeting key is stored only as a salted hash), so impact and experiments light up automatically.

All requests go to the Flagon API host and authenticate with a client key, the same key you evaluate with:

HTTP
POST /ofrep/v1/exposures
Authorization: Bearer flagon_client_...
Content-Type: application/json

Base URL

Examples use https://api.flagon.io. Running Flagon locally, the API is at http://localhost:3002.

Cached evaluation: log with an OpenFeature hook

If you evaluate from a cached config (the SDK fetches all flags and resolves locally), log exposures yourself with an OpenFeature hook registered once, globally. Its after stage fires on every successful evaluation, so you never have to remember to record an exposure, it just happens, with the served variant and targeting key already in hand.

TypeScript
import { OpenFeature, type Hook } from "@openfeature/server-sdk";
import { recordExposure } from "./exposures"; // the batching helper below

/** Records an exposure for every flag your app evaluates. */
const exposureHook: Hook = {
  after(hookContext, details) {
    recordExposure({
      key: hookContext.flagKey,
      variant: details.variant,
      targetingKey: hookContext.context.targetingKey as string | undefined,
    });
  },
};

// Register once at startup. Now every check across your app logs an exposure.
OpenFeature.addHooks(exposureHook);

Every OpenFeature SDK exposes the same hook interface, so the pattern is identical in Python, Go, Java, and the rest: register one hook, get exposures everywhere. Prefer to be explicit instead? Call the endpoint directly, below.

Send a batch

The body is an events array. Each entry names the flag key it relates to. To attribute an exposure to a running experiment, also send the served variant and the targetingKey (the unit). Flagon records which arm the unit saw (the targeting key is stored only as a salted hash). Any other fields are ignored. Send up to 1,000 events per request and chunk anything larger.

curl -X POST "https://api.flagon.io/ofrep/v1/exposures" \
-H "Authorization: Bearer $FLAGON_CLIENT_KEY" \
-H "Content-Type: application/json" \
-d '{"events":[{"key":"new-checkout","value":true},{"key":"theme","value":"dark"}]}'

A 202 means the batch was recorded. recorded is how many events this request counted; duplicate is true when the batch was a retry Flagon had already seen (see below):

JSON
{ "recorded": 2, "duplicate": false }

Idempotent retries

Exposures are billable, so recording is durable, not fire-and-forget: a failed request is safe to retry. To make a retry count exactly once, send an Idempotency-Key header with a stable, unique id for the batch. Flagon treats two requests with the same key as the same batch, so a network retry never double-counts:

HTTP
POST /ofrep/v1/exposures
Authorization: Bearer flagon_client_...
Idempotency-Key: 6f9c2b1e-8a3d-4c77-b0a1-2f5e9d4c1a20

A repeat of a key Flagon has seen returns 202 with { "recorded": 0, "duplicate": true } and records nothing further. Omit the header and each request is a distinct batch (still durable, but a retry would count twice).

A batching helper

You rarely want a network call per exposure. Buffer them and flush in batches on a timer (and once more as the page unloads, so nothing is lost):

JavaScript
const FLAGON_API = "https://api.flagon.io";
const MAX_BATCH = 1000;

// Buffer exposures and flush them in batches.
export function createExposureRecorder(clientKey, { flushMs = 5000 } = {}) {
  let queue = [];

  async function flush() {
    if (queue.length === 0) return;
    const events = queue.splice(0, MAX_BATCH);
    // One id per batch: if this flush fails and you retry it, reuse the SAME key
    // so the retry is counted once, not twice.
    const idempotencyKey = crypto.randomUUID();
    await fetch(`${FLAGON_API}/ofrep/v1/exposures`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${clientKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({ events }),
      keepalive: true, // let an in-flight flush finish during unload
    });
  }

  const timer = setInterval(flush, flushMs);
  if (typeof window !== "undefined") {
    window.addEventListener("pagehide", flush);
  }

  return {
    // Call right after you evaluate a flag you want analytics on.
    record(key, extra) {
      queue.push({ key, ...extra });
      if (queue.length >= MAX_BATCH) flush();
    },
    flush,
    stop() {
      clearInterval(timer);
      return flush();
    },
  };
}

Wire it in next to your evaluations:

JavaScript
const exposures = createExposureRecorder(process.env.FLAGON_CLIENT_KEY);

const value = client.getBooleanValue("new-checkout", false);
exposures.record("new-checkout", { value });

Errors

StatuserrorCodeMeaning
400PARSE_ERRORThe body was not valid JSON.
400INVALID_CONTEXTevents was missing, not an array, or larger than 1,000.
401AUTHENTICATION_ERRORMissing, malformed, or revoked client key.
403PLAN_LIMIT_REACHEDThe plan's monthly event allowance is exhausted (the Hobby hard cap). Upgrade or wait for the cycle to reset.
429NoneToo many requests; back off. A Retry-After header says for how long.

Notes on behavior

  • Recording is durable and idempotent. A 202 means the batch was recorded, not just queued. Because exposures are billable, a failed request is safe to retry; send an Idempotency-Key so a retry counts exactly once. Still, do not block a user path on the flush: buffer and send it in the background.
  • Only the count is metered. Flagon stores per-day counts, not the exposure detail, so extra fields you attach do not change your bill.
  • Checks stay free. Evaluating flags never counts against your plan, only the exposures you choose to send do. See pricing for each plan's included events.
  • Allowances are enforced. Each plan includes a monthly exposure allowance (your current allowance is shown on the usage page in the app). A free-plan org past its cap gets a 403 here and needs to upgrade to continue. Flag evaluation stays free and unaffected: only metering stops. Pro meters the overage instead of blocking. See pricing for current numbers.

Next