Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ecebdb8
feat(flags): add rules engine boundary
btthomas Jul 24, 2026
faa43dc
fix(flags): align rules boundary with upstream
btthomas Jul 27, 2026
a6264ea
fix(flags): refresh upstream compatibility TODOs
btthomas Jul 28, 2026
7c40f7c
test(flags): enforce portable wire boundary
btthomas Jul 28, 2026
fb3020d
docs(flags): align parser migration TODOs
btthomas Jul 29, 2026
e66a244
fix(flags): preserve invalid rules errors
btthomas Jul 29, 2026
521a0cf
fix(flags): reject unsafe rules integers
btthomas Jul 29, 2026
ddc0876
fix(flags): align compatibility with upstream rules
btthomas Jul 30, 2026
55f897b
test(flags): preserve unknown rules fields
btthomas Jul 31, 2026
6171275
docs(flags): refresh capability migration TODOs
btthomas Aug 3, 2026
33e391b
fix(openfeature): treat empty contexts literally
btthomas Aug 5, 2026
e392ed4
chore(openfeature): track upstream context helper
btthomas Aug 7, 2026
753c26a
docs(openfeature): clarify helper migration
btthomas Aug 7, 2026
4c65287
docs(flags): preserve core parser boundary
btthomas Aug 3, 2026
fa16903
fix(flags): preserve configuration parse errors
btthomas Aug 7, 2026
b6ca8ea
docs(flags): refresh upstream TODO anchors
btthomas Aug 10, 2026
a8a8d67
docs(flags): correct BigInt TODO
btthomas Aug 10, 2026
a4f6bdd
docs(flags): refresh rebased upstream TODOs
btthomas Aug 11, 2026
0c4ed19
fix(flags): guard inherited rules context attributes
btthomas Aug 17, 2026
98b4fd3
docs(flags): align TODOs with final upstream contracts
btthomas Aug 20, 2026
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
7 changes: 3 additions & 4 deletions example-new-architecture/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,9 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
})();

function AppWithProviders() {
// No OpenFeature.setContext here on purpose: the offline precomputed configuration is a
// single-subject snapshot served against the context it was computed for (see the wire's
// embedded context in flags/). Setting a different runtime context would put the provider into
// the OpenFeature ERROR state and fall back to coded defaults.
// setFlagsProvider gets a supported copy of the precomputed context and sets it on OpenFeature
// before provider registration. A later different context puts the offline provider into ERROR
// and evaluations use their coded defaults.
return (
<Suspense
fallback={
Expand Down
12 changes: 9 additions & 3 deletions example-new-architecture/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
DatadogOpenFeatureProvider,
DatadogOfflineOpenFeatureProvider,
configurationFromString,
getPrecomputedContext,
} from '@datadog/mobile-react-native-openfeature';
import {OpenFeature} from '@openfeature/react-sdk';

Expand All @@ -24,12 +25,17 @@ export type FlagsSource = 'online' | 'offline';
*/
export const setFlagsProvider = async (source: FlagsSource): Promise<void> => {
if (source === 'offline') {
const configuration = configurationFromString(buildSampleWire());
const context = getPrecomputedContext(configuration);

if (context !== undefined) {
await OpenFeature.setContext(context);
}

const provider = new DatadogOfflineOpenFeatureProvider({
clientName: 'offline',
});
provider.setConfiguration(
configurationFromString(buildSampleWire()),
);
provider.setConfiguration(configuration);
await OpenFeature.setProviderAndWait(provider);
return;
}
Expand Down
16 changes: 12 additions & 4 deletions example/src/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
DatadogOpenFeatureProvider,
DatadogOfflineOpenFeatureProvider,
configurationFromString
configurationFromString,
getPrecomputedContext
} from '@datadog/mobile-react-native-openfeature';
import { OpenFeature } from '@openfeature/react-sdk';

Expand All @@ -28,12 +29,19 @@ export const setFlagsProvider = async (
offlineContext?: OfflineWireContext
): Promise<void> => {
if (source === 'offline') {
const configuration = configurationFromString(
buildSampleWire(offlineContext)
);
const context = getPrecomputedContext(configuration);

if (context !== undefined) {
await OpenFeature.setContext(context);
}

const provider = new DatadogOfflineOpenFeatureProvider({
clientName: 'offline'
});
provider.setConfiguration(
configurationFromString(buildSampleWire(offlineContext))
);
provider.setConfiguration(configuration);
await OpenFeature.setProviderAndWait(provider);
return;
}
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/flags/FlagsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,10 @@ export class FlagsClient {
/**
* Clear any externally-set evaluation context and reconcile.
*
* This is the offline counterpart to clearing/omitting an OpenFeature context: it drops the
* external override so a loaded precomputed configuration is served against **its embedded
* context** again. Clearing the override (rather than skipping) matters so that a
* configuration loaded *after* a clear is not judged against a stale override. With no
* configuration loaded the result is `PROVIDER_NOT_READY`.
* This is an explicit low-level Datadog reset operation. It drops the external override so a
* loaded precomputed configuration is served against **its embedded context** again. It does
* not represent OpenFeature `clearContext()`, which supplies the resulting effective context
* to a provider. With no configuration loaded the result is `PROVIDER_NOT_READY`.
*/
resetEvaluationContextWithoutFetching = (): ConfigurationResult => {
this.externalContext = undefined;
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/flags/__tests__/FlagsClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,33 @@ describe('FlagsClient', () => {
).not.toHaveBeenCalled();
});

it('stores an empty context as an explicit override', () => {
const flagsClient = DdFlags.getClient();
flagsClient.setConfiguration(
buildConfig(offlineFlags, { targetingKey: 'user-1' })
);

const result = flagsClient.setEvaluationContextWithoutFetching({
attributes: {}
} as never);

expect(result).toEqual({
status: 'error',
errorCode: 'INVALID_CONTEXT'
});

// Reloading the snapshot reconciles against the stored empty override. It does not
// silently restore the snapshot's embedded user-1 context.
expect(
flagsClient.setConfiguration(
buildConfig(offlineFlags, { targetingKey: 'user-1' })
)
).toEqual({
status: 'error',
errorCode: 'INVALID_CONTEXT'
});
});

it('recovers to ready when a matching context is set after a mismatch', () => {
const flagsClient = DdFlags.getClient();
flagsClient.setConfiguration(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://github.com/DataDog).
* Copyright 2016-Present Datadog, Inc.
*/

import { OperatorType } from '@datadog/flagging-core';
import type { UniversalFlagConfigurationV1 } from '@datadog/flagging-core';

import type {
RulesEngine,
RulesEvaluationDetails,
RulesEvaluationRequest,
RulesValueType
} from '../../rules';

export const buildRulesConfiguration = (): UniversalFlagConfigurationV1 => ({
createdAt: '2026-07-23T12:00:00.000Z',
format: 'SERVER',
environment: { name: 'test' },
flags: {
'dynamic-flag': {
key: 'dynamic-flag',
enabled: true,
variationType: 'BOOLEAN',
variations: {
enabled: { key: 'enabled', value: true },
disabled: { key: 'disabled', value: false }
},
allocations: [
{
key: 'allocation-1',
rules: [
{
conditions: [
{
operator: OperatorType.ONE_OF,
attribute: 'country',
value: ['US']
}
]
}
],
splits: [
{
variationKey: 'enabled',
serialId: 7,
shards: [
{
salt: 'test-salt',
ranges: [{ start: 0, end: 100 }],
totalShards: 100
}
]
}
],
doLog: false
}
]
}
}
});

type FakeRulesEvaluation = RulesEvaluationDetails<unknown>;

export interface FakeRulesEngine extends RulesEngine {
evaluate: jest.Mock<
FakeRulesEvaluation,
[RulesEvaluationRequest<RulesValueType>]
>;
}

// Client tests use this fake to control evaluation independently of the
// flagging-core implementation and its canonical integration vectors.
export const createFakeRulesEngine = (
result: FakeRulesEvaluation
): FakeRulesEngine => {
return {
evaluate: jest.fn(() => result)
} as FakeRulesEngine;
};
12 changes: 10 additions & 2 deletions packages/core/src/flags/configuration/__tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ describe('normalizeWireContext', () => {
});
});

it('defaults a missing targeting key to an empty string', () => {
it('preserves a missing targeting key', () => {
expect(normalizeWireContext({ country: 'US' })).toEqual({
targetingKey: '',
targetingKey: undefined,
attributes: { country: 'US' }
});
});
Expand Down Expand Up @@ -80,6 +80,14 @@ describe('contextMatchesConfiguration', () => {
).toBe(true);
});

it('matches empty contexts without inventing a targeting key', () => {
expect(
contextMatchesConfiguration({}, {
attributes: {}
} as EvaluationContext)
).toBe(true);
});

it('does not match a different targeting key', () => {
expect(
contextMatchesConfiguration(
Expand Down
Loading