Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-flags-environments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/flags-core': minor
---

Add an `environment` client option for associating evaluation metrics with an environment when sending them to the ingestion endpoint.
6 changes: 6 additions & 0 deletions packages/vercel-flags-core/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,16 @@ type ControllerOptions = {
stream?: boolean | { initTimeoutMs: number }; // default: true (3000ms)
polling?: boolean | { intervalMs: number; initTimeoutMs: number }; // default: true (30s interval, 3s timeout)
buildStep?: boolean; // Override build step auto-detection
environment?: string; // Environment attached to ingested evaluation metrics
sources?: { stream?: StreamSource; polling?: PollingSource; bundled?: BundledSource }; // DI for testing
};
```

When `environment` is provided, evaluation metrics sent to `/v1/ingest` include
it as `X-Vercel-Environment`. The option does not affect datafile or stream
requests, or the environment used for flag evaluation. When it is omitted, the
header is not sent.

### Data Source Priority (Fallback Chain)

Behavior differs based on environment:
Expand Down
13 changes: 13 additions & 0 deletions packages/vercel-flags-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ const result = await client.evaluate<boolean>('show-new-feature', false, {
});
```

To associate evaluation metrics with an environment, pass the `environment`
option:

```ts
const client = createClient(process.env.FLAGS!, {
environment: 'preview',
});
```

This option is sent only to the metrics ingestion endpoint. It does not select
the environment used for flag evaluation. When omitted, the environment header
is not sent.

## OpenFeature

An OpenFeature-compatible provider is available at `@vercel/flags-core/openfeature`:
Expand Down
51 changes: 51 additions & 0 deletions packages/vercel-flags-core/src/black-box.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,57 @@ describe('Controller (black-box)', () => {
});
});

// ---------------------------------------------------------------------------
// Environment option
// ---------------------------------------------------------------------------
describe('environment option', () => {
it('should include the environment with evaluation metrics', async () => {
const cleanupCtx = setRequestContext({ host: 'example.com' });
const client = createClient(sdkKey, {
datafile: makeBundled(),
environment: 'preview',
fetch: fetchMock,
stream: false,
polling: false,
});

await client.evaluate('flagA');
await client.shutdown();
cleanupCtx();

expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenLastCalledWith(
'https://flags.vercel.com/v1/ingest',
expect.objectContaining({
headers: {
...ingestRequestHeaders,
'X-Vercel-Environment': 'preview',
},
}),
);
});

it('should omit the environment from evaluation metrics when unspecified', async () => {
const cleanupCtx = setRequestContext({ host: 'example.com' });
const client = createClient(sdkKey, {
datafile: makeBundled(),
fetch: fetchMock,
stream: false,
polling: false,
});

await client.evaluate('flagA');
await client.shutdown();
cleanupCtx();

expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenLastCalledWith(
'https://flags.vercel.com/v1/ingest',
expect.objectContaining({ headers: ingestRequestHeaders }),
);
});
});

// ---------------------------------------------------------------------------
// Build step detection
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export type ControllerOptions = {
*/
fetch?: typeof globalThis.fetch;

/**
* Environment included with evaluation metrics sent to the ingest endpoint.
Comment thread
luismeyer marked this conversation as resolved.
* When omitted, the environment header is not sent.
*/
environment?: string;

/**
* Custom client name included in evaluation telemetry.
*/
Expand All @@ -73,6 +79,7 @@ export type NormalizedOptions = {
buildStep: boolean;
fetch: typeof globalThis.fetch;
host: string;
environment: string | undefined;
clientName: string | undefined;
disableMetrics: boolean;
};
Expand Down Expand Up @@ -124,6 +131,7 @@ export function normalizeOptions(
buildStep,
fetch: options.fetch ?? globalThis.fetch,
host: 'https://flags.vercel.com',
environment: options.environment,
clientName: options.clientName,
disableMetrics: options.disableMetrics ?? false,
};
Expand Down
4 changes: 4 additions & 0 deletions packages/vercel-flags-core/src/utils/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface IngestOptions {
auth: Auth;
host: string;
fetch: typeof fetch;
environment?: string;
}

async function getEvaluatingOidcToken(auth: Auth): Promise<string | undefined> {
Expand All @@ -55,6 +56,9 @@ async function getIngestHeaders(
Authorization: `Bearer ${token}`,
'User-Agent': `VercelFlagsCore/${version}`,
[FLUSH_REASON_HEADER]: flushReason,
...(options.environment
Comment thread
luismeyer marked this conversation as resolved.
Outdated
? { 'X-Vercel-Environment': options.environment }
: null),
...(process.env.VERCEL_ENV
? { 'X-Vercel-Env': process.env.VERCEL_ENV }
: null),
Expand Down
Loading