Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
236 changes: 236 additions & 0 deletions packages/core/src/flags/configuration/__tests__/precomputed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* 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://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

import { InternalLog } from '../../../InternalLog';
import {
UnsupportedConfigurationError,
decodePrecomputedFlags
} from '../precomputed';
import type {
PrecomputedConfigurationResponse,
PrecomputedFlag
} from '../types';

jest.mock('../../../InternalLog', () => {
return {
InternalLog: { log: jest.fn() },
DATADOG_MESSAGE_PREFIX: 'DATADOG:'
};
});

const flag = (overrides: Partial<PrecomputedFlag>): PrecomputedFlag => ({
variationType: 'boolean',
variationValue: true,
variationKey: 'true',
allocationKey: 'alloc-1',
reason: 'STATIC',
doLog: false,
extraLogging: {},
...overrides
});

const responseWith = (
flags: Record<string, PrecomputedFlag>,
obfuscated = false
): PrecomputedConfigurationResponse => ({
data: {
id: '2',
type: 'precomputed-assignments',
attributes: {
obfuscated,
createdAt: '2026-07-06T23:01:56.822171460Z',
format: 'PRECOMPUTED',
environment: { name: 'Staging' },
flags
}
}
});

describe('decodePrecomputedFlags', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('maps each variation type to a FlagCacheEntry with the correct value + string form', () => {
const cache = decodePrecomputedFlags(
responseWith({
bool: flag({
variationType: 'boolean',
variationValue: false,
variationKey: 'false'
}),
str: flag({
variationType: 'string',
variationValue: 'hello',
variationKey: 'Hello'
}),
num: flag({
variationType: 'number',
variationValue: 42,
variationKey: '42'
}),
int: flag({
variationType: 'integer',
variationValue: 7,
variationKey: '7'
}),
flt: flag({
variationType: 'float',
variationValue: 1.5,
variationKey: '1.5'
}),
obj: flag({
variationType: 'object',
variationValue: { greeting: 'hi' },
variationKey: 'Greeting'
})
})
);

expect(cache.bool).toEqual({
key: 'bool',
value: false,
allocationKey: 'alloc-1',
variationKey: 'false',
variationType: 'boolean',
variationValue: 'false',
reason: 'STATIC',
doLog: false,
extraLogging: {}
});
expect(cache.str.value).toBe('hello');
expect(cache.str.variationValue).toBe('hello');
expect(cache.num.value).toBe(42);
expect(cache.num.variationValue).toBe('42');
// integer/float keep their wire variationType but decode to a JS number.
expect(cache.int.value).toBe(7);
expect(cache.int.variationType).toBe('integer');
expect(cache.int.variationValue).toBe('7');
expect(cache.flt.value).toBe(1.5);
expect(cache.flt.variationType).toBe('float');
expect(cache.flt.variationValue).toBe('1.5');
// objects are JSON-encoded for the string form; value stays an object.
expect(cache.obj.value).toEqual({ greeting: 'hi' });
expect(cache.obj.variationValue).toBe('{"greeting":"hi"}');
});

it('uses the flag map key as the entry key', () => {
const cache = decodePrecomputedFlags(
responseWith({ 'my-feature': flag({}) })
);

expect(cache['my-feature'].key).toBe('my-feature');
});

it('defaults missing extraLogging to an empty object', () => {
const cache = decodePrecomputedFlags(
responseWith({ f: flag({ extraLogging: undefined }) })
);

expect(cache.f.extraLogging).toEqual({});
});

it('tolerates a null serialId', () => {
const cache = decodePrecomputedFlags(
responseWith({ f: flag({ serialId: null }) })
);

expect(cache.f.key).toBe('f');
});

it('omits flags with an unsupported variation type and logs a warning', () => {
const cache = decodePrecomputedFlags(
responseWith({
good: flag({}),
bad: flag({ variationType: 'timestamp' })
})
);

expect(cache.good).toBeDefined();
expect(cache.bad).toBeUndefined();
expect(InternalLog.log).toHaveBeenCalled();
});

it('omits flags whose value does not match their variation type', () => {
const cache = decodePrecomputedFlags(
responseWith({
mismatched: flag({
variationType: 'number',
variationValue: 'not-a-number'
})
})
);

expect(cache.mismatched).toBeUndefined();
expect(InternalLog.log).toHaveBeenCalled();
});

it('throws UnsupportedConfigurationError for an obfuscated response', () => {
expect(() =>
decodePrecomputedFlags(responseWith({ f: flag({}) }, true))
).toThrow(UnsupportedConfigurationError);
});

it('returns an empty map when there are no flags', () => {
expect(decodePrecomputedFlags(responseWith({}))).toEqual({});
});

it('omits an integer flag with a fractional value', () => {
const cache = decodePrecomputedFlags(
responseWith({
frac: flag({ variationType: 'integer', variationValue: 7.9 })
})
);

expect(cache.frac).toBeUndefined();
expect(InternalLog.log).toHaveBeenCalled();
});

it('omits a number flag whose value is not finite', () => {
const cache = decodePrecomputedFlags(
responseWith({
inf: flag({ variationType: 'number', variationValue: Infinity })
})
);

expect(cache.inf).toBeUndefined();
});

it('returns an empty map for a structurally broken response', () => {
expect(
decodePrecomputedFlags(
({} as unknown) as Parameters<typeof decodePrecomputedFlags>[0]
)
).toEqual({});
});

it('rejects an array value for an object flag', () => {
const cache = decodePrecomputedFlags(
responseWith({
arr: flag({
variationType: 'object',
variationValue: [1, 2, 3]
})
})
);

expect(cache.arr).toBeUndefined();
expect(InternalLog.log).toHaveBeenCalled();
});

it('stores a flag keyed "__proto__" as data without polluting the prototype', () => {
// Computed key mirrors how JSON.parse yields an own "__proto__" property.
const cache = decodePrecomputedFlags(
responseWith({ ['__proto__']: flag({ variationValue: true }) })
);

// Stored as an own data property, not via the Object.prototype setter.
expect(Object.getPrototypeOf(cache)).toBe(Object.prototype);
expect(Object.keys(cache)).toContain('__proto__');
// No global prototype pollution.
expect(({} as Record<string, unknown>).variationType).toBeUndefined();
});
});
137 changes: 137 additions & 0 deletions packages/core/src/flags/configuration/__tests__/wire.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

import type { ParsedFlagsConfiguration } from '../types';
import { configurationFromString, configurationToString } from '../wire';

const buildResponse = () => ({
data: {
id: '2',
type: 'precomputed-assignments',
attributes: {
obfuscated: false,
createdAt: '2026-07-06T23:01:56.822171460Z',
format: 'PRECOMPUTED',
environment: { name: 'Staging' },
flags: {
'a-flag': {
variationType: 'boolean',
variationValue: true,
variationKey: 'true',
allocationKey: 'alloc-1',
reason: 'STATIC',
doLog: false,
extraLogging: {}
},
'num-flag': {
variationType: 'number',
variationValue: 1.5,
variationKey: '1.5',
allocationKey: 'alloc-2',
reason: 'STATIC',
doLog: true,
extraLogging: {}
},
'obj-flag': {
variationType: 'object',
variationValue: { nested: { a: 1 }, list: [1, 2] },
variationKey: 'obj',
allocationKey: 'alloc-3',
reason: 'TARGETING_MATCH',
doLog: false,
extraLogging: { extra: 'x' }
}
}
}
}
});

const buildWire = (overrides: Record<string, unknown> = {}) =>
JSON.stringify({
version: 1,
precomputed: {
response: JSON.stringify(buildResponse()),
context: { targetingKey: 'user-1', country: 'US' },
fetchedAt: 1748449320785
},
...overrides
});

describe('configurationFromString', () => {
it('parses a valid v1 wire with a precomputed branch', () => {
const config = configurationFromString(buildWire());

expect(config.precomputed).toBeDefined();
expect(config.precomputed?.context).toEqual({
targetingKey: 'user-1',
country: 'US'
});
expect(config.precomputed?.fetchedAt).toBe(1748449320785);
// The inner `response` string is parsed into an object.
expect(
config.precomputed?.response.data.attributes.flags['a-flag']
.variationValue
).toBe(true);
});

it('returns an empty config for an unsupported version', () => {
const wire = JSON.stringify({
version: 2,
precomputed: { response: JSON.stringify(buildResponse()) }
});

expect(configurationFromString(wire)).toEqual({});
});

it('returns an empty config for invalid JSON', () => {
expect(configurationFromString('not json')).toEqual({});
});

it('returns an empty config when the inner response is invalid JSON', () => {
const wire = JSON.stringify({
version: 1,
precomputed: { response: '{ not json' }
});

expect(configurationFromString(wire)).toEqual({});
});

it('returns a config with no precomputed branch when none is present', () => {
const wire = JSON.stringify({ version: 1 });

expect(configurationFromString(wire)).toEqual({});
});

it('does not populate a precomputed branch from a server-only wire (MVP)', () => {
const wire = JSON.stringify({
version: 1,
server: { response: '{}' }
});

const config = configurationFromString(wire);
expect(config.precomputed).toBeUndefined();
});
});

describe('configurationToString round-trip', () => {
it('round-trips a precomputed configuration', () => {
const original = configurationFromString(buildWire());

const restored = configurationFromString(
configurationToString(original)
);

expect(restored).toEqual(original);
});

it('serializes an empty configuration to a v1 wire', () => {
const empty: ParsedFlagsConfiguration = {};

expect(configurationToString(empty)).toBe(
JSON.stringify({ version: 1 })
);
});
});
23 changes: 23 additions & 0 deletions packages/core/src/flags/configuration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

// Internal module boundary for portable-configuration handling. Intentionally NOT
// re-exported from the package's public entry point (`packages/core/src/index.tsx`)
// until the exports step (FFL-2690). Keeping the surface behind this boundary makes a
// future "port -> depend on a shared core" swap contained.

export { configurationFromString, configurationToString } from './wire';
export {
decodePrecomputedFlags,
UnsupportedConfigurationError
} from './precomputed';
export type {
ParsedFlagsConfiguration,
ParsedPrecomputedConfiguration,
PrecomputedConfigurationResponse,
PrecomputedFlag,
WireEvaluationContext
} from './types';
Loading