diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 8a07a0d59..835c0c5b7 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -26,6 +26,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ submodules: recursive
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 82b2bbb44..8ed65827d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -12,6 +12,8 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ submodules: recursive
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24.x'
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 36485cb44..66edaf4ab 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,6 +5,7 @@ include:
variables:
GIT_DEPTH: 5
+ GIT_SUBMODULE_STRATEGY: recursive
DEVELOP_BRANCH: 'develop'
ANDROID_SDK_VERSION: 'commandlinetools-mac-11076708_latest'
EMULATOR_NAME: 'android_emulator'
@@ -92,6 +93,7 @@ test:native-android:
- (cd packages/core/android && ./gradlew build -PDdSdkReactNative_minSdkVersion=24)
- (cd packages/react-native-session-replay/android && ./gradlew build -PDdSdkReactNative_minSdkVersion=24 -PDatadogSDKReactNativeSessionReplay_minSdkVersion=24)
- (cd packages/internal-testing-tools/android && ./gradlew build -PDdSdkReactNative_minSdkVersion=24 -PDatadogInternalTesting_minSdkVersion=24)
+ - ./example-new-architecture/scripts/native-ffe-offline-android-smoke.sh
test:native-ios:
tags: ['macos:sonoma', 'specific:true']
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..c1300d309
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "packages/core/src/flags/__fixtures__/ffe-system-test-data"]
+ path = packages/core/src/flags/__fixtures__/ffe-system-test-data
+ url = https://github.com/DataDog/ffe-system-test-data.git
diff --git a/example-new-architecture/.gitignore b/example-new-architecture/.gitignore
index 2d6734832..f787b9bd7 100644
--- a/example-new-architecture/.gitignore
+++ b/example-new-architecture/.gitignore
@@ -64,6 +64,7 @@ yarn-error.log
# Secrets
ddCredentials.*
+!ddCredentials.example.js
# Yarn ignored files
.pnp.*
diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx
index 190667007..07d0340f1 100644
--- a/example-new-architecture/App.tsx
+++ b/example-new-architecture/App.tsx
@@ -9,19 +9,12 @@ import {
DdLogs,
DdTrace,
TrackingConsent,
- DdFlags,
PropagatorType,
} from '@datadog/mobile-react-native';
-import {DatadogOpenFeatureProvider} from '@datadog/mobile-react-native-openfeature';
-import {
- OpenFeature,
- OpenFeatureProvider,
- useObjectFlagDetails,
-} from '@openfeature/react-sdk';
-import React, {Suspense} from 'react';
+import React from 'react';
import type {PropsWithChildren} from 'react';
import {
- ActivityIndicator,
+ Pressable,
SafeAreaView,
ScrollView,
StatusBar,
@@ -38,10 +31,14 @@ import {
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
-// @ts-ignore
-import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
+import * as ddCredentials from './ddCredentials.example';
+import {runNativeFfeOfflineFixtureCorpus} from './nativeFfeOfflineFixtureRunner';
+
+const APPLICATION_ID = ddCredentials.APPLICATION_ID;
+const CLIENT_TOKEN = ddCredentials.CLIENT_TOKEN;
+const ENVIRONMENT = ddCredentials.ENVIRONMENT;
-(async () => {
+const datadogInitialization = (async () => {
const config = new CoreConfiguration(
CLIENT_TOKEN,
ENVIRONMENT,
@@ -56,14 +53,19 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
sessionSampleRate: 100,
telemetrySampleRate: 100,
nativeCrashReportEnabled: true,
- firstPartyHosts: [{
- match: "example.com",
- propagatorTypes: [PropagatorType.B3MULTI, PropagatorType.TRACECONTEXT]
- }]
+ firstPartyHosts: [
+ {
+ match: 'example.com',
+ propagatorTypes: [
+ PropagatorType.B3MULTI,
+ PropagatorType.TRACECONTEXT,
+ ],
+ },
+ ],
},
logsConfiguration: {},
- traceConfiguration: {}
- }
+ traceConfiguration: {},
+ },
);
config.verbosity = SdkVerbosity.DEBUG;
config.uploadFrequency = UploadFrequency.FREQUENT;
@@ -72,13 +74,6 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
// Initialize the Datadog SDK.
await DdSdkReactNative.initialize(config);
- // Enable Datadog Flags feature.
- await DdFlags.enable();
-
- // Set the provider with OpenFeature.
- const provider = new DatadogOpenFeatureProvider();
- OpenFeature.setProvider(provider);
-
// Datadog SDK usage examples.
await DdRum.startView('main', 'Main');
setTimeout(async () => {
@@ -92,38 +87,7 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
await DdTrace.finishSpan(spanId);
})();
-function AppWithProviders() {
- React.useEffect(() => {
- const user = {
- id: 'user-123',
- favoriteFruit: 'apple',
- };
-
- OpenFeature.setContext({
- targetingKey: user.id,
- favoriteFruit: user.favoriteFruit,
- });
- }, []);
-
- return (
-
-
-
- }>
-
-
-
-
- );
-}
-
function App(): React.JSX.Element {
- const greetingFlag = useObjectFlagDetails('rn-sdk-test-json-flag', {
- greeting: 'Default greeting',
- });
-
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
@@ -135,20 +99,16 @@ function App(): React.JSX.Element {
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
-
+
-
-
- The title of this section is based on the{' '}
- {greetingFlag.flagKey} feature
- flag.{'\n\n'}
- If it's different from "Default greeting", then it is coming from
- the feature flag evaluation.{'\n\n'}
- Evaluation reason is {greetingFlag.reason}.{'\n\n'}Inspect greetingFlag in{' '}
- App.tsx for more evaluation
- details.
-
+
+
Edit App.tsx to change this
@@ -170,6 +130,111 @@ function App(): React.JSX.Element {
);
}
+type NativeFfeFlowState = {
+ status: 'idle' | 'loading' | 'ready' | 'error';
+ summary: string;
+ details?: string;
+};
+
+function NativeFfeFlowPanel({
+ isDarkMode,
+}: {
+ isDarkMode: boolean;
+}): React.JSX.Element {
+ const [flowState, setFlowState] = React.useState({
+ status: 'idle',
+ summary: 'Native FF&E flow has not run yet.',
+ });
+ const loading = flowState.status === 'loading';
+
+ const runNativeFfeFlow = React.useCallback(async () => {
+ setFlowState({
+ status: 'loading',
+ summary: 'Running native FF&E offline fixture corpus...',
+ });
+
+ try {
+ await datadogInitialization;
+
+ const report = await runNativeFfeOfflineFixtureCorpus();
+
+ setFlowState({
+ status: 'ready',
+ summary: report.summary,
+ details: JSON.stringify(report.details, null, 2),
+ });
+ } catch (error) {
+ setFlowState({
+ status: 'error',
+ summary:
+ error instanceof Error ? error.message : 'Native FF&E flow failed.',
+ });
+ }
+ }, []);
+
+ const autoRunStarted = React.useRef(false);
+ React.useEffect(() => {
+ if (autoRunStarted.current) {
+ return;
+ }
+ autoRunStarted.current = true;
+ void runNativeFfeFlow();
+ }, [runNativeFfeFlow]);
+
+ return (
+
+
+ Native FFE flow
+
+
+ {flowState.summary}
+
+ [
+ styles.nativeFfeButton,
+ loading && styles.nativeFfeButtonDisabled,
+ pressed && !loading && styles.nativeFfeButtonPressed,
+ ]}
+ >
+
+ {loading ? 'Running...' : 'Run offline fixture corpus'}
+
+
+ {flowState.details ? (
+
+ {flowState.details}
+
+ ) : null}
+
+ );
+}
+
type SectionProps = PropsWithChildren<{
title: string;
}>;
@@ -184,7 +249,8 @@ function Section({children, title}: SectionProps): React.JSX.Element {
{
color: isDarkMode ? Colors.white : Colors.black,
},
- ]}>
+ ]}
+ >
{title}
+ ]}
+ >
{children}
@@ -217,6 +284,35 @@ const styles = StyleSheet.create({
highlight: {
fontWeight: '700',
},
+ nativeFfeButton: {
+ alignItems: 'center',
+ alignSelf: 'flex-start',
+ backgroundColor: '#2563eb',
+ borderRadius: 6,
+ marginTop: 16,
+ minHeight: 44,
+ justifyContent: 'center',
+ paddingHorizontal: 16,
+ },
+ nativeFfeButtonDisabled: {
+ opacity: 0.6,
+ },
+ nativeFfeButtonPressed: {
+ backgroundColor: '#1d4ed8',
+ },
+ nativeFfeButtonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ nativeFfeDetails: {
+ borderRadius: 6,
+ fontFamily: 'Menlo',
+ fontSize: 12,
+ lineHeight: 16,
+ marginTop: 12,
+ padding: 12,
+ },
});
-export default AppWithProviders;
+export default App;
diff --git a/example-new-architecture/README.md b/example-new-architecture/README.md
new file mode 100644
index 000000000..63f4c459e
--- /dev/null
+++ b/example-new-architecture/README.md
@@ -0,0 +1,32 @@
+# New Architecture Example
+
+## Datadog Credentials
+
+The example imports `ddCredentials.example.js`, which contains fake placeholder
+values. That is enough for the offline native FF&E fixture demo and for CI,
+because the demo passes a bundled rules configuration JSON file through the
+React Native bridge and does not make a feature flag network request.
+
+To point the example at a real staging app for manual SDK/RUM validation, edit
+the values in `ddCredentials.example.js` locally:
+
+- `CLIENT_TOKEN`: Datadog public client token for SDK initialization.
+- `APPLICATION_ID`: RUM application ID. The placeholder keeps the native FF&E demo runnable; use a real staging RUM application ID to validate RUM flag annotation.
+- `ENVIRONMENT`: use `staging` for this demo.
+
+Do not commit real credentials.
+
+## Native FFE Offline Smoke
+
+CI runs the Android smoke test below to exercise the offline fixture corpus from
+React Native JS through the native bridge:
+
+```sh
+ANDROID_HOME="$ANDROID_HOME" ANDROID_SDK_ROOT="$ANDROID_SDK_ROOT" \
+ ./example-new-architecture/scripts/native-ffe-offline-android-smoke.sh
+```
+
+The smoke test starts Metro if needed, installs the new-architecture Android
+app, launches it on an emulator, and waits for `Native FFE offline fixture pass:
+233 cases across 30 files`. It does not use credentials or make a feature flag
+network request.
diff --git a/example-new-architecture/android/build.gradle b/example-new-architecture/android/build.gradle
index 9eb5eac03..94bf25930 100644
--- a/example-new-architecture/android/build.gradle
+++ b/example-new-architecture/android/build.gradle
@@ -1,3 +1,5 @@
+import org.gradle.api.artifacts.repositories.MavenArtifactRepository
+
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
@@ -21,3 +23,16 @@ buildscript {
}
apply plugin: "com.facebook.react.rootproject"
+
+subprojects {
+ repositories.configureEach { repo ->
+ if (
+ repo instanceof MavenArtifactRepository &&
+ repo.url.toString().contains("oss.sonatype.org/content/repositories/snapshots")
+ ) {
+ repo.mavenContent {
+ snapshotsOnly()
+ }
+ }
+ }
+}
diff --git a/example-new-architecture/ddCredentials.example.js b/example-new-architecture/ddCredentials.example.js
new file mode 100644
index 000000000..81f587541
--- /dev/null
+++ b/example-new-architecture/ddCredentials.example.js
@@ -0,0 +1,7 @@
+export const APPLICATION_ID = '00000000-0000-0000-0000-000000000000';
+export const CLIENT_TOKEN = 'pub00000000000000000000000000000000';
+export const ENVIRONMENT = 'staging';
+
+// Used by the native FF&E dynamic rules fetch demo. This can match CLIENT_TOKEN
+// when that token is authorized for the staging rules-based configuration API.
+export const NATIVE_FFE_CLIENT_TOKEN = CLIENT_TOKEN;
diff --git a/example-new-architecture/fixtures/native-ffe/offline-rules-configuration-wire.json b/example-new-architecture/fixtures/native-ffe/offline-rules-configuration-wire.json
new file mode 100644
index 000000000..63c3ca653
--- /dev/null
+++ b/example-new-architecture/fixtures/native-ffe/offline-rules-configuration-wire.json
@@ -0,0 +1 @@
+{"version":2,"server":{"response":"{\n \"createdAt\": \"2024-04-17T19:40:53.716Z\",\n \"format\": \"SERVER\",\n \"environment\": {\n \"name\": \"Test\",\n \"unknownEnvironmentField\": \"ignored\"\n },\n \"unknownTopLevelField\": \"ignored\",\n \"unknownTopLevelNull\": null,\n \"flags\": {\n \"empty_flag\": {\n \"key\": \"empty_flag\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {},\n \"allocations\": []\n },\n \"disabled_flag\": {\n \"key\": \"disabled_flag\",\n \"enabled\": false,\n \"variationType\": \"INTEGER\",\n \"variations\": {},\n \"allocations\": []\n },\n \"no_allocations_flag\": {\n \"key\": \"no_allocations_flag\",\n \"enabled\": true,\n \"variationType\": \"JSON\",\n \"variations\": {\n \"control\": {\n \"key\": \"control\",\n \"value\": {\n \"variant\": \"control\"\n }\n },\n \"treatment\": {\n \"key\": \"treatment\",\n \"value\": {\n \"variant\": \"treatment\"\n }\n }\n },\n \"allocations\": []\n },\n \"numeric_flag\": {\n \"key\": \"numeric_flag\",\n \"enabled\": true,\n \"variationType\": \"NUMERIC\",\n \"variations\": {\n \"e\": {\n \"key\": \"e\",\n \"value\": 2.7182818\n },\n \"pi\": {\n \"key\": \"pi\",\n \"value\": 3.1415926\n }\n },\n \"allocations\": [\n {\n \"key\": \"rollout\",\n \"splits\": [\n {\n \"variationKey\": \"pi\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"regex-flag\": {\n \"key\": \"regex-flag\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"partial-example\": {\n \"key\": \"partial-example\",\n \"value\": \"partial-example\"\n },\n \"test\": {\n \"key\": \"test\",\n \"value\": \"test\"\n },\n \"capturing-groups\": {\n \"key\": \"capturing-groups\",\n \"value\": \"capturing-groups\"\n },\n \"unicode-word-boundary\": {\n \"key\": \"unicode-word-boundary\",\n \"value\": \"unicode-word-boundary\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"capturing-groups\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"email\",\n \"operator\": \"MATCHES\",\n \"value\": \"^([[:alnum:]._%+-]+)@capture\\\\.example$\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"capturing-groups\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"unicode-word-boundary\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"email\",\n \"operator\": \"MATCHES\",\n \"value\": \"(?u)\\\\bmañana\\\\b\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"unicode-word-boundary\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"partial-example\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"email\",\n \"operator\": \"MATCHES\",\n \"value\": \"@example\\\\.com\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"partial-example\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"test\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"email\",\n \"operator\": \"MATCHES\",\n \"value\": \".*@test\\\\.com\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"test\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"numeric-one-of\": {\n \"key\": \"numeric-one-of\",\n \"enabled\": true,\n \"variationType\": \"INTEGER\",\n \"variations\": {\n \"1\": {\n \"key\": \"1\",\n \"value\": 1\n },\n \"2\": {\n \"key\": \"2\",\n \"value\": 2\n },\n \"3\": {\n \"key\": \"3\",\n \"value\": 3\n }\n },\n \"allocations\": [\n {\n \"key\": \"1-for-1\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"number\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"1\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"1\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"2-for-123456789\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"number\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"123456789\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"2\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"3-for-not-2\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"number\",\n \"operator\": \"NOT_ONE_OF\",\n \"value\": [\n \"2\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"3\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"boolean-one-of-matches\": {\n \"key\": \"boolean-one-of-matches\",\n \"enabled\": true,\n \"variationType\": \"INTEGER\",\n \"variations\": {\n \"1\": {\n \"key\": \"1\",\n \"value\": 1\n },\n \"2\": {\n \"key\": \"2\",\n \"value\": 2\n },\n \"3\": {\n \"key\": \"3\",\n \"value\": 3\n },\n \"4\": {\n \"key\": \"4\",\n \"value\": 4\n },\n \"5\": {\n \"key\": \"5\",\n \"value\": 5\n }\n },\n \"allocations\": [\n {\n \"key\": \"1-for-one-of\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"one_of_flag\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"1\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"2-for-matches\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"matches_flag\",\n \"operator\": \"MATCHES\",\n \"value\": \"true\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"2\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"3-for-not-one-of\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"not_one_of_flag\",\n \"operator\": \"NOT_ONE_OF\",\n \"value\": [\n \"false\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"3\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"4-for-not-matches\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"not_matches_flag\",\n \"operator\": \"NOT_MATCHES\",\n \"value\": \"false\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"4\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"5-for-matches-null\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"null_flag\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"null\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"5\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"empty_string_flag\": {\n \"key\": \"empty_string_flag\",\n \"enabled\": true,\n \"comment\": \"Testing the empty string as a variation value\",\n \"variationType\": \"STRING\",\n \"variations\": {\n \"empty_string\": {\n \"key\": \"empty_string\",\n \"value\": \"\"\n },\n \"non_empty\": {\n \"key\": \"non_empty\",\n \"value\": \"non_empty\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"allocation-empty\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"country\",\n \"operator\": \"MATCHES\",\n \"value\": \"US\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"empty_string\",\n \"shards\": [\n {\n \"salt\": \"allocation-empty-shards\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"non_empty\",\n \"shards\": [\n {\n \"salt\": \"allocation-empty-shards\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"kill-switch\": {\n \"key\": \"kill-switch\",\n \"enabled\": true,\n \"variationType\": \"BOOLEAN\",\n \"variations\": {\n \"on\": {\n \"key\": \"on\",\n \"value\": true\n },\n \"off\": {\n \"key\": \"off\",\n \"value\": false\n }\n },\n \"allocations\": [\n {\n \"key\": \"on-for-NA\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"country\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"US\",\n \"Canada\",\n \"Mexico\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"on\",\n \"shards\": [\n {\n \"salt\": \"some-salt\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"on-for-age-50+\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"age\",\n \"operator\": \"GTE\",\n \"value\": 50\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"on\",\n \"shards\": [\n {\n \"salt\": \"some-salt\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"off-for-all\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"off\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"comparator-operator-test\": {\n \"key\": \"comparator-operator-test\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"small\": {\n \"key\": \"small\",\n \"value\": \"small\"\n },\n \"medium\": {\n \"key\": \"medium\",\n \"value\": \"medium\"\n },\n \"large\": {\n \"key\": \"large\",\n \"value\": \"large\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"small-size\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"size\",\n \"operator\": \"LT\",\n \"value\": 10\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"small\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"medum-size\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"size\",\n \"operator\": \"GTE\",\n \"value\": 10\n },\n {\n \"attribute\": \"size\",\n \"operator\": \"LTE\",\n \"value\": 20\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"medium\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"large-size\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"size\",\n \"operator\": \"GT\",\n \"value\": 25\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"large\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"start-and-end-date-test\": {\n \"key\": \"start-and-end-date-test\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"old\": {\n \"key\": \"old\",\n \"value\": \"old\"\n },\n \"current\": {\n \"key\": \"current\",\n \"value\": \"current\"\n },\n \"new\": {\n \"key\": \"new\",\n \"value\": \"new\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"old-versions\",\n \"splits\": [\n {\n \"variationKey\": \"old\",\n \"shards\": []\n }\n ],\n \"endAt\": \"2002-10-31T09:00:00.594Z\",\n \"doLog\": true\n },\n {\n \"key\": \"future-versions\",\n \"splits\": [\n {\n \"variationKey\": \"new\",\n \"shards\": []\n }\n ],\n \"startAt\": \"2052-10-31T09:00:00.594Z\",\n \"doLog\": true\n },\n {\n \"key\": \"current-versions\",\n \"splits\": [\n {\n \"variationKey\": \"current\",\n \"shards\": []\n }\n ],\n \"startAt\": \"2022-10-31T09:00:00.594Z\",\n \"endAt\": \"2050-10-31T09:00:00.594Z\",\n \"doLog\": true\n }\n ]\n },\n \"null-operator-test\": {\n \"key\": \"null-operator-test\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"old\": {\n \"key\": \"old\",\n \"value\": \"old\"\n },\n \"new\": {\n \"key\": \"new\",\n \"value\": \"new\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"null-operator\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"size\",\n \"operator\": \"IS_NULL\",\n \"value\": true\n }\n ]\n },\n {\n \"conditions\": [\n {\n \"attribute\": \"size\",\n \"operator\": \"LT\",\n \"value\": 10\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"old\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"not-null-operator\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"size\",\n \"operator\": \"IS_NULL\",\n \"value\": false\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"new\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"new-user-onboarding\": {\n \"key\": \"new-user-onboarding\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"control\": {\n \"key\": \"control\",\n \"value\": \"control\"\n },\n \"red\": {\n \"key\": \"red\",\n \"value\": \"red\"\n },\n \"blue\": {\n \"key\": \"blue\",\n \"value\": \"blue\"\n },\n \"green\": {\n \"key\": \"green\",\n \"value\": \"green\"\n },\n \"yellow\": {\n \"key\": \"yellow\",\n \"value\": \"yellow\"\n },\n \"purple\": {\n \"key\": \"purple\",\n \"value\": \"purple\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"id rule\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"id\",\n \"operator\": \"MATCHES\",\n \"value\": \"zach\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"purple\",\n \"shards\": []\n }\n ],\n \"doLog\": false\n },\n {\n \"key\": \"internal users\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"email\",\n \"operator\": \"MATCHES\",\n \"value\": \"@mycompany.com\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"green\",\n \"shards\": []\n }\n ],\n \"doLog\": false\n },\n {\n \"key\": \"experiment\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"country\",\n \"operator\": \"NOT_ONE_OF\",\n \"value\": [\n \"US\",\n \"Canada\",\n \"Mexico\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"control\",\n \"shards\": [\n {\n \"salt\": \"traffic-new-user-onboarding-experiment\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 6000\n }\n ]\n },\n {\n \"salt\": \"split-new-user-onboarding-experiment\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 5000\n }\n ]\n }\n ]\n },\n {\n \"variationKey\": \"red\",\n \"shards\": [\n {\n \"salt\": \"traffic-new-user-onboarding-experiment\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 6000\n }\n ]\n },\n {\n \"salt\": \"split-new-user-onboarding-experiment\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 5000,\n \"end\": 8000\n }\n ]\n }\n ]\n },\n {\n \"variationKey\": \"yellow\",\n \"shards\": [\n {\n \"salt\": \"traffic-new-user-onboarding-experiment\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 6000\n }\n ]\n },\n {\n \"salt\": \"split-new-user-onboarding-experiment\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 8000,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"rollout\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"country\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"US\",\n \"Canada\",\n \"Mexico\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"blue\",\n \"shards\": [\n {\n \"salt\": \"split-new-user-onboarding-rollout\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 8000\n }\n ]\n }\n ],\n \"extraLogging\": {\n \"allocationvalue_type\": \"rollout\",\n \"owner\": \"hippo\"\n }\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"integer-flag\": {\n \"key\": \"integer-flag\",\n \"enabled\": true,\n \"variationType\": \"INTEGER\",\n \"variations\": {\n \"one\": {\n \"key\": \"one\",\n \"value\": 1\n },\n \"two\": {\n \"key\": \"two\",\n \"value\": 2\n },\n \"three\": {\n \"key\": \"three\",\n \"value\": 3\n }\n },\n \"allocations\": [\n {\n \"key\": \"targeted allocation\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"country\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"US\",\n \"Canada\",\n \"Mexico\"\n ]\n }\n ]\n },\n {\n \"conditions\": [\n {\n \"attribute\": \"email\",\n \"operator\": \"MATCHES\",\n \"value\": \".*@example.com\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"three\",\n \"shards\": [\n {\n \"salt\": \"full-range-salt\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"50/50 split\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"one\",\n \"shards\": [\n {\n \"salt\": \"split-numeric-flag-some-allocation\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 5000\n }\n ]\n }\n ]\n },\n {\n \"variationKey\": \"two\",\n \"shards\": [\n {\n \"salt\": \"split-numeric-flag-some-allocation\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 5000,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"json-config-flag\": {\n \"key\": \"json-config-flag\",\n \"enabled\": true,\n \"variationType\": \"JSON\",\n \"variations\": {\n \"one\": {\n \"key\": \"one\",\n \"value\": {\n \"integer\": 1,\n \"string\": \"one\",\n \"float\": 1.0\n }\n },\n \"two\": {\n \"key\": \"two\",\n \"value\": {\n \"integer\": 2,\n \"string\": \"two\",\n \"float\": 2.0\n }\n },\n \"empty\": {\n \"key\": \"empty\",\n \"value\": {}\n }\n },\n \"allocations\": [\n {\n \"key\": \"Optionally Force Empty\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"Force Empty\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"empty\",\n \"shards\": [\n {\n \"salt\": \"full-range-salt\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"50/50 split\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"one\",\n \"shards\": [\n {\n \"salt\": \"traffic-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n },\n {\n \"salt\": \"split-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 5000\n }\n ]\n }\n ]\n },\n {\n \"variationKey\": \"two\",\n \"shards\": [\n {\n \"salt\": \"traffic-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000\n }\n ]\n },\n {\n \"salt\": \"split-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 5000,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"special-characters\": {\n \"key\": \"special-characters\",\n \"enabled\": true,\n \"variationType\": \"JSON\",\n \"variations\": {\n \"de\": {\n \"key\": \"de\",\n \"value\": {\n \"a\": \"kümmert\",\n \"b\": \"schön\"\n }\n },\n \"ua\": {\n \"key\": \"ua\",\n \"value\": {\n \"a\": \"піклуватися\",\n \"b\": \"любов\"\n }\n },\n \"zh\": {\n \"key\": \"zh\",\n \"value\": {\n \"a\": \"照顾\",\n \"b\": \"漂亮\"\n }\n },\n \"emoji\": {\n \"key\": \"emoji\",\n \"value\": {\n \"a\": \"🤗\",\n \"b\": \"🌸\"\n }\n }\n },\n \"allocations\": [\n {\n \"key\": \"allocation-test\",\n \"splits\": [\n {\n \"variationKey\": \"de\",\n \"shards\": [\n {\n \"salt\": \"split-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 2500\n }\n ]\n }\n ]\n },\n {\n \"variationKey\": \"ua\",\n \"shards\": [\n {\n \"salt\": \"split-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 2500,\n \"end\": 5000\n }\n ]\n }\n ]\n },\n {\n \"variationKey\": \"zh\",\n \"shards\": [\n {\n \"salt\": \"split-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 5000,\n \"end\": 7500\n }\n ]\n }\n ]\n },\n {\n \"variationKey\": \"emoji\",\n \"shards\": [\n {\n \"salt\": \"split-json-flag\",\n \"totalShards\": 10000,\n \"ranges\": [\n {\n \"start\": 7500,\n \"end\": 10000\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-default\",\n \"splits\": [\n {\n \"variationKey\": \"de\",\n \"shards\": []\n }\n ],\n \"doLog\": false\n }\n ]\n },\n \"string_flag_with_special_characters\": {\n \"key\": \"string_flag_with_special_characters\",\n \"enabled\": true,\n \"comment\": \"Testing the string with special characters and spaces\",\n \"variationType\": \"STRING\",\n \"variations\": {\n \"string_with_spaces\": {\n \"key\": \"string_with_spaces\",\n \"value\": \" a b c d e f \"\n },\n \"string_with_only_one_space\": {\n \"key\": \"string_with_only_one_space\",\n \"value\": \" \"\n },\n \"string_with_only_multiple_spaces\": {\n \"key\": \"string_with_only_multiple_spaces\",\n \"value\": \" \"\n },\n \"string_with_dots\": {\n \"key\": \"string_with_dots\",\n \"value\": \".a.b.c.d.e.f.\"\n },\n \"string_with_only_one_dot\": {\n \"key\": \"string_with_only_one_dot\",\n \"value\": \".\"\n },\n \"string_with_only_multiple_dots\": {\n \"key\": \"string_with_only_multiple_dots\",\n \"value\": \".......\"\n },\n \"string_with_comas\": {\n \"key\": \"string_with_comas\",\n \"value\": \",a,b,c,d,e,f,\"\n },\n \"string_with_only_one_coma\": {\n \"key\": \"string_with_only_one_coma\",\n \"value\": \",\"\n },\n \"string_with_only_multiple_comas\": {\n \"key\": \"string_with_only_multiple_comas\",\n \"value\": \",,,,,,,\"\n },\n \"string_with_colons\": {\n \"key\": \"string_with_colons\",\n \"value\": \":a:b:c:d:e:f:\"\n },\n \"string_with_only_one_colon\": {\n \"key\": \"string_with_only_one_colon\",\n \"value\": \":\"\n },\n \"string_with_only_multiple_colons\": {\n \"key\": \"string_with_only_multiple_colons\",\n \"value\": \":::::::\"\n },\n \"string_with_semicolons\": {\n \"key\": \"string_with_semicolons\",\n \"value\": \";a;b;c;d;e;f;\"\n },\n \"string_with_only_one_semicolon\": {\n \"key\": \"string_with_only_one_semicolon\",\n \"value\": \";\"\n },\n \"string_with_only_multiple_semicolons\": {\n \"key\": \"string_with_only_multiple_semicolons\",\n \"value\": \";;;;;;;\"\n },\n \"string_with_slashes\": {\n \"key\": \"string_with_slashes\",\n \"value\": \"/a/b/c/d/e/f/\"\n },\n \"string_with_only_one_slash\": {\n \"key\": \"string_with_only_one_slash\",\n \"value\": \"/\"\n },\n \"string_with_only_multiple_slashes\": {\n \"key\": \"string_with_only_multiple_slashes\",\n \"value\": \"///////\"\n },\n \"string_with_dashes\": {\n \"key\": \"string_with_dashes\",\n \"value\": \"-a-b-c-d-e-f-\"\n },\n \"string_with_only_one_dash\": {\n \"key\": \"string_with_only_one_dash\",\n \"value\": \"-\"\n },\n \"string_with_only_multiple_dashes\": {\n \"key\": \"string_with_only_multiple_dashes\",\n \"value\": \"-------\"\n },\n \"string_with_underscores\": {\n \"key\": \"string_with_underscores\",\n \"value\": \"_a_b_c_d_e_f_\"\n },\n \"string_with_only_one_underscore\": {\n \"key\": \"string_with_only_one_underscore\",\n \"value\": \"_\"\n },\n \"string_with_only_multiple_underscores\": {\n \"key\": \"string_with_only_multiple_underscores\",\n \"value\": \"_______\"\n },\n \"string_with_plus_signs\": {\n \"key\": \"string_with_plus_signs\",\n \"value\": \"+a+b+c+d+e+f+\"\n },\n \"string_with_only_one_plus_sign\": {\n \"key\": \"string_with_only_one_plus_sign\",\n \"value\": \"+\"\n },\n \"string_with_only_multiple_plus_signs\": {\n \"key\": \"string_with_only_multiple_plus_signs\",\n \"value\": \"+++++++\"\n },\n \"string_with_equal_signs\": {\n \"key\": \"string_with_equal_signs\",\n \"value\": \"=a=b=c=d=e=f=\"\n },\n \"string_with_only_one_equal_sign\": {\n \"key\": \"string_with_only_one_equal_sign\",\n \"value\": \"=\"\n },\n \"string_with_only_multiple_equal_signs\": {\n \"key\": \"string_with_only_multiple_equal_signs\",\n \"value\": \"=======\"\n },\n \"string_with_dollar_signs\": {\n \"key\": \"string_with_dollar_signs\",\n \"value\": \"$a$b$c$d$e$f$\"\n },\n \"string_with_only_one_dollar_sign\": {\n \"key\": \"string_with_only_one_dollar_sign\",\n \"value\": \"$\"\n },\n \"string_with_only_multiple_dollar_signs\": {\n \"key\": \"string_with_only_multiple_dollar_signs\",\n \"value\": \"$$$$$$$\"\n },\n \"string_with_at_signs\": {\n \"key\": \"string_with_at_signs\",\n \"value\": \"@a@b@c@d@e@f@\"\n },\n \"string_with_only_one_at_sign\": {\n \"key\": \"string_with_only_one_at_sign\",\n \"value\": \"@\"\n },\n \"string_with_only_multiple_at_signs\": {\n \"key\": \"string_with_only_multiple_at_signs\",\n \"value\": \"@@@@@@@\"\n },\n \"string_with_amp_signs\": {\n \"key\": \"string_with_amp_signs\",\n \"value\": \"&a&b&c&d&e&f&\"\n },\n \"string_with_only_one_amp_sign\": {\n \"key\": \"string_with_only_one_amp_sign\",\n \"value\": \"&\"\n },\n \"string_with_only_multiple_amp_signs\": {\n \"key\": \"string_with_only_multiple_amp_signs\",\n \"value\": \"&&&&&&&\"\n },\n \"string_with_hash_signs\": {\n \"key\": \"string_with_hash_signs\",\n \"value\": \"#a#b#c#d#e#f#\"\n },\n \"string_with_only_one_hash_sign\": {\n \"key\": \"string_with_only_one_hash_sign\",\n \"value\": \"#\"\n },\n \"string_with_only_multiple_hash_signs\": {\n \"key\": \"string_with_only_multiple_hash_signs\",\n \"value\": \"#######\"\n },\n \"string_with_percentage_signs\": {\n \"key\": \"string_with_percentage_signs\",\n \"value\": \"%a%b%c%d%e%f%\"\n },\n \"string_with_only_one_percentage_sign\": {\n \"key\": \"string_with_only_one_percentage_sign\",\n \"value\": \"%\"\n },\n \"string_with_only_multiple_percentage_signs\": {\n \"key\": \"string_with_only_multiple_percentage_signs\",\n \"value\": \"%%%%%%%\"\n },\n \"string_with_tilde_signs\": {\n \"key\": \"string_with_tilde_signs\",\n \"value\": \"~a~b~c~d~e~f~\"\n },\n \"string_with_only_one_tilde_sign\": {\n \"key\": \"string_with_only_one_tilde_sign\",\n \"value\": \"~\"\n },\n \"string_with_only_multiple_tilde_signs\": {\n \"key\": \"string_with_only_multiple_tilde_signs\",\n \"value\": \"~~~~~~~\"\n },\n \"string_with_asterix_signs\": {\n \"key\": \"string_with_asterix_signs\",\n \"value\": \"*a*b*c*d*e*f*\"\n },\n \"string_with_only_one_asterix_sign\": {\n \"key\": \"string_with_only_one_asterix_sign\",\n \"value\": \"*\"\n },\n \"string_with_only_multiple_asterix_signs\": {\n \"key\": \"string_with_only_multiple_asterix_signs\",\n \"value\": \"*******\"\n },\n \"string_with_single_quotes\": {\n \"key\": \"string_with_single_quotes\",\n \"value\": \"'a'b'c'd'e'f'\"\n },\n \"string_with_only_one_single_quote\": {\n \"key\": \"string_with_only_one_single_quote\",\n \"value\": \"'\"\n },\n \"string_with_only_multiple_single_quotes\": {\n \"key\": \"string_with_only_multiple_single_quotes\",\n \"value\": \"'''''''\"\n },\n \"string_with_question_marks\": {\n \"key\": \"string_with_question_marks\",\n \"value\": \"?a?b?c?d?e?f?\"\n },\n \"string_with_only_one_question_mark\": {\n \"key\": \"string_with_only_one_question_mark\",\n \"value\": \"?\"\n },\n \"string_with_only_multiple_question_marks\": {\n \"key\": \"string_with_only_multiple_question_marks\",\n \"value\": \"???????\"\n },\n \"string_with_exclamation_marks\": {\n \"key\": \"string_with_exclamation_marks\",\n \"value\": \"!a!b!c!d!e!f!\"\n },\n \"string_with_only_one_exclamation_mark\": {\n \"key\": \"string_with_only_one_exclamation_mark\",\n \"value\": \"!\"\n },\n \"string_with_only_multiple_exclamation_marks\": {\n \"key\": \"string_with_only_multiple_exclamation_marks\",\n \"value\": \"!!!!!!!\"\n },\n \"string_with_opening_parentheses\": {\n \"key\": \"string_with_opening_parentheses\",\n \"value\": \"(a(b(c(d(e(f(\"\n },\n \"string_with_only_one_opening_parenthese\": {\n \"key\": \"string_with_only_one_opening_parenthese\",\n \"value\": \"(\"\n },\n \"string_with_only_multiple_opening_parentheses\": {\n \"key\": \"string_with_only_multiple_opening_parentheses\",\n \"value\": \"(((((((\"\n },\n \"string_with_closing_parentheses\": {\n \"key\": \"string_with_closing_parentheses\",\n \"value\": \")a)b)c)d)e)f)\"\n },\n \"string_with_only_one_closing_parenthese\": {\n \"key\": \"string_with_only_one_closing_parenthese\",\n \"value\": \")\"\n },\n \"string_with_only_multiple_closing_parentheses\": {\n \"key\": \"string_with_only_multiple_closing_parentheses\",\n \"value\": \")))))))\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"allocation-test-string_with_spaces\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_spaces\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_spaces\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_space\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_space\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_space\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_spaces\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_spaces\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_spaces\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_dots\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_dots\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_dots\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_dot\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_dot\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_dot\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_dots\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_dots\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_dots\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_comas\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_comas\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_comas\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_coma\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_coma\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_coma\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_comas\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_comas\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_comas\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_colons\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_colons\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_colons\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_colon\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_colon\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_colon\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_colons\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_colons\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_colons\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_semicolons\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_semicolons\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_semicolons\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_semicolon\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_semicolon\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_semicolon\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_semicolons\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_semicolons\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_semicolons\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_slashes\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_slashes\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_slashes\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_slash\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_slash\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_slash\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_slashes\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_slashes\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_slashes\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_dashes\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_dashes\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_dashes\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_dash\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_dash\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_dash\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_dashes\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_dashes\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_dashes\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_underscores\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_underscores\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_underscores\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_underscore\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_underscore\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_underscore\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_underscores\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_underscores\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_underscores\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_plus_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_plus_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_plus_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_plus_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_plus_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_plus_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_plus_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_plus_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_plus_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_equal_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_equal_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_equal_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_equal_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_equal_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_equal_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_equal_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_equal_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_equal_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_dollar_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_dollar_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_dollar_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_dollar_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_dollar_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_dollar_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_dollar_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_dollar_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_dollar_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_at_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_at_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_at_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_at_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_at_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_at_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_at_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_at_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_at_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_amp_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_amp_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_amp_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_amp_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_amp_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_amp_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_amp_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_amp_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_amp_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_hash_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_hash_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_hash_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_hash_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_hash_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_hash_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_hash_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_hash_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_hash_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_percentage_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_percentage_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_percentage_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_percentage_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_percentage_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_percentage_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_percentage_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_percentage_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_percentage_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_tilde_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_tilde_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_tilde_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_tilde_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_tilde_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_tilde_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_tilde_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_tilde_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_tilde_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_asterix_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_asterix_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_asterix_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_asterix_sign\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_asterix_sign\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_asterix_sign\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_asterix_signs\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_asterix_signs\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_asterix_signs\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_single_quotes\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_single_quotes\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_single_quotes\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_single_quote\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_single_quote\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_single_quote\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_single_quotes\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_single_quotes\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_single_quotes\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_question_marks\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_question_marks\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_question_marks\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_question_mark\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_question_mark\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_question_mark\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_question_marks\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_question_marks\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_question_marks\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_exclamation_marks\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_exclamation_marks\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_exclamation_marks\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_exclamation_mark\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_exclamation_mark\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_exclamation_mark\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_exclamation_marks\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_exclamation_marks\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_exclamation_marks\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_opening_parentheses\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_opening_parentheses\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_opening_parentheses\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_opening_parenthese\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_opening_parenthese\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_opening_parenthese\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_opening_parentheses\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_opening_parentheses\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_opening_parentheses\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_closing_parentheses\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_closing_parentheses\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_closing_parentheses\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_one_closing_parenthese\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_one_closing_parenthese\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_one_closing_parenthese\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"allocation-test-string_with_only_multiple_closing_parentheses\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"string_with_only_multiple_closing_parentheses\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"string_with_only_multiple_closing_parentheses\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"boolean-false-assignment\": {\n \"key\": \"boolean-false-assignment\",\n \"enabled\": true,\n \"variationType\": \"BOOLEAN\",\n \"variations\": {\n \"false-variation\": {\n \"key\": \"false-variation\",\n \"value\": false\n },\n \"true-variation\": {\n \"key\": \"true-variation\",\n \"value\": true\n }\n },\n \"allocations\": [\n {\n \"key\": \"disable-feature\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"should_disable_feature\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"true\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"false-variation\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"enable-feature\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"should_disable_feature\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"false\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"true-variation\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ],\n \"totalShards\": 10000\n },\n \"empty-string-variation\": {\n \"key\": \"empty-string-variation\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"empty-content\": {\n \"key\": \"empty-content\",\n \"value\": \"\"\n },\n \"detailed-content\": {\n \"key\": \"detailed-content\",\n \"value\": \"detailed_content\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"minimal-content\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"content_type\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"minimal\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"empty-content\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"full-content\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"content_type\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"full\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"detailed-content\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ],\n \"totalShards\": 10000\n },\n \"falsy-value-assignments\": {\n \"key\": \"falsy-value-assignments\",\n \"enabled\": true,\n \"variationType\": \"INTEGER\",\n \"variations\": {\n \"zero-limit\": {\n \"key\": \"zero-limit\",\n \"value\": 0\n },\n \"premium-limit\": {\n \"key\": \"premium-limit\",\n \"value\": 100\n }\n },\n \"allocations\": [\n {\n \"key\": \"free-tier-limit\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"plan_tier\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"free\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"zero-limit\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"premium-tier-limit\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"plan_tier\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"premium\"\n ]\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"premium-limit\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ],\n \"totalShards\": 10000\n },\n \"empty-targeting-key-flag\": {\n \"key\": \"empty-targeting-key-flag\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"on\": {\n \"key\": \"on\",\n \"value\": \"on-value\"\n },\n \"off\": {\n \"key\": \"off\",\n \"value\": \"off-value\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"default-allocation\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"on\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"microsecond-date-test\": {\n \"key\": \"microsecond-date-test\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"variations\": {\n \"expired\": {\n \"key\": \"expired\",\n \"value\": \"expired\"\n },\n \"active\": {\n \"key\": \"active\",\n \"value\": \"active\"\n },\n \"future\": {\n \"key\": \"future\",\n \"value\": \"future\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"expired-allocation\",\n \"splits\": [\n {\n \"variationKey\": \"expired\",\n \"shards\": []\n }\n ],\n \"endAt\": \"2002-10-31T09:00:00.594321Z\",\n \"doLog\": true\n },\n {\n \"key\": \"future-allocation\",\n \"splits\": [\n {\n \"variationKey\": \"future\",\n \"shards\": []\n }\n ],\n \"startAt\": \"2052-10-31T09:00:00.123456Z\",\n \"doLog\": true\n },\n {\n \"key\": \"active-allocation\",\n \"splits\": [\n {\n \"variationKey\": \"active\",\n \"shards\": []\n }\n ],\n \"startAt\": \"2022-10-31T09:00:00.235982Z\",\n \"endAt\": \"2050-10-31T09:00:00.987654Z\",\n \"doLog\": true\n }\n ]\n },\n \"unknown-fields-tolerance-flag\": {\n \"key\": \"unknown-fields-tolerance-flag\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"comment\": \"This flag deliberately contains unknown fields at every fixed-schema UFC level. SDKs must ignore those fields and evaluate the flag normally.\",\n \"unknownFlagField\": \"ignored\",\n \"variations\": {\n \"on\": {\n \"key\": \"on\",\n \"value\": \"on\",\n \"unknownVariationField\": \"ignored\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"unknown-fields-allocation\",\n \"unknownAllocationField\": \"ignored\",\n \"rules\": [\n {\n \"unknownRuleField\": \"ignored\",\n \"conditions\": [\n {\n \"attribute\": \"country\",\n \"operator\": \"ONE_OF\",\n \"value\": [\n \"US\"\n ],\n \"unknownConditionField\": \"ignored\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"on\",\n \"unknownSplitField\": \"ignored\",\n \"shards\": [\n {\n \"salt\": \"unknown-fields-tolerance-flag\",\n \"totalShards\": 10000,\n \"unknownShardField\": \"ignored\",\n \"ranges\": [\n {\n \"start\": 0,\n \"end\": 10000,\n \"unknownRangeField\": \"ignored\"\n }\n ]\n }\n ]\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"operator-grease-flag\": {\n \"key\": \"operator-grease-flag\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"comment\": \"This flag deliberately uses an unknown condition operator. SDKs must ignore this flag rather than falling through to the catch-all allocation or rejecting the whole config.\",\n \"variations\": {\n \"trap\": {\n \"key\": \"trap\",\n \"value\": \"trap\"\n },\n \"on\": {\n \"key\": \"on\",\n \"value\": \"on\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"grease-allocation\",\n \"rules\": [\n {\n \"conditions\": [\n {\n \"attribute\": \"country\",\n \"operator\": \"NOT_A_REAL_OPERATOR\",\n \"value\": \"anything\"\n }\n ]\n }\n ],\n \"splits\": [\n {\n \"variationKey\": \"trap\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n },\n {\n \"key\": \"default-allocation\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"on\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"malformed-allocations-flag\": {\n \"key\": \"malformed-allocations-flag\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"comment\": \"This flag is intentionally UFC-schema-invalid: allocations must be an array, but this fixture uses a string to verify per-flag parse error isolation.\",\n \"variations\": {\n \"on\": {\n \"key\": \"on\",\n \"value\": \"on\"\n }\n },\n \"allocations\": \"this-is-not-a-list\"\n },\n \"missing-split-shards-flag\": {\n \"key\": \"missing-split-shards-flag\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"comment\": \"This flag is intentionally UFC-schema-invalid: split.shards is required even when the intended split is unsharded. SDKs must treat omitted shards differently from an explicit empty shards array.\",\n \"variations\": {\n \"trap\": {\n \"key\": \"trap\",\n \"value\": \"trap\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"malformed-split-allocation\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"trap\"\n }\n ],\n \"doLog\": true\n }\n ]\n },\n \"valid-flag-after-invalid-config\": {\n \"key\": \"valid-flag-after-invalid-config\",\n \"enabled\": true,\n \"variationType\": \"STRING\",\n \"comment\": \"This valid neighbor proves malformed or unsupported flags do not poison the rest of the remote config.\",\n \"variations\": {\n \"expected\": {\n \"key\": \"expected\",\n \"value\": \"expected\"\n }\n },\n \"allocations\": [\n {\n \"key\": \"default-allocation\",\n \"rules\": [],\n \"splits\": [\n {\n \"variationKey\": \"expected\",\n \"shards\": []\n }\n ],\n \"doLog\": true\n }\n ]\n }\n }\n}\n","etag":"ffe-system-test-data"}}
\ No newline at end of file
diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock
index cc0c0b9dd..2b2fea464 100644
--- a/example-new-architecture/ios/Podfile.lock
+++ b/example-new-architecture/ios/Podfile.lock
@@ -1872,7 +1872,7 @@ SPEC CHECKSUMS:
DatadogInternal: 00709affd9889ca9e8fd96e85e39ad865e651c32
DatadogLogs: ef98708261f8f7ba82d15360f9951d43f578163b
DatadogRUM: 67d130164f4a7663e05a3d02803be9e9dd1abbb7
- DatadogSDKReactNative: c2877e376b00655cf51363ab6cdc0fd013eb7e3b
+ DatadogSDKReactNative: bf53c1c065e3dea976d71d45d6015ddd173762f4
DatadogTrace: 17f50647107755fba79fc2298bf4cb282e0efb1f
DatadogWebViewTracking: 1290fce6010bf65b5e0e0ffa9048b448782e469c
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
diff --git a/example-new-architecture/nativeFfeOfflineFixtureRunner.ts b/example-new-architecture/nativeFfeOfflineFixtureRunner.ts
new file mode 100644
index 000000000..3d7cc19d7
--- /dev/null
+++ b/example-new-architecture/nativeFfeOfflineFixtureRunner.ts
@@ -0,0 +1,359 @@
+import {DdSdkReactNative} from '@datadog/mobile-react-native';
+import type {
+ FlagEvaluationResult,
+ FlagValue,
+ FlagsEvaluationContext,
+ FlagsProviderDebugState,
+} from '@datadog/mobile-react-native';
+
+import ufcConfig from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/ufc-config.json';
+import testCaseBooleanFalseAssignment from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-boolean-false-assignment.json';
+import testCaseBooleanOneOfMatches from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-boolean-one-of-matches.json';
+import testCaseComparatorOperatorFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-comparator-operator-flag.json';
+import testCaseDisabledFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-disabled-flag.json';
+import testCaseEmptyFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-empty-flag.json';
+import testCaseEmptyStringVariation from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-empty-string-variation.json';
+import testCaseFalsyValueAssignments from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-falsy-value-assignments.json';
+import testCaseFlagWithEmptyString from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-flag-with-empty-string.json';
+import testCaseIntegerFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-integer-flag.json';
+import testCaseKillSwitchFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-kill-switch-flag.json';
+import testCaseMalformedFlagIsolation from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-malformed-flag-isolation.json';
+import testCaseMicrosecondDateFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-microsecond-date-flag.json';
+import testCaseMissingSplitShardsIsolation from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-missing-split-shards-isolation.json';
+import testCaseNewUserOnboardingFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-new-user-onboarding-flag.json';
+import testCaseNoAllocationsFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-no-allocations-flag.json';
+import testCaseNullOperatorFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-null-operator-flag.json';
+import testCaseNullTargetingKey from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-null-targeting-key.json';
+import testCaseNumericFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-numeric-flag.json';
+import testCaseNumericOneOfDefault from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-numeric-one-of-default.json';
+import testCaseNumericOneOf from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-numeric-one-of.json';
+import testCaseOf7EmptyTargetingKey from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-of-7-empty-targeting-key.json';
+import testCaseRegexFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-regex-flag.json';
+import testCaseStartAndEndDateFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-start-and-end-date-flag.json';
+import testCaseUnknownFieldsTolerance from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-unknown-fields-tolerance.json';
+import testCaseUnknownOperatorIsolation from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-case-unknown-operator-isolation.json';
+import testFlagThatDoesNotExist from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-flag-that-does-not-exist.json';
+import testJsonConfigFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-json-config-flag.json';
+import testNoAllocationsFlag from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-no-allocations-flag.json';
+import testSpecialCharacters from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-special-characters.json';
+import testStringWithSpecialCharacters from '../packages/core/src/flags/__fixtures__/ffe-system-test-data/evaluation-cases/test-string-with-special-characters.json';
+
+type FixtureResult = {
+ reason: string;
+ value: FlagValue;
+ errorCode?: string;
+};
+
+type EvaluationCase = {
+ attributes?: Record;
+ defaultValue: FlagValue;
+ flag: string;
+ result: FixtureResult;
+ targetingKey?: string | null;
+ variationType: 'BOOLEAN' | 'STRING' | 'INTEGER' | 'NUMERIC' | 'JSON';
+};
+
+type EvaluationCaseFixture = [string, EvaluationCase[]];
+
+export type NativeFfeOfflineFixtureReport = {
+ summary: string;
+ details: {
+ caseCount: number;
+ fixtureCount: number;
+ configuration: {
+ etag?: string;
+ kind?: string;
+ version?: number;
+ };
+ serializedWireBytes: number;
+ saveState: FlagsProviderDebugState;
+ setState: FlagsProviderDebugState;
+ finalState: FlagsProviderDebugState;
+ };
+};
+
+export const NATIVE_FFE_OFFLINE_FIXTURE_SUCCESS_PREFIX =
+ 'Native FFE offline fixture pass';
+
+export const NATIVE_FFE_SHARED_RULES_WIRE = JSON.stringify({
+ version: 2,
+ server: {
+ response: JSON.stringify(ufcConfig),
+ etag: 'ffe-system-test-data',
+ },
+});
+
+const NATIVE_FFE_FIXTURE_STORAGE_OPTIONS = {
+ slot: 'offline-fixture-corpus',
+};
+
+const NUMERIC_TOLERANCE = 0.0000001;
+
+const evaluationCaseFixtures: EvaluationCaseFixture[] = [
+ [
+ 'test-case-boolean-false-assignment.json',
+ testCaseBooleanFalseAssignment as EvaluationCase[],
+ ],
+ [
+ 'test-case-boolean-one-of-matches.json',
+ testCaseBooleanOneOfMatches as EvaluationCase[],
+ ],
+ [
+ 'test-case-comparator-operator-flag.json',
+ testCaseComparatorOperatorFlag as EvaluationCase[],
+ ],
+ ['test-case-disabled-flag.json', testCaseDisabledFlag as EvaluationCase[]],
+ ['test-case-empty-flag.json', testCaseEmptyFlag as EvaluationCase[]],
+ [
+ 'test-case-empty-string-variation.json',
+ testCaseEmptyStringVariation as EvaluationCase[],
+ ],
+ [
+ 'test-case-falsy-value-assignments.json',
+ testCaseFalsyValueAssignments as EvaluationCase[],
+ ],
+ [
+ 'test-case-flag-with-empty-string.json',
+ testCaseFlagWithEmptyString as EvaluationCase[],
+ ],
+ ['test-case-integer-flag.json', testCaseIntegerFlag as EvaluationCase[]],
+ ['test-case-kill-switch-flag.json', testCaseKillSwitchFlag as EvaluationCase[]],
+ [
+ 'test-case-malformed-flag-isolation.json',
+ testCaseMalformedFlagIsolation as EvaluationCase[],
+ ],
+ [
+ 'test-case-microsecond-date-flag.json',
+ testCaseMicrosecondDateFlag as EvaluationCase[],
+ ],
+ [
+ 'test-case-missing-split-shards-isolation.json',
+ testCaseMissingSplitShardsIsolation as EvaluationCase[],
+ ],
+ [
+ 'test-case-new-user-onboarding-flag.json',
+ testCaseNewUserOnboardingFlag as EvaluationCase[],
+ ],
+ [
+ 'test-case-no-allocations-flag.json',
+ testCaseNoAllocationsFlag as EvaluationCase[],
+ ],
+ [
+ 'test-case-null-operator-flag.json',
+ testCaseNullOperatorFlag as EvaluationCase[],
+ ],
+ [
+ 'test-case-null-targeting-key.json',
+ testCaseNullTargetingKey as EvaluationCase[],
+ ],
+ ['test-case-numeric-flag.json', testCaseNumericFlag as EvaluationCase[]],
+ [
+ 'test-case-numeric-one-of-default.json',
+ testCaseNumericOneOfDefault as EvaluationCase[],
+ ],
+ ['test-case-numeric-one-of.json', testCaseNumericOneOf as EvaluationCase[]],
+ [
+ 'test-case-of-7-empty-targeting-key.json',
+ testCaseOf7EmptyTargetingKey as EvaluationCase[],
+ ],
+ ['test-case-regex-flag.json', testCaseRegexFlag as EvaluationCase[]],
+ [
+ 'test-case-start-and-end-date-flag.json',
+ testCaseStartAndEndDateFlag as EvaluationCase[],
+ ],
+ [
+ 'test-case-unknown-fields-tolerance.json',
+ testCaseUnknownFieldsTolerance as EvaluationCase[],
+ ],
+ [
+ 'test-case-unknown-operator-isolation.json',
+ testCaseUnknownOperatorIsolation as EvaluationCase[],
+ ],
+ ['test-flag-that-does-not-exist.json', testFlagThatDoesNotExist as EvaluationCase[]],
+ ['test-json-config-flag.json', testJsonConfigFlag as EvaluationCase[]],
+ ['test-no-allocations-flag.json', testNoAllocationsFlag as EvaluationCase[]],
+ ['test-special-characters.json', testSpecialCharacters as EvaluationCase[]],
+ [
+ 'test-string-with-special-characters.json',
+ testStringWithSpecialCharacters as EvaluationCase[],
+ ],
+];
+
+export async function runNativeFfeOfflineFixtureCorpus(): Promise {
+ const startingState = await DdSdkReactNative.getProviderDebugState();
+ const parsedConfiguration = await DdSdkReactNative.configurationFromString(
+ NATIVE_FFE_SHARED_RULES_WIRE,
+ );
+ const serializedWire = await DdSdkReactNative.configurationToString(
+ parsedConfiguration,
+ );
+ assertEqual(
+ serializedWire,
+ NATIVE_FFE_SHARED_RULES_WIRE,
+ 'configurationToString should preserve the original wire',
+ );
+
+ const reparsedConfiguration = await DdSdkReactNative.configurationFromString(
+ serializedWire,
+ );
+ const saveState = await DdSdkReactNative.saveConfiguration(
+ reparsedConfiguration,
+ NATIVE_FFE_FIXTURE_STORAGE_OPTIONS,
+ );
+ const loadedConfiguration = await DdSdkReactNative.loadConfiguration(
+ NATIVE_FFE_FIXTURE_STORAGE_OPTIONS,
+ );
+ const setState = await DdSdkReactNative.setConfiguration(loadedConfiguration);
+
+ let caseCount = 0;
+ for (const [fixtureName, cases] of evaluationCaseFixtures) {
+ for (let caseIndex = 0; caseIndex < cases.length; caseIndex += 1) {
+ const evaluationCase = cases[caseIndex];
+ await evaluateCase(fixtureName, caseIndex, evaluationCase);
+ caseCount += 1;
+ }
+ }
+
+ const finalState = await DdSdkReactNative.getProviderDebugState();
+ assertEqual(
+ finalState.fetchCount,
+ startingState.fetchCount,
+ 'offline fixture runner should not perform network fetches',
+ );
+ assertEqual(
+ finalState.evaluationCount - startingState.evaluationCount,
+ caseCount,
+ 'offline fixture runner should evaluate every shared case exactly once',
+ );
+
+ return {
+ summary: `${NATIVE_FFE_OFFLINE_FIXTURE_SUCCESS_PREFIX}: ${caseCount} cases across ${evaluationCaseFixtures.length} files.`,
+ details: {
+ caseCount,
+ fixtureCount: evaluationCaseFixtures.length,
+ configuration: {
+ etag: loadedConfiguration.etag,
+ kind: loadedConfiguration.kind,
+ version: loadedConfiguration.version,
+ },
+ serializedWireBytes: serializedWire.length,
+ saveState,
+ setState,
+ finalState,
+ },
+ };
+}
+
+async function evaluateCase(
+ fixtureName: string,
+ caseIndex: number,
+ evaluationCase: EvaluationCase,
+): Promise {
+ await DdSdkReactNative.setEvaluationContext(evaluationContext(evaluationCase));
+ const result = await resolveEvaluation(evaluationCase);
+ const source = `${fixtureName}[${caseIndex}]`;
+
+ assertEqual(result.flagKey, evaluationCase.flag, `${source} flagKey`);
+ assertEqual(result.reason, evaluationCase.result.reason, `${source} reason`);
+ assertJsonValue(result.value, evaluationCase.result.value, `${source} value`);
+ if (evaluationCase.result.errorCode !== undefined) {
+ assertEqual(
+ result.errorCode,
+ evaluationCase.result.errorCode,
+ `${source} errorCode`,
+ );
+ }
+}
+
+function evaluationContext(
+ evaluationCase: EvaluationCase,
+): FlagsEvaluationContext {
+ return {
+ ...(typeof evaluationCase.targetingKey === 'string'
+ ? {targetingKey: evaluationCase.targetingKey}
+ : {}),
+ attributes: evaluationCase.attributes ?? {},
+ };
+}
+
+function resolveEvaluation(
+ evaluationCase: EvaluationCase,
+): Promise {
+ switch (evaluationCase.variationType) {
+ case 'BOOLEAN':
+ return DdSdkReactNative.resolveBooleanEvaluation(
+ evaluationCase.flag,
+ evaluationCase.defaultValue as boolean,
+ );
+ case 'STRING':
+ return DdSdkReactNative.resolveStringEvaluation(
+ evaluationCase.flag,
+ evaluationCase.defaultValue as string,
+ );
+ case 'INTEGER':
+ case 'NUMERIC':
+ return DdSdkReactNative.resolveNumberEvaluation(
+ evaluationCase.flag,
+ evaluationCase.defaultValue as number,
+ );
+ case 'JSON':
+ return DdSdkReactNative.resolveObjectEvaluation(
+ evaluationCase.flag,
+ evaluationCase.defaultValue as Record,
+ );
+ }
+}
+
+function assertJsonValue(
+ actual: unknown,
+ expected: unknown,
+ message: string,
+): void {
+ if (!jsonValuesEqual(actual, expected)) {
+ throw new Error(
+ `${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
+ );
+ }
+}
+
+function assertEqual(actual: T, expected: T, message: string): void {
+ if (actual !== expected) {
+ throw new Error(`${message}: expected ${expected}, got ${actual}`);
+ }
+}
+
+function jsonValuesEqual(actual: unknown, expected: unknown): boolean {
+ if (typeof actual === 'number' && typeof expected === 'number') {
+ return Math.abs(actual - expected) <= NUMERIC_TOLERANCE;
+ }
+ if (Array.isArray(actual) && Array.isArray(expected)) {
+ return arraysEqual(actual, expected);
+ }
+ if (isRecord(actual) && isRecord(expected)) {
+ return recordsEqual(actual, expected);
+ }
+ return Object.is(actual, expected);
+}
+
+function arraysEqual(actual: unknown[], expected: unknown[]): boolean {
+ return (
+ actual.length === expected.length &&
+ actual.every((item, index) => jsonValuesEqual(item, expected[index]))
+ );
+}
+
+function recordsEqual(
+ actual: Record,
+ expected: Record,
+): boolean {
+ const actualKeys = Object.keys(actual).sort();
+ const expectedKeys = Object.keys(expected).sort();
+ return (
+ arraysEqual(actualKeys, expectedKeys) &&
+ actualKeys.every(key => jsonValuesEqual(actual[key], expected[key]))
+ );
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
diff --git a/example-new-architecture/scripts/native-ffe-offline-android-smoke.sh b/example-new-architecture/scripts/native-ffe-offline-android-smoke.sh
new file mode 100755
index 000000000..69a1b7ae0
--- /dev/null
+++ b/example-new-architecture/scripts/native-ffe-offline-android-smoke.sh
@@ -0,0 +1,149 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+EXAMPLE_DIR="$ROOT_DIR/example-new-architecture"
+ANDROID_DIR="$EXAMPLE_DIR/android"
+LOG_DIR="${NATIVE_FFE_SMOKE_LOG_DIR:-$ROOT_DIR/build/native-ffe-smoke}"
+SUCCESS_TEXT="Native FFE offline fixture pass: 233 cases across 30 files"
+PACKAGE_NAME="com.ddsdkreactnativeexample"
+ACTIVITY_NAME="$PACKAGE_NAME/.MainActivity"
+
+mkdir -p "$LOG_DIR"
+
+ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
+if [[ -z "$ANDROID_SDK_ROOT" ]]; then
+ echo "ANDROID_HOME or ANDROID_SDK_ROOT must be set." >&2
+ exit 1
+fi
+
+ADB="$ANDROID_SDK_ROOT/platform-tools/adb"
+EMULATOR="$ANDROID_SDK_ROOT/emulator/emulator"
+
+if [[ ! -x "$ADB" ]]; then
+ echo "adb not found at $ADB" >&2
+ exit 1
+fi
+
+metro_pid=""
+emulator_pid=""
+
+cleanup() {
+ if [[ -n "$metro_pid" ]]; then
+ kill "$metro_pid" >/dev/null 2>&1 || true
+ fi
+ if [[ -n "$emulator_pid" ]]; then
+ "$ADB" emu kill >/dev/null 2>&1 || true
+ fi
+}
+trap cleanup EXIT
+
+has_connected_device() {
+ "$ADB" devices | awk 'NR > 1 && $2 == "device" { found = 1 } END { exit found ? 0 : 1 }'
+}
+
+wait_for_boot() {
+ "$ADB" wait-for-device
+ for _ in $(seq 1 120); do
+ if [[ "$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]]; then
+ return 0
+ fi
+ sleep 2
+ done
+ echo "Timed out waiting for Android emulator boot." >&2
+ exit 1
+}
+
+start_emulator_if_needed() {
+ if has_connected_device; then
+ return 0
+ fi
+
+ local avd_name="${NATIVE_FFE_ANDROID_AVD:-${EMULATOR_NAME:-android_emulator}}"
+ if [[ ! -x "$EMULATOR" ]]; then
+ echo "emulator not found at $EMULATOR" >&2
+ exit 1
+ fi
+
+ "$EMULATOR" \
+ -avd "$avd_name" \
+ -no-window \
+ -no-audio \
+ -no-boot-anim \
+ -gpu swiftshader_indirect \
+ >"$LOG_DIR/emulator.log" 2>&1 &
+ emulator_pid="$!"
+ wait_for_boot
+}
+
+start_metro_if_needed() {
+ if curl -fsS "http://localhost:8081/status" 2>/dev/null | grep -q "packager-status:running"; then
+ return 0
+ fi
+
+ (
+ cd "$EXAMPLE_DIR"
+ yarn start --port 8081
+ ) >"$LOG_DIR/metro.log" 2>&1 &
+ metro_pid="$!"
+
+ for _ in $(seq 1 60); do
+ if curl -fsS "http://localhost:8081/status" 2>/dev/null | grep -q "packager-status:running"; then
+ return 0
+ fi
+ sleep 2
+ done
+
+ echo "Timed out waiting for Metro on port 8081." >&2
+ tail -100 "$LOG_DIR/metro.log" >&2 || true
+ exit 1
+}
+
+install_and_launch_app() {
+ "$ADB" reverse tcp:8081 tcp:8081 >/dev/null 2>&1 || true
+ for attempt in 1 2 3; do
+ if (
+ cd "$ANDROID_DIR"
+ ./gradlew --no-daemon :app:installDebug
+ ); then
+ break
+ fi
+ if [[ "$attempt" == "3" ]]; then
+ echo "Failed to install Android example after $attempt attempts." >&2
+ exit 1
+ fi
+ echo "Android example install failed; retrying attempt $((attempt + 1)) of 3." >&2
+ sleep 20
+ done
+ "$ADB" shell am force-stop "$PACKAGE_NAME" >/dev/null 2>&1 || true
+ "$ADB" shell am start -n "$ACTIVITY_NAME" >/dev/null
+}
+
+wait_for_success_text() {
+ for _ in $(seq 1 120); do
+ if "$ADB" shell uiautomator dump /sdcard/native_ffe_window.xml >/dev/null 2>&1; then
+ "$ADB" exec-out cat /sdcard/native_ffe_window.xml >"$LOG_DIR/window.xml" || true
+ if grep -q "$SUCCESS_TEXT" "$LOG_DIR/window.xml"; then
+ echo "$SUCCESS_TEXT"
+ return 0
+ fi
+ if grep -Eq "Native FF(&|&)E failed" "$LOG_DIR/window.xml"; then
+ echo "Native FFE offline fixture runner failed." >&2
+ grep -o "Native FF[^\"]*" "$LOG_DIR/window.xml" >&2 || true
+ exit 1
+ fi
+ fi
+ sleep 2
+ done
+
+ echo "Timed out waiting for '$SUCCESS_TEXT'." >&2
+ "$ADB" logcat -d -t 300 ReactNativeJS '*:S' >"$LOG_DIR/react-native-js.log" || true
+ tail -100 "$LOG_DIR/react-native-js.log" >&2 || true
+ exit 1
+}
+
+start_emulator_if_needed
+start_metro_if_needed
+install_and_launch_app
+wait_for_success_text
diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec
index 9fa6f9b59..a84c732b0 100644
--- a/packages/core/DatadogSDKReactNative.podspec
+++ b/packages/core/DatadogSDKReactNative.podspec
@@ -31,7 +31,7 @@ Pod::Spec.new do |s|
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'ios/Tests/**/*.{swift,json}'
- test_spec.resources = 'ios/Tests/Fixtures'
+ test_spec.resources = ['ios/Tests/Fixtures', 'src/flags/__fixtures__/ffe-system-test-data']
test_spec.platforms = { :ios => "13.4", :tvos => "13.4" }
end
diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts
index db10d7f31..626ccbdcd 100644
--- a/packages/core/__mocks__/react-native.ts
+++ b/packages/core/__mocks__/react-native.ts
@@ -15,6 +15,67 @@ import type { DdTraceType } from '../src/types';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const actualRN = require('react-native');
+const mockFlagsDebugState = {
+ status: 'ready',
+ activeConfigurationKind: 'rules',
+ activeEtag: 'ffe-system-test-data',
+ configurationSetCount: 1,
+ configurationSaveCount: 0,
+ configurationLoadCount: 0,
+ fetchCount: 1,
+ evaluationCount: 0,
+ lastEvent: 'provider_ready',
+ lastFetchRequest: {
+ url: 'https://mock.datadog.test/config',
+ method: 'GET',
+ headers: {
+ Accept: 'application/json'
+ },
+ statusCode: 200
+ },
+ evaluationSideEffects: {
+ attemptedCount: 0,
+ trackedCount: 0,
+ skippedCount: 0,
+ failedCount: 0,
+ lastStatus: 'skipped'
+ }
+};
+
+const mockConfigurationFromString = (wire: string) => {
+ const parsed = JSON.parse(wire);
+ const kind =
+ parsed.server && parsed.precomputed
+ ? 'mixed'
+ : parsed.server
+ ? 'rules'
+ : 'precomputed';
+
+ return {
+ __ddNativeFfeConfiguration: true,
+ version: parsed.version,
+ kind,
+ etag: parsed.server?.etag ?? parsed.precomputed?.etag,
+ wire
+ };
+};
+
+const mockFetchConfiguration = (
+ kind: 'precomputed' | 'rules',
+ options: { previousConfigurationWire?: string }
+) => {
+ return mockConfigurationFromString(
+ options.previousConfigurationWire ??
+ JSON.stringify({
+ version: 2,
+ [kind === 'rules' ? 'server' : 'precomputed']: {
+ response: '{}',
+ etag: 'mock-fetch'
+ }
+ })
+ );
+};
+
actualRN.NativeModules.DdSdk = {
initialize: jest.fn().mockImplementation(
() => new Promise(resolve => resolve())
@@ -58,6 +119,114 @@ actualRN.NativeModules.DdSdk = {
clearAllData: jest.fn().mockImplementation(
() => new Promise(resolve => resolve())
) as jest.MockedFunction,
+ configurationFromString: jest.fn().mockImplementation(
+ (wire: string) =>
+ new Promise