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
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,19 @@ private fun convertUnparsedFlagToMap(
}

// Return a [Map] as an intermediate because it is easier to use; we can convert it to WritableMap right before sending to React Native.
return mapOf(
"key" to flagKey,
"value" to (parsedValue ?: flag.variationValue),
"allocationKey" to flag.allocationKey,
"variationKey" to flag.variationKey,
"variationType" to flag.variationType,
"variationValue" to flag.variationValue,
"reason" to flag.reason,
"doLog" to flag.doLog,
"extraLogging" to flag.extraLogging.toMap(),
return buildMap {
put("key", flagKey)
put("value", parsedValue ?: flag.variationValue)
put("allocationKey", flag.allocationKey)
put("variationKey", flag.variationKey)
put("variationType", flag.variationType)
put("variationValue", flag.variationValue)
put("reason", flag.reason)
put("doLog", flag.doLog)
put("extraLogging", flag.extraLogging.toMap())
// Serialized as a String because the React Native bridge converts Long values to Double
SERIAL_ID_KEY to flag.serialId?.toString()
)
flag.serialId?.let { put(SERIAL_ID_KEY, it.toString()) }
}
}

@Suppress("UNCHECKED_CAST")
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
}
},
"dependencies": {
"@datadog/flagging-core": "~2.0.1",
"@datadog/flagging-core": "~2.1.0",
"big-integer": "^1.6.52"
}
}
78 changes: 78 additions & 0 deletions packages/core/src/flags/__tests__/FlagsClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,4 +913,82 @@ describe('FlagsClient', () => {
).toEqual({ status: 'error', errorCode: 'PROVIDER_NOT_READY' });
});
});

describe('serial id', () => {
const trackedFlag = () =>
(NativeModules.DdFlags.trackEvaluation as jest.Mock).mock
.calls[0][2];

it('hands the serial id to native exposure tracking on the offline path', () => {
const flagsClient = DdFlags.getClient();

flagsClient.setConfiguration(
buildConfig({
'offline-bool': {
...offlineFlags['offline-bool'],
serialId: 340132
}
})
);
flagsClient.getBooleanValue('offline-bool', false);

expect(trackedFlag().serialId).toBe('340132');
});

it('hands serial id 0 to native exposure tracking', () => {
const flagsClient = DdFlags.getClient();

flagsClient.setConfiguration(
buildConfig({
'offline-bool': {
...offlineFlags['offline-bool'],
serialId: 0
}
})
);
flagsClient.getBooleanValue('offline-bool', false);

expect(trackedFlag().serialId).toBe('0');
});

it('sends no serial id key when the configuration carries none', () => {
const flagsClient = DdFlags.getClient();

flagsClient.setConfiguration(buildConfig(offlineFlags));
flagsClient.getBooleanValue('offline-bool', false);

expect(JSON.stringify(trackedFlag())).not.toContain('serialId');
});

it('passes through the serial id from a native snapshot on the online path', async () => {
// The native snapshot is cached verbatim, so a field the bridge adds reaches
// trackEvaluation without the JS layer naming it.
jest.spyOn(
NativeModules.DdFlags,
'setEvaluationContext'
).mockResolvedValueOnce({
'test-boolean-flag': {
key: 'test-boolean-flag',
value: true,
allocationKey: '',
variationKey: 'true',
reason: 'STATIC',
doLog: true,
variationType: '',
variationValue: '',
extraLogging: {},
serialId: '340132'
}
});

const flagsClient = DdFlags.getClient();
await flagsClient.setEvaluationContext({
targetingKey: 'user-1',
attributes: {}
});
flagsClient.getBooleanValue('test-boolean-flag', false);

expect(trackedFlag().serialId).toBe('340132');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,77 @@ describe('decodePrecomputedFlags', () => {
expect(cache.get('f')?.extraLogging).toEqual({});
});

it('tolerates a null serialId', () => {
const cache = decodePrecomputedFlags(
responseWith({ f: flag({ serialId: null }) })
describe('serialId', () => {
it('propagates a serial id as its string form', () => {
const cache = decodePrecomputedFlags(
responseWith({ f: flag({ serialId: 340132 }) })
);

expect(cache.get('f')?.serialId).toBe('340132');
});

it('propagates serial id 0', () => {
// Serial ids are zero-based per org, so 0 is the most common value in the fleet
// and the one a truthiness check would silently drop.
const cache = decodePrecomputedFlags(
responseWith({ f: flag({ serialId: 0 }) })
);

expect(cache.get('f')?.serialId).toBe('0');
});

it.each([
['null', null],
['undefined', undefined],
['a string', '340132'],
['a boolean', true],
['NaN', NaN],
['Infinity', Infinity]
])(
'omits the key entirely and keeps the flag when the serial id is %s',
(_label, serialId) => {
const cache = decodePrecomputedFlags(
responseWith({
f: flag({
serialId: (serialId as unknown) as PrecomputedFlag['serialId']
})
})
);

const entry = cache.get('f');

// The flag itself must still decode: a bad serial id binds to the serial id,
// never to the flag (nor to its siblings).
expect(entry?.key).toBe('f');
expect(entry?.serialId).toBeUndefined();
expect('serialId' in (entry as object)).toBe(false);
Comment thread
danyal002 marked this conversation as resolved.
}
);

expect(cache.get('f')?.key).toBe('f');
it('keeps a sibling flag decodable when one has a malformed serial id', () => {
const cache = decodePrecomputedFlags(
responseWith({
good: flag({ serialId: 7 }),
bad: flag({
serialId: ('x' as unknown) as PrecomputedFlag['serialId']
})
})
);

expect(cache.get('good')?.serialId).toBe('7');
expect(cache.get('bad')?.key).toBe('bad');
expect(cache.get('bad')?.serialId).toBeUndefined();
});

it('omits the key from the object sent across the bridge, not just the value', () => {
// The bridge serializes the entry; a present-but-undefined property and an absent
// one are indistinguishable on the object but not on the wire.
const cache = decodePrecomputedFlags(
responseWith({ f: flag({ serialId: null }) })
);

expect(JSON.stringify(cache.get('f'))).not.toContain('serialId');
});
});

it('omits flags with an unsupported variation type and logs a warning', () => {
Expand Down
14 changes: 9 additions & 5 deletions packages/core/src/flags/configuration/precomputed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ const toFlagCacheEntry = (
allocationKey,
reason,
doLog,
extraLogging
extraLogging,
serialId
} = flag as Partial<PrecomputedFlag>;

if (
Expand Down Expand Up @@ -152,10 +153,7 @@ const toFlagCacheEntry = (
return null;
}

// `serialId` is intentionally not propagated: `FlagCacheEntry` has no slot for it
// and the native CDN-fetched snapshot omits it too, so dropping it keeps
// offline/online parity.
return {
const entry: FlagCacheEntry = {
key,
value: variationValue,
allocationKey,
Expand All @@ -166,6 +164,12 @@ const toFlagCacheEntry = (
doLog,
extraLogging: extraLogging ?? {}
};

if (typeof serialId === 'number' && Number.isFinite(serialId)) {
entry.serialId = String(serialId);
}
Comment thread
danyal002 marked this conversation as resolved.

return entry;
};

const valueMatchesVariationType = (
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/flags/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface FlagCacheEntry {
reason: string;
doLog: boolean;
extraLogging: Record<string, unknown>;
serialId?: string;
}

export const processEvaluationContext = (
Expand Down
18 changes: 5 additions & 13 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1980,20 +1980,12 @@ __metadata:
languageName: node
linkType: hard

"@datadog/flagging-core@npm:~2.0.1":
version: 2.0.1
resolution: "@datadog/flagging-core@npm:2.0.1"
"@datadog/flagging-core@npm:~2.1.0":
version: 2.1.0
resolution: "@datadog/flagging-core@npm:2.1.0"
dependencies:
"@datadog/js-core": 0.0.3
spark-md5: ^3.0.2
checksum: d4864a76be535acda9bbcdaa95718484f1b16842fba8591ba13308b988e043f7aafd002e240b2393349af616091b5cdb9a75325e24e82aeede8b34dc2bc7560b
languageName: node
linkType: hard

"@datadog/js-core@npm:0.0.3":
version: 0.0.3
resolution: "@datadog/js-core@npm:0.0.3"
checksum: c8d70cb21f0c5089fa9d3e8d54a9bc2fafde8927b9f9286721b5a12c82cf46b5ff2d39486ecd1c871e4d8a321d88ed5ad5d1cccc06659bce7b9c67c4eaac6695
checksum: 0bb9488ff29a059135804a688c1e929a3eea35a585510ce63480a84ad8d9f7e07eae6b84e18243b979fef79094cda71c94cec6492951a79f7a46aa03ec0e8739
languageName: node
linkType: hard

Expand Down Expand Up @@ -2138,7 +2130,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@datadog/mobile-react-native@workspace:packages/core"
dependencies:
"@datadog/flagging-core": ~2.0.1
"@datadog/flagging-core": ~2.1.0
"@testing-library/react-native": 7.0.2
big-integer: ^1.6.52
react-native-builder-bob: 0.26.0
Expand Down