name: bq-analytics-flags description: Add feature flags to a project that already has (or is adding) bq-analytics, then operate them — flip on/off, change rollout %, manage allowlists, materialize cohorts from BigQuery, and evaluate flag impact. Backed by Vercel Edge Config; exposures auto-track to events.raw. Use when the user wants flags, says "add flags", or asks how to ramp / kill / experiment with a feature.
bq-analytics feature flags
Flags live in a single Vercel Edge Config item under the key flags. The
SDK reads at runtime via edgeConfigSource(); you mutate via the
bq-flags CLI shipped in the package. Propagation is sub-second.
{
"new-checkout": { "on": true, "rollout": 0.5 },
"ai-suggestions": { "on": true, "users": ["u_john", "u_beta1"] },
"kill-old-flow": { "on": false }
}
Detect current state
Before doing anything, figure out which path you're on:
| Signal | What it means | Where to start |
|---|---|---|
bq-analytics in package.json deps and EDGE_CONFIG= in .env.local |
fully wired | jump to Operate |
bq-analytics in deps, no EDGE_CONFIG= |
events wired, flags not yet | Step 1 below |
| neither | greenfield | run /bq-analytics-install first; it handles flags as Phase 3 |
grep -E '"bq-analytics"' package.json && echo "sdk present"
grep -E '^EDGE_CONFIG=' .env.local 2>/dev/null && echo "edge config provisioned"
Step 1 — Add the flag dependency
pnpm add bq-analytics @vercel/edge-config
# or: npm i / yarn add
@vercel/edge-config is an optional peer of bq-analytics. Only needed
when using edgeConfigSource() server-side; browser/RN clients use
httpSource() and don't need it.
Step 2 — Provision Edge Config
./node_modules/bq-analytics/scripts/setup-edge-config.sh
What it does (idempotent — safe to re-run):
vercel linkif not already linked- Creates an Edge Config store named
bq-analytics-flags(or reuses one with that slug) - Initializes the
flagskey as{} - Mints a read token, pushes
EDGE_CONFIGenv var to Vercel Production vercel env pull .env.local --environment productionso local dev can read it
Note: only Production is set automatically (CLI quirk on preview/dev
envs). For full env propagation, set VERCEL_TOKEN first, or replicate
via the Vercel dashboard.
After this, bq-flags is available via pnpm exec bq-flags (or
./node_modules/.bin/bq-flags).
Step 3 — Wire the SDK
Pick the surfaces relevant to the project. Skip any that don't apply.
Server (Next.js / Hono / Node on Vercel)
Drop a singleton next to your existing analytics one:
// src/lib/flags.ts
import { Flags } from "bq-analytics";
import { edgeConfigSource } from "bq-analytics/edge-config";
import { analytics } from "./analytics";
declare global { var __bqf: Flags | undefined; }
export function flags() {
return globalThis.__bqf ??= new Flags({
source: edgeConfigSource(),
analytics: analytics(), // emits "$flag_called" exposures
refreshIntervalMs: 60_000, // pull updates every 60s
});
}
Use anywhere on the server:
import { flags } from "@/lib/flags";
await flags().ready();
if (flags().isOn("new-checkout", userId)) { /* new flow */ }
Browser / Expo / RN — never expose the Edge Config token
Drop a /api/flags route on the server first (one line):
// src/app/api/flags/route.ts (Next.js App Router)
// Subpath import — only this entry resolves `@vercel/edge-config`,
// so the base `bq-analytics/next` bundle stays edge-config-free.
import { createFlagsRoute } from "bq-analytics/next/flags";
export const GET = createFlagsRoute({
resolveUser: async (req) => /* same auth as /api/track */ null,
filter: (flags) => Object.fromEntries( // strip allowlists
Object.entries(flags).map(([k, v]) => [k, { ...v, users: undefined }]),
),
});
Then on the client:
// browser
import { Flags, httpSource } from "bq-analytics";
const flags = new Flags({ source: httpSource({ url: "/api/flags" }) });
await flags.ready();
flags.isOn("new-checkout", userId);
// Expo / RN — point at your hosted API
const flags = new Flags({
source: httpSource({
url: `${API_URL}/api/flags`,
headers: { authorization: `Bearer ${deviceToken}` },
}),
});
Verify the install
pnpm exec bq-flags on smoke-test --rollout 100%
pnpm exec bq-flags list # shows smoke-test row
pnpm exec bq-flags delete smoke-test
Then in code, gate something on a flag, redeploy, and confirm exposures land in BigQuery:
bq query --nouse_legacy_sql --format=pretty \
"SELECT * FROM \`<gcp>.events.raw\` WHERE event_name = '\$flag_called' ORDER BY ts DESC LIMIT 5"
Operate
bq-flags list # current state
bq-flags get new-checkout # one flag's JSON
bq-flags raw # full flags object
bq-flags on new-checkout --rollout 25% # create / turn on
bq-flags on new-checkout --rollout 50% --users u_a,u_b
bq-flags rollout new-checkout 100% # ramp to everyone
bq-flags off new-checkout # kill switch
bq-flags allow ai-suggestions u_alice u_bob # add to allowlist
bq-flags disallow ai-suggestions u_alice # remove
bq-flags delete ai-suggestions # remove flag entirely
--rollout accepts 0..1, 0..100, or N%. Allowlists are deduped.
All mutations are read-modify-write under the hood; safe to interleave.
Materializing a cohort from BigQuery
When the user asks "turn on ai-suggestions for all Pro users who imported
3+ videos":
Translate to BigQuery against
events.users/events.raw. Always return a dedupeduser_id[]:SELECT user_id FROM `proj.events.users` u JOIN ( SELECT user_id, COUNT(*) c FROM `proj.events.raw` WHERE event_name = 'import.completed' GROUP BY 1 HAVING c >= 3 ) i USING (user_id) WHERE JSON_VALUE(u.traits, '$.plan') = 'pro'Run via the
bq-analytics-queryskill orbq query --format=csv, capture the user IDs, then pipe intobq-flags allow:USERS=$(bq query --nouse_legacy_sql --format=csv --quiet "<query>" \ | tail -n +2 | tr '\n' ' ') # Restrict to allowlist only: bq-flags on ai-suggestions --rollout 0 bq-flags allow ai-suggestions $USERS--rollout 0means "nobody by default";allowadds the cohort.
Static lists go stale. Re-run on cadence (cron / /loop / weekly).
Evaluating impact
The SDK auto-emits $flag_called exposure events to events.raw. The CLI
runs the standard analysis:
bq-flags eval new-checkout # coverage only
bq-flags eval new-checkout --outcome subscription.started # adds lift
bq-flags eval new-checkout --outcome subscription.started --days 30
This prints (variant, users, exposures) coverage and (with --outcome)
(variant_on, users, conversions, rate_pct) for first-exposure ITT lift.
Compute lift = (on_rate - off_rate) / off_rate. Flag borderline results
to the user before adding sequential / chi-square stats.
If bq-flags eval complains about GCP_PROJECT_ID, pass it:
bq-flags eval new-checkout --project my-gcp-project --dataset events
Where flags evaluate
| Runtime | Pattern | Source |
|---|---|---|
| Next.js / Hono / Node server | direct read from Edge Config | edgeConfigSource() |
| Node CLI | direct read | edgeConfigSource({ connectionString: process.env.EDGE_CONFIG }) |
| Browser (Next.js client) | fetch from /api/flags route |
httpSource({ url: '/api/flags' }) |
| React Native / Expo | fetch from your API server | httpSource({ url: \${API_URL}/api/flags`, headers: ... })` |
Server code consumes Edge Config directly (no HTTP). Browser/RN MUST go
through your own /api/flags route — never expose the EDGE_CONFIG token
to clients. The route is one line:
// src/app/api/flags/route.ts
export { createFlagsRoute as GET } from "bq-analytics/next/flags";
Pass resolveUser to authenticate, or filter to strip users[]
allowlists before they hit the wire.
Removing the Edge Config (full teardown)
Skip unless the user explicitly asks. Destructive.
EC_ID=$(grep '^EDGE_CONFIG=' .env.local | sed -E 's|.*/(ecfg_[^?]+)\?.*|\1|')
vercel edge-config remove "$EC_ID"
vercel env rm EDGE_CONFIG production
Notes
- 512 KB total store cap, 8 KB per-item — don't put more than ~20k user IDs in a single allowlist. Past that, materialise in a separate flag per cohort or move to attribute-based gating in code.
- Exposure event name is
$flag_calledunless overridden. Match in SQL. - Edge Config writes propagate in <1s globally. Reads take 8–15ms warm.