diff --git a/.github/chainguard/self.precomputed-fixtures.sts.yaml b/.github/chainguard/self.precomputed-fixtures.sts.yaml new file mode 100644 index 0000000000..10df730638 --- /dev/null +++ b/.github/chainguard/self.precomputed-fixtures.sts.yaml @@ -0,0 +1,9 @@ +issuer: https://token.actions.githubusercontent.com +subject: repo:DataDog/dd-sdk-android:ref:refs/heads/develop +claim_pattern: + event_name: "(schedule|workflow_dispatch)" + workflow_ref: 'DataDog/dd-sdk-android/\.github/workflows/precomputed-fixtures\.yml@refs/heads/develop' + repository: "DataDog/dd-sdk-android" +permissions: + contents: write + pull_requests: write diff --git a/.github/workflows/precomputed-fixtures.yml b/.github/workflows/precomputed-fixtures.yml new file mode 100644 index 0000000000..57e4e657b9 --- /dev/null +++ b/.github/workflows/precomputed-fixtures.yml @@ -0,0 +1,86 @@ +name: Precomputed flag fixtures +on: + pull_request: + paths: + - 'ci/*precomputed*' + - 'reliability/single-fit/flags/src/test/resources/precomputed/**' + - '.github/workflows/precomputed-fixtures.yml' + schedule: + - cron: '23 8 * * 1' + workflow_dispatch: +permissions: + contents: read +concurrency: + group: precomputed-fixtures-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: false +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Verify the local snapshot + run: python3 ci/sync-precomputed-fixtures.py --verify + - name: Compare with the recorded upstream commit + run: python3 ci/sync-precomputed-fixtures.py --check + update: + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + steps: + - uses: DataDog/dd-octo-sts-action@c85802a58e202b7e764b5441e4378c76043c8108 # v1.0.5 + id: token + with: + scope: DataDog/dd-sdk-android + policy: self.precomputed-fixtures + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Import upstream fixtures + id: sync + run: python3 ci/sync-precomputed-fixtures.py --ref main + - name: Prepare a draft update + id: prepare + if: steps.sync.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + SOURCE_COMMIT: ${{ steps.sync.outputs.source_commit }} + run: | + existing=$(gh pr list --state open --search '"Update precomputed flag fixtures" in:title' --json number --jq 'length') + if [ "$existing" != 0 ]; then + echo 'An update PR is already open. Merge or close it before the next update.' + exit 0 + fi + branch="automation/precomputed-fixtures-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + git add -- 'reliability/single-fit/flags/src/test/resources/precomputed' + echo "branch=$branch" >> "$GITHUB_OUTPUT" + echo 'ready=true' >> "$GITHUB_OUTPUT" + python3 - <<'BODY' + import os + from pathlib import Path + template = Path('ci/precomputed-fixtures-pr.md').read_text() + Path('/tmp/precomputed-pr.md').write_text(template.replace('SOURCE_COMMIT', os.environ['SOURCE_COMMIT'])) + BODY + - name: Create a signed fixture commit + if: steps.prepare.outputs.ready == 'true' + uses: DataDog/commit-headless@d6afabfd690ea1dbdd7d368acbefd71304928699 # action/v3.4.1 + with: + command: commit + token: ${{ steps.token.outputs.token }} + branch: ${{ steps.prepare.outputs.branch }} + head-sha: ${{ github.sha }} + create-branch: true + force: false + message: 'FFL-3568: Update precomputed flag fixtures' + - name: Open the draft pull request + if: steps.prepare.outputs.ready == 'true' + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + UPDATE_BRANCH: ${{ steps.prepare.outputs.branch }} + run: | + gh pr create --draft --base develop --head "$UPDATE_BRANCH" --title 'FFL-3568: Update precomputed flag fixtures' --body-file /tmp/precomputed-pr.md diff --git a/ci/precomputed-fixtures-pr.md b/ci/precomputed-fixtures-pr.md new file mode 100644 index 0000000000..59e09253e6 --- /dev/null +++ b/ci/precomputed-fixtures-pr.md @@ -0,0 +1,17 @@ +### What does this PR do? + +Refresh the shared precomputed response fixtures to `SOURCE_COMMIT`. + +### Motivation + +Keep mobile response tests aligned with the canonical JSON contract. Ref: FFL-3568. + +### Additional Notes + +The existing fixture loop runs new cases in normal SDK CI. Review those results before merge. + +### Review checklist (to be filled by reviewers) + +- [ ] Feature or bugfix MUST have appropriate tests (unit, integration, e2e) +- [ ] Make sure you discussed the feature or bugfix with the maintaining team in an Issue +- [ ] Make sure each commit and the PR mention the Issue number (cf the [CONTRIBUTING](../CONTRIBUTING.md) doc) diff --git a/ci/precomputed-fixtures.json b/ci/precomputed-fixtures.json new file mode 100644 index 0000000000..33727dd641 --- /dev/null +++ b/ci/precomputed-fixtures.json @@ -0,0 +1,3 @@ +{ + "destination": "reliability/single-fit/flags/src/test/resources/precomputed" +} diff --git a/ci/sync-precomputed-fixtures.py b/ci/sync-precomputed-fixtures.py new file mode 100755 index 0000000000..0ad58fb521 --- /dev/null +++ b/ci/sync-precomputed-fixtures.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# Unless explicitly stated otherwise, all files in this repository are licensed under the +# Apache License Version 2.0. This product includes software developed at Datadog +# (https://www.datadoghq.com/). Copyright 2025-Present Datadog, Inc. +"""Import a reproducible precompute test snapshot; never edit imported JSON.""" +import argparse +import hashlib +import json +import os +import re +import subprocess +import tempfile +import uuid +from pathlib import Path + +REPOSITORY = 'https://github.com/DataDog/ffe-system-test-data.git' +PREFIX = 'precomputed-assignments/' +FILES = { + 'schemas/precomputed-assignment.schema.json': 'schema.json', + 'precomputed-assignments/README.md': 'README.md', + 'LICENSE': 'LICENSE', + 'NOTICE': 'NOTICE', +} + + +def git(source, *args): + return subprocess.check_output(['git', '-C', str(source), *args]) + + +def response_bytes(content): + """Extract the top-level response without re-encoding any numeric token.""" + text = content.decode('utf-8') + decoder = json.JSONDecoder() + position = text.index('{') + 1 + while True: + while text[position].isspace() or text[position] == ',': + position += 1 + if text[position] == '}': + raise ValueError('Fixture is missing response') + key, position = decoder.raw_decode(text, position) + while text[position].isspace(): + position += 1 + if text[position] != ':': + raise ValueError('Invalid fixture object') + position += 1 + while text[position].isspace(): + position += 1 + start = position + _, position = decoder.raw_decode(text, position) + if key == 'response': + return text[start:position].encode('utf-8') + b'\n' + + +def snapshot(source, ref): + if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', ref) or '..' in ref: + raise ValueError('Invalid fixture revision') + sha = git(source, 'rev-parse', '--verify', ref + '^{commit}').decode().strip() + if not re.fullmatch(r'[0-9a-f]{40}', sha): + raise ValueError('Expected a full source commit SHA') + result = {} + for entry in git(source, 'ls-tree', '-rz', '--full-tree', sha).split(b'\0'): + if not entry: + continue + metadata, raw_path = entry.split(b'\t', 1) + path = raw_path.decode() + target = FILES.get(path) + if path.startswith(PREFIX + 'cases/') and path.endswith('.json'): + target = path[len(PREFIX):] + if target is None: + continue + mode, kind, blob = metadata.decode().split() + if mode != '100644' or kind != 'blob': + raise ValueError('Fixture entries must be ordinary files: ' + path) + result[target] = git(source, 'cat-file', 'blob', blob) + cases = sorted(p for p in result if p.startswith('cases/')) + if not cases or not all(p in result for p in FILES.values()): + raise ValueError('Missing or empty precomputed fixture corpus') + for path in cases: + data = json.loads(result[path]) + if not isinstance(data, dict) or data.get('name') != Path(path).stem: + raise ValueError('Invalid fixture identity: ' + path) + if not isinstance(data.get('evaluations'), list): + raise TypeError('Missing evaluations: ' + path) + result['responses/' + path] = response_bytes(result[path]) + result['index.json'] = (json.dumps(cases, indent=2) + '\n').encode() + result['SOURCE.json'] = (json.dumps({ + 'repository': REPOSITORY, + 'commit': sha, + 'files': {p: hashlib.sha256(v).hexdigest() for p, v in sorted(result.items())}, + }, indent=2) + '\n').encode() + return result + + +def read_snapshot(destination): + if not destination.exists(): + return {} + result = {} + for path in destination.rglob('*'): + if path.is_symlink(): + raise ValueError('Snapshot must not contain symbolic links') + if path.is_file() and path.suffix != '.meta': + result[path.relative_to(destination).as_posix()] = path.read_bytes() + return result + + +def verify(destination): + content = read_snapshot(destination) + metadata = json.loads(content.pop('SOURCE.json')) + if metadata['repository'] != REPOSITORY or not re.fullmatch(r'[0-9a-f]{40}', metadata['commit']): + raise ValueError('Invalid source provenance') + hashes = {p: hashlib.sha256(v).hexdigest() for p, v in sorted(content.items())} + if hashes != metadata['files']: + raise ValueError('Snapshot content differs from its recorded checksums') + cases = sorted(p for p in content if p.startswith('cases/') and p.endswith('.json')) + if not cases or json.loads(content['index.json']) != cases: + raise ValueError('Missing, empty, or stale fixture index') + print(f"Verified {len(cases)} fixtures at {metadata['commit']}") + + +def unity_metadata(destination): + for asset in sorted(destination.rglob('*')): + if asset.suffix == '.meta': + continue + meta = asset.with_name(asset.name + '.meta') + if not meta.exists(): + guid = uuid.uuid5(uuid.NAMESPACE_URL, 'ffe-precomputed/' + asset.relative_to(destination).as_posix()).hex + folder = 'folderAsset: yes\n' if asset.is_dir() else '' + importer = 'DefaultImporter' if asset.is_dir() or asset.suffix != '.json' else 'TextScriptImporter' + meta.write_text(f'fileFormatVersion: 2\nguid: {guid}\n{folder}{importer}:\n externalObjects: {{}}\n userData:\n assetBundleName:\n assetBundleVariant:\n') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--ref', default='main') + parser.add_argument('--source', type=Path, help='Read a local Git checkout at the requested committed revision') + parser.add_argument('--verify', action='store_true', help='Verify local checksums and discovery index without network access') + parser.add_argument('--check', action='store_true', help='Compare with the recorded upstream SHA; do not write') + args = parser.parse_args() + root = Path(__file__).resolve().parent.parent + config = json.loads((root / 'ci/precomputed-fixtures.json').read_text()) + relative = Path(config['destination']) + if relative.is_absolute() or '..' in relative.parts: + raise ValueError('Destination must be inside the repository') + destination = root / relative + resolved = destination.resolve() + if not resolved.is_relative_to(root): + raise ValueError('Destination escapes the repository') + if resolved == root or '.git' in relative.parts: + raise ValueError('Destination must be a dedicated directory inside the repository') + if resolved != destination: + raise ValueError('Destination must not contain symbolic links') + if destination.exists(): + if not destination.is_dir(): + raise ValueError('Destination must be a directory') + if any(destination.iterdir()): + provenance = destination / 'SOURCE.json' + if not provenance.is_file(): + raise ValueError('Destination must be empty or contain a previous snapshot') + metadata = json.loads(provenance.read_text()) + if metadata.get('repository') != REPOSITORY or not isinstance(metadata.get('files'), dict): + raise ValueError('Destination must contain valid snapshot provenance') + extra = read_snapshot(destination).keys() - metadata['files'].keys() - {'SOURCE.json'} + if extra: + raise ValueError('Destination contains files outside the previous snapshot') + if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', args.ref) or '..' in args.ref: + raise ValueError('Invalid fixture revision') + if args.verify: + verify(destination) + return + before = read_snapshot(destination) + ref = json.loads(before['SOURCE.json'])['commit'] if args.check else args.ref + with tempfile.TemporaryDirectory(prefix='ffe-precompute-') as temporary: + source = args.source + if source is None: + source = Path(temporary) / 'source' + subprocess.run(['git', 'init', '-q', str(source)], check=True) + subprocess.run(['git', '-C', str(source), 'fetch', '-q', '--depth=1', REPOSITORY, ref], check=True) + ref = 'FETCH_HEAD' + after = snapshot(source, ref) + changed = {p:v for p,v in before.items() if p != 'SOURCE.json'} != {p:v for p,v in after.items() if p != 'SOURCE.json'} + if args.check: + if before != after: + raise ValueError('Snapshot differs from its recorded source commit') + elif changed: + destination.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='.precompute-', dir=destination.parent) as staging: + staged = Path(staging) / 'snapshot' + staged.mkdir() + for path, content in after.items(): + output = staged / path + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(content) + # Preserve Unity's existing asset identities when refreshing a snapshot. + if destination.exists(): + for meta in destination.rglob('*.meta'): + rel = meta.relative_to(destination) + asset = staged / str(rel)[:-5] + if asset.exists(): + (staged / rel).write_bytes(meta.read_bytes()) + backup = Path(staging) / 'previous' + destination.rename(backup) + try: + staged.rename(destination) + except BaseException: + backup.rename(destination) + raise + else: + staged.rename(destination) + if config.get('unityMetadata') and not args.check: + unity_metadata(destination) + selected = before if args.check or not changed else after + metadata = json.loads(selected['SOURCE.json']) + count = len(json.loads(selected['index.json'])) + print(f"FFE precomputed fixtures: {metadata['commit']}; {count} files; changed={str(changed).lower()}") + if os.environ.get('GITHUB_OUTPUT'): + with open(os.environ['GITHUB_OUTPUT'], 'a') as output: + output.write(f"changed={str(changed).lower()}\nsource_commit={metadata['commit']}\nfixture_count={count}\n") + + +if __name__ == '__main__': + main() diff --git a/dd-sdk-android-core/build.gradle.kts b/dd-sdk-android-core/build.gradle.kts index 5213782b81..31166fc47b 100644 --- a/dd-sdk-android-core/build.gradle.kts +++ b/dd-sdk-android-core/build.gradle.kts @@ -146,14 +146,7 @@ dependencies { testFixturesImplementation(libs.okHttp) testFixturesImplementation(libs.bundles.testTools) testFixturesImplementation(project(":dd-sdk-android-internal")) - testFixturesImplementation(project(":tools:unit")) { - attributes { - attribute( - com.android.build.api.attributes.ProductFlavorAttr.of("platform"), - objects.named("jvm") - ) - } - } + testFixturesImplementation(testFixtures(project(":dd-sdk-android-internal"))) } unMock { diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt index da0c04c86b..4f12dcbf4c 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/AccountInfoForgeryFactory.kt @@ -7,6 +7,7 @@ package com.datadog.android.tests.elmyr import com.datadog.android.api.context.AccountInfo +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt index 94eaa61af8..7de45b3b82 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/DatadogContextForgeryFactory.kt @@ -8,6 +8,7 @@ package com.datadog.android.tests.elmyr import com.datadog.android.DatadogSite import com.datadog.android.api.context.DatadogContext +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.privacy.TrackingConsent import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt index bb35a06a32..477898f1bc 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/ForgeExt.kt @@ -13,40 +13,9 @@ import fr.xgouchet.elmyr.Forge import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import org.json.JSONArray -import org.json.JSONObject -import java.io.File -import java.util.Date -import java.util.Locale -import java.util.TimeZone const val URL_FORGERY_PATTERN = "http(s?)://[a-z]+\\.com/[a-z]+" -fun Forge.exhaustiveAttributes( - excludedKeys: Set = emptySet() -): MutableMap { - return listOf( - aBool(), - anInt(), - aLong(), - aFloat(), - aDouble(), - anAsciiString(), - getForgery(), - getForgery(), - getForgery(), - getForgery(), - getForgery(), - getForgery(), - aList { anAlphabeticalString() }, - aList { aDouble() }, - null - ) - .associateBy { anAlphaNumericalString() } - .filter { it.key !in excludedKeys } - .toMutableMap() -} - fun T.useCoreFactories(): T { addFactory(DatadogContextForgeryFactory()) addFactory(DeviceInfoForgeryFactory()) diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt index ac4e6bb911..6277c379fb 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/UserInfoForgeryFactory.kt @@ -7,6 +7,7 @@ package com.datadog.android.tests.elmyr import com.datadog.android.api.context.UserInfo +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/utils/DatadogExt.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/utils/DatadogExt.kt new file mode 100644 index 0000000000..e1c3a1f2c7 --- /dev/null +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/utils/DatadogExt.kt @@ -0,0 +1,24 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.utils + +import com.datadog.android.Datadog +import com.datadog.android.api.SdkCore + +/** + * Adds [SdkCore] instance to [Datadog]. + */ +fun Datadog.registerInstance(name: String?, sdkCore: SdkCore) { + registry.register(name, sdkCore) +} + +/** + * Removes all [SdkCore] instances registered. + */ +fun Datadog.clearInstances() { + registry.clear() +} diff --git a/dd-sdk-android-internal/build.gradle.kts b/dd-sdk-android-internal/build.gradle.kts index 4e3c77723d..f8a8c1cbfe 100644 --- a/dd-sdk-android-internal/build.gradle.kts +++ b/dd-sdk-android-internal/build.gradle.kts @@ -63,16 +63,9 @@ dependencies { } } testFixturesImplementation(libs.kotlinRuntime) + testFixturesImplementation(libs.gson) testFixturesImplementation(libs.bundles.jUnit5) testFixturesImplementation(libs.bundles.testTools) - testFixturesImplementation(project(":tools:unit")) { - attributes { - attribute( - com.android.build.api.attributes.ProductFlavorAttr.of("platform"), - objects.named("jvm") - ) - } - } unmock(libs.robolectric) } diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ErrorForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ErrorForgeryFactory.kt new file mode 100644 index 0000000000..40dfdd93df --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ErrorForgeryFactory.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [Error] instance with a forged message. + */ +class ErrorForgeryFactory : + ForgeryFactory { + /** @inheritDoc */ + override fun getForgery(forge: Forge): Error { + return forge.anError() + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ExceptionForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ExceptionForgeryFactory.kt new file mode 100644 index 0000000000..800d028cc2 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ExceptionForgeryFactory.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [Exception] instance with a forged message. + */ +class ExceptionForgeryFactory : + ForgeryFactory { + /** @inheritDoc */ + override fun getForgery(forge: Forge): Exception { + return forge.anException() + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ForgeExt.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ForgeExt.kt new file mode 100644 index 0000000000..42092b53a4 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ForgeExt.kt @@ -0,0 +1,136 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import android.annotation.SuppressLint +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryException +import org.json.JSONArray +import org.json.JSONObject +import org.junit.jupiter.api.Assumptions +import java.io.EOFException +import java.io.File +import java.io.FileNotFoundException +import java.io.IOError +import java.io.IOException +import java.io.UncheckedIOException +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +/** + * @return a random [Throwable] instance with a forged message. + */ +fun Forge.aThrowable(): Throwable { + return anElementFrom( + anError(), + anException() + ) +} + +/** + * @return a random [Error] instance with a forged message. + */ +fun Forge.anError(): Error { + val errorMessage = anAlphabeticalString() + return anElementFrom( + UnknownError(errorMessage), + IOError(RuntimeException(errorMessage)), + NotImplementedError(errorMessage), + StackOverflowError(errorMessage), + OutOfMemoryError(errorMessage) + ) +} + +/** + * @return a random [Exception] instance with a forged message. + * Because Mockito will prevent throwing undeclared checked exceptions, + * all the exceptions here must extend [RuntimeException]. + * [UncheckedIOException] is a wrapper around a checked [IOException], and is considered a new + * API (only available in Android 24+); we ignore the warning since tests are run on the JVM. + */ +@SuppressLint("NewApi") +fun Forge.anException(): Exception { + val errorMessage = anAlphabeticalString() + return anElementFrom( + IndexOutOfBoundsException(errorMessage), + ArithmeticException(errorMessage), + IllegalStateException(errorMessage), + ArrayIndexOutOfBoundsException(errorMessage), + NullPointerException(errorMessage), + ForgeryException(errorMessage), + UnsupportedOperationException(errorMessage), + SecurityException(errorMessage), + UncheckedIOException(IOException(errorMessage)), + UncheckedIOException(FileNotFoundException(errorMessage)), + UncheckedIOException(EOFException(errorMessage)) + ) +} + +/** + * Will generate a map with different value types with a possibility to filter out given keys. + * + * @param excludedKeys Keys to exclude from generated map. + * @param filterThreshold Max ratio of keys removed from originally generated map. If ratio + * is more than that, [Assumptions.assumeTrue] mechanism will be used. + */ +fun Forge.exhaustiveAttributes( + excludedKeys: Set = emptySet(), + filterThreshold: Float = 0.5f +): MutableMap { + val map = generateMapWithExhaustiveValues(this).toMutableMap() + + map[""] = anHexadecimalString() + map[aWhitespaceString()] = anHexadecimalString() + map[anAlphabeticalString()] = generateMapWithExhaustiveValues(this).toMutableMap().apply { + this[anAlphabeticalString()] = generateMapWithExhaustiveValues(this@exhaustiveAttributes) + } + + val filtered = map.filterKeys { it !in excludedKeys } + + assumeDifferenceIsNoMore(filtered.size, map.size, filterThreshold) + + return filtered.toMutableMap() +} + +private fun assumeDifferenceIsNoMore(result: Int, base: Int, maxDifference: Float) { + check(result <= base) { + "Number of elements after filtering cannot exceed the number of original elements." + } + + val diff = (base - result).toFloat() / base + Assumptions.assumeTrue( + diff <= maxDifference, + "Too many elements removed, condition cannot be satisfied." + ) +} + +private fun generateMapWithExhaustiveValues(forge: Forge): MutableMap { + return forge.run { + listOf( + aBool(), + anInt(), + aLong(), + aFloat(), + aDouble(), + anAsciiString(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + getForgery(), + aList { anAlphabeticalString() }, + aList { aDouble() }, + null + ).associateByTo(mutableMapOf()) { anAlphaNumericalString() } + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonArrayForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonArrayForgeryFactory.kt new file mode 100644 index 0000000000..7af3efd12a --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonArrayForgeryFactory.kt @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.google.gson.JsonArray +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [JsonArray] instance. + */ +class GsonJsonArrayForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JsonArray { + return forge.anElementFrom( + JsonArray(), + forge.aStringArray(), + forge.anIntArray(), + forge.aDoubleArray(), + forge.aBooleanArray() + ) + } + + // region Internal + + private fun Forge.aStringArray(): JsonArray { + return JsonArray().apply { + aList { anAlphabeticalString() } + .forEach { add(it) } + } + } + + private fun Forge.anIntArray(): JsonArray { + return JsonArray().apply { + aList { anInt() } + .forEach { add(it) } + } + } + + private fun Forge.aDoubleArray(): JsonArray { + return JsonArray().apply { + aList { aDouble() } + .forEach { add(it) } + } + } + + private fun Forge.aBooleanArray(): JsonArray { + return JsonArray().apply { + aList { aBool() } + .forEach { add(it) } + } + } + + // endregion +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonObjectForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonObjectForgeryFactory.kt new file mode 100644 index 0000000000..e960fc8cca --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonObjectForgeryFactory.kt @@ -0,0 +1,35 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [JsonObject] instance. + */ +class GsonJsonObjectForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JsonObject { + return JsonObject().apply { + val fieldCount = forge.aTinyInt() + repeat(fieldCount + 1) { + add( + forge.anAlphabeticalString(), + forge.anElementFrom( + forge.getForgery(), + forge.getForgery() + ) + ) + } + } + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonPrimitiveForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonPrimitiveForgeryFactory.kt new file mode 100644 index 0000000000..1d839cab56 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/GsonJsonPrimitiveForgeryFactory.kt @@ -0,0 +1,32 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import com.google.gson.JsonPrimitive +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [JsonPrimitive] instance. + */ +class GsonJsonPrimitiveForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JsonPrimitive { + return forge.anElementFrom( + JsonPrimitive(forge.aBool()), + JsonPrimitive(forge.anInt()), + JsonPrimitive(forge.aFloat()), + JsonPrimitive(forge.aLong()), + JsonPrimitive(forge.aDouble()), + JsonPrimitive(forge.anAlphabeticalString()), + JsonPrimitive(forge.anHexadecimalString()), + JsonPrimitive(forge.aNumericalString()), + JsonPrimitive("") + ) + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt index 843bcc581c..2120dd9522 100644 --- a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/InternalTelemetryErrorLogForgeryFactory.kt @@ -7,7 +7,6 @@ package com.datadog.android.internal.tests.elmyr import com.datadog.android.internal.telemetry.InternalTelemetryEvent -import com.datadog.tools.unit.forge.aThrowable import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONArrayForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONArrayForgeryFactory.kt new file mode 100644 index 0000000000..b279c402c4 --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONArrayForgeryFactory.kt @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import org.json.JSONArray + +/** + * A [ForgeryFactory] generating a random [JSONArray] instance. + */ +class OrgJSONArrayForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JSONArray { + return forge.anElementFrom( + JSONArray(), + forge.aStringArray(), + forge.anIntArray(), + forge.aDoubleArray(), + forge.aBooleanArray() + ) + } + + // region Internal + + private fun Forge.aStringArray(): JSONArray { + return JSONArray().apply { + aList { anAlphabeticalString() } + .forEach { put(it) } + } + } + + private fun Forge.anIntArray(): JSONArray { + return JSONArray().apply { + aList { anInt() } + .forEach { put(it) } + } + } + + private fun Forge.aDoubleArray(): JSONArray { + return JSONArray().apply { + aList { aDouble() } + .forEach { put(it) } + } + } + + private fun Forge.aBooleanArray(): JSONArray { + return JSONArray().apply { + aList { aBool() } + .forEach { put(it) } + } + } + + // endregion +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONObjectForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONObjectForgeryFactory.kt new file mode 100644 index 0000000000..c8c7577e4f --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/OrgJSONObjectForgeryFactory.kt @@ -0,0 +1,38 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import org.json.JSONArray +import org.json.JSONObject + +/** + * A [ForgeryFactory] generating a random [JSONObject] instance. + */ +class OrgJSONObjectForgeryFactory : ForgeryFactory { + + /** @inheritDoc */ + override fun getForgery(forge: Forge): JSONObject { + return JSONObject().apply { + val fieldCount = forge.aTinyInt() + repeat(fieldCount) { + put( + forge.anAlphabeticalString(), + forge.anElementFrom( + forge.anInt(), + forge.aLong(), + forge.aBool(), + forge.anAlphaNumericalString(), + forge.aDouble(), + forge.getForgery() + ) + ) + } + } + } +} diff --git a/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ThrowableForgeryFactory.kt b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ThrowableForgeryFactory.kt new file mode 100644 index 0000000000..067fd648aa --- /dev/null +++ b/dd-sdk-android-internal/src/testFixtures/kotlin/com/datadog/android/internal/tests/elmyr/ThrowableForgeryFactory.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.internal.tests.elmyr + +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +/** + * A [ForgeryFactory] generating a random [Throwable] instance with a forged message. + */ +class ThrowableForgeryFactory : + ForgeryFactory { + /** @inheritDoc */ + override fun getForgery(forge: Forge): Throwable { + return forge.aThrowable() + } +} diff --git a/detekt_custom_safe_calls_third_party.yml b/detekt_custom_safe_calls_third_party.yml index 7f4b7a3b20..be58e9b384 100644 --- a/detekt_custom_safe_calls_third_party.yml +++ b/detekt_custom_safe_calls_third_party.yml @@ -367,6 +367,7 @@ datadog: - "java.lang.StringBuilder.append(kotlin.String?)" - "java.lang.StringBuilder.clear()" - "java.lang.StringBuilder.constructor()" + - "java.math.BigDecimal.compareTo(java.math.BigDecimal?)" - "java.math.BigDecimal.constructor(kotlin.Int)" - "java.math.BigDecimal.scale()" - "java.math.BigDecimal.signum()" @@ -846,6 +847,7 @@ datadog: - "kotlin.enums.EnumEntries.associateBy(kotlin.Function1)" - "kotlin.enums.EnumEntries.firstOrNull(kotlin.Function1)" - "kotlin.enums.EnumEntries.forEach(kotlin.Function1)" + - "kotlin.enums.EnumEntries.none(kotlin.Function1)" - "kotlin.enums.EnumEntries.toSet()" - "kotlin.ranges.IntProgression.asSequence()" - "kotlin.sequences.Sequence.groupBy(kotlin.Function1)" @@ -886,6 +888,7 @@ datadog: - "kotlin.Char.isLowSurrogate()" - "kotlin.Char.titlecase(java.util.Locale)" - "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)" + - "kotlin.Double.isFinite()" - "kotlin.Double.coerceAtMost(kotlin.Double)" - "kotlin.Double.coerceIn(kotlin.Double, kotlin.Double)" - "kotlin.Double.isNaN()" diff --git a/detekt_custom_unsafe_calls.yml b/detekt_custom_unsafe_calls.yml index ea2c4d36d1..13578d493e 100644 --- a/detekt_custom_unsafe_calls.yml +++ b/detekt_custom_unsafe_calls.yml @@ -167,6 +167,7 @@ datadog: - "java.lang.System.loadLibrary(kotlin.String?):java.lang.SecurityException,java.lang.UnsatisfiedLinkError,java.lang.NullPointerException" - "java.lang.reflect.Field.get(kotlin.Any):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" - "java.lang.reflect.Field.get(kotlin.Any?):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError" + - "java.math.BigDecimal.constructor(kotlin.Double):java.lang.NumberFormatException" - "java.math.BigDecimal.stripTrailingZeros():java.lang.ArithmeticException" - "java.math.BigInteger.and(java.math.BigInteger?):java.lang.NumberFormatException,java.lang.ArithmeticException" - "java.math.BigInteger.constructor(kotlin.String?, kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException" diff --git a/features/dd-sdk-android-flags/README.md b/features/dd-sdk-android-flags/README.md index 95b9b57d87..bf6be3c354 100644 --- a/features/dd-sdk-android-flags/README.md +++ b/features/dd-sdk-android-flags/README.md @@ -93,6 +93,26 @@ val flagsConfig = FlagsConfiguration.Builder() .build() ``` +#### Load only selected flags + +To load only the flags that your app uses, set a flag selection. The SDK sends it with the flag request, and the server returns only the flags that have any of the tags or whose key matches any of the key patterns: + +```kotlin +val flagsConfig = FlagsConfiguration.Builder() + .flagSelection( + tags = listOf("mobile"), + keyPatterns = listOf("checkout-*") + ) + .build() +``` + +- Tags match exactly and are case-sensitive. A key pattern matches the whole flag key, and only `*` is a wildcard. +- Each list accepts at most 31 values, and each value is at most 200 UTF-8 bytes. The SDK does not send a selection that is over a limit. It logs an error and loads the full flag configuration. +- When the server applies the selection, evaluating a flag outside it returns the default value with `ErrorCode.FLAG_NOT_FOUND`. +- The flag selection is fixed when the Flags feature is enabled and applies to every `FlagsClient`. Flags cached under a different selection are not loaded at startup; until the next successful fetch, a client starts without cached flags. +- If the server ignores a selection, the SDK loads the full flag configuration and logs an error with the reason. A reason that the SDK does not know is reported as `unknown`. +- With a custom flag endpoint, the proxy must forward the `DD-Flag-Selection-Ignored` response header. Otherwise, the SDK cannot report a selection that the server ignored. + ## Use the Feature Flags SDK ### Create a Flags client @@ -270,7 +290,7 @@ val maxRetries = client.resolveIntValue("max-retry-count", 3) val discountPercentage = client.resolveDoubleValue("discount-rate", 0.0) ``` -Any numeric flag can be read with either method. `resolveIntValue` returns the value only when it is a whole number within `Int` range, such as `5` or `5.0`. Otherwise, it returns the default value: `resolve()`, reports `TYPE_MISMATCH` when the value is fractional, such as `1.2`, or outside `Int` range, such as `2147483648`, and `PARSE_ERROR` when the value can't be parsed. To bound parsing work, a value longer than 64 characters is not parsed unless it is a plain integer within `Int` range, so it reports `PARSE_ERROR`. `resolveDoubleValue` returns a fractional value as the nearest `Double`, such as `1.2`. It returns a whole value only when the `Double` has exactly that value, or prints as that value, such as `1e300`. Otherwise, `resolve()` reports `TYPE_MISMATCH`, such as for `9007199254740993`. `NaN` and `Infinity` report `PARSE_ERROR`. The same applies to a value longer than 64 characters whose `Double` is 2^53 or more in magnitude. +Any numeric flag can be read with either method. `resolveIntValue` accepts whole original values within `Int` range, such as `5` or `5.0`. Fractional or out-of-range values return the default with `TYPE_MISMATCH`. `resolveDoubleValue` rounds fractional values to the nearest finite `Double`, such as `1.20000000000000001` to `1.2`; whole values must be represented exactly. `9007199254740993` and `1e300` therefore return `TYPE_MISMATCH`, as do overflow and nonzero underflow. Malformed numeric text, including `NaN` and `Infinity`, reports `PARSE_ERROR`. Decimal parsing is bounded to 64 characters per value; longer values report `PARSE_ERROR`. These rules apply to `integer`, `float`, and `number` flags, including cached assignments. **Structured flags** ```kotlin @@ -434,3 +454,49 @@ For more information on Feature Flags in Datadog, see the [official Feature Flag [1]: https://docs.datadoghq.com/real_user_monitoring/application_monitoring/android/setup [2]: https://docs.datadoghq.com/getting_started/feature_flags/ + +## Shared precomputed response tests + +The Flags module uses the JSON contract from [ffe-system-test-data](https://github.com/DataDog/ffe-system-test-data/tree/main/precomputed-assignments). +This keeps response behavior consistent across mobile SDKs. The snapshot is test data only. +It adds no runtime dependency and requires no Git submodule. + +The generated snapshot is at `reliability/single-fit/flags/src/test/resources/precomputed`. `SOURCE.json` records the source commit and file digests. +The runner discovers all nested JSON cases through the generated index. +Adding a case requires an upstream fixture change and snapshot refresh. It requires no runner or filename-list change. +Missing or empty fixtures fail the suite. Shared `_include` and `_skip` fields describe API applicability. +Do not add local exclusions to hide failures. + +Numeric behavior follows the [shared numeric contract](https://github.com/DataDog/ffe-system-test-data/blob/main/precomputed-assignments/README.md#mobile-conformance-and-numeric-types). +The native context API requires a string targeting key. Fixtures with `targetingKey: null` must declare this platform in `_skip` upstream; the runner reports an actionable fixture error if an applicable case violates that requirement. + +Upstream CI requires a generated obfuscated companion for each applicable plain fixture. It rejects missing, stale, or orphaned companions. Edit the plain case upstream and regenerate; do not maintain two sets of expectations. Dedicated malformed-encoding cases and fixed digest vectors remain separate. + +Cases cover values, defaults, errors, reasons, variants, emission gates, duplicate evaluations, and numeric boundaries. +Obfuscation cases cover fixed digests, salts, Unicode, whitespace, malformed metadata, and plaintext event identity. +The suite uses the feature integration harness. Each valid fixture also runs after cache restoration without networking. It checks public getters, response rejection, and real exposure and evaluation records. + +Run from the repository root: + +```sh +python3 ci/sync-precomputed-fixtures.py --verify +./gradlew :reliability:single-fit:flags:testDebugUnitTest --tests '*CanonicalPrecomputedTest*' +``` + +Refresh the snapshot from a reviewed upstream commit: + +```sh +python3 ci/sync-precomputed-fixtures.py --ref +python3 ci/sync-precomputed-fixtures.py --check +``` + +The importer copies committed bytes, regenerates the index, and removes deleted cases. +It preserves Unity asset GUIDs where applicable. Do not edit imported JSON or its checksums. +Use `--source --ref ` to test an upstream fixture branch before publication. +`--verify` checks local integrity without networking. `--check` also compares the recorded upstream commit. + +The weekly `Precomputed flag fixtures` workflow opens a draft update PR when fixture content changes. +It uses a short-lived token restricted to the default branch and this workflow. +It waits when an update PR is already open. Normal SDK CI must pass before merge. +Merge the upstream fixture contract before merging its consumer PRs. +The update workflow never merges or publishes an SDK release. diff --git a/features/dd-sdk-android-flags/api/apiSurface b/features/dd-sdk-android-flags/api/apiSurface index 46cc8e7990..9c1443a151 100644 --- a/features/dd-sdk-android-flags/api/apiSurface +++ b/features/dd-sdk-android-flags/api/apiSurface @@ -38,6 +38,7 @@ data class com.datadog.android.flags.FlagsConfiguration fun evaluationFlushInterval(Long): Builder fun useCustomFlagEndpoint(String): Builder fun initializationTimeout(Long): Builder + fun flagSelection(List?, List?): Builder fun rumIntegrationEnabled(Boolean): Builder fun gracefulModeEnabled(Boolean): Builder fun build(): FlagsConfiguration diff --git a/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api b/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api index e8ed206824..74d89c76be 100644 --- a/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api +++ b/features/dd-sdk-android-flags/api/dd-sdk-android-flags.api @@ -76,6 +76,7 @@ public final class com/datadog/android/flags/FlagsConfiguration$Builder { public fun ()V public final fun build ()Lcom/datadog/android/flags/FlagsConfiguration; public final fun evaluationFlushInterval (J)Lcom/datadog/android/flags/FlagsConfiguration$Builder; + public final fun flagSelection (Ljava/util/List;Ljava/util/List;)Lcom/datadog/android/flags/FlagsConfiguration$Builder; public final fun gracefulModeEnabled (Z)Lcom/datadog/android/flags/FlagsConfiguration$Builder; public final fun initializationTimeout (J)Lcom/datadog/android/flags/FlagsConfiguration$Builder; public final fun rumIntegrationEnabled (Z)Lcom/datadog/android/flags/FlagsConfiguration$Builder; diff --git a/features/dd-sdk-android-flags/src/main/json/flags/flags-state.json b/features/dd-sdk-android-flags/src/main/json/flags/flags-state.json index 66fbd93752..fe90cc630d 100644 --- a/features/dd-sdk-android-flags/src/main/json/flags/flags-state.json +++ b/features/dd-sdk-android-flags/src/main/json/flags/flags-state.json @@ -22,6 +22,14 @@ "salt": { "type": "string" } }, "required": ["scheme", "salt"] + }, + "flagSelection": { + "type": "object", + "properties": { + "tags": { "type": "array", "items": { "type": "string" } }, + "keyPatterns": { "type": "array", "items": { "type": "string" } } + }, + "required": ["tags", "keyPatterns"] } }, "required": ["evaluationContext", "lastUpdateTimestamp"] diff --git a/features/dd-sdk-android-flags/src/main/json/flags/precomputed-assignments-request.json b/features/dd-sdk-android-flags/src/main/json/flags/precomputed-assignments-request.json index 56bb5c2a29..069cb4d38a 100644 --- a/features/dd-sdk-android-flags/src/main/json/flags/precomputed-assignments-request.json +++ b/features/dd-sdk-android-flags/src/main/json/flags/precomputed-assignments-request.json @@ -68,6 +68,28 @@ "targeting_key", "targeting_attributes" ] + }, + "flag_selection": { + "description": "Canonical flag selection. Sent only when the application configures a valid selection.", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "key_patterns": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tags", + "key_patterns" + ] } }, "required": [ diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt index 3dc14db92f..33c3ddf776 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt @@ -479,6 +479,7 @@ interface FlagsClient { featureSdkCore = featureSdkCore, dataStore = datastore, instanceName = name, + flagSelection = flagsFeature.flagSelection, experimentationOptOutMode = experimentationOptOutMode, deliverFirstFlags = { delivery -> val worker = featureSdkCore.createSingleThreadExecutorService(FLAGS_FIRST_FLAGS_EXECUTOR_NAME) @@ -501,6 +502,7 @@ interface FlagsClient { PrecomputedAssignmentsRequestFactory( featureSdkCore.internalLogger, configuration.customFlagEndpoint, + flagSelection = flagsFeature.flagSelection, experimentationOptOutMode = experimentationOptOutMode ) } else { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt index 5a51768925..eb5e919626 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsConfiguration.kt @@ -6,6 +6,9 @@ package com.datadog.android.flags +import com.datadog.android.flags.internal.selection.FlagSelection +import com.datadog.android.flags.model.ErrorCode + private const val DEFAULT_INITIALIZATION_TIMEOUT_MS = 5_000L /** @@ -21,10 +24,11 @@ data class FlagsConfiguration internal constructor( internal val evaluationFlushIntervalMs: Long, internal val rumIntegrationEnabled: Boolean, internal val gracefulModeEnabled: Boolean, - internal val initializationTimeoutMs: Long? + internal val initializationTimeoutMs: Long?, + internal val flagSelection: FlagSelection? ) { /** - * Copies this configuration and preserves the initialization timeout. + * Copies this configuration and preserves the initialization timeout and the flag selection. * * This overload keeps the public JVM signature that existed before the initialization timeout was added. */ @@ -46,7 +50,8 @@ data class FlagsConfiguration internal constructor( evaluationFlushIntervalMs = evaluationFlushIntervalMs, rumIntegrationEnabled = rumIntegrationEnabled, gracefulModeEnabled = gracefulModeEnabled, - initializationTimeoutMs = initializationTimeoutMs + initializationTimeoutMs = initializationTimeoutMs, + flagSelection = flagSelection ) /** @@ -62,6 +67,7 @@ data class FlagsConfiguration internal constructor( private var rumIntegrationEnabled: Boolean = true private var gracefulModeEnabled: Boolean = true private var initializationTimeoutMs: Long? = DEFAULT_INITIALIZATION_TIMEOUT_MS + private var flagSelection: FlagSelection? = null /** * Sets whether exposures should be logged to the dedicated exposures intake endpoint. @@ -172,6 +178,36 @@ data class FlagsConfiguration internal constructor( initializationTimeoutMs = timeoutMs.takeIf { it > 0 } } + /** + * Loads only the flags that match the given tags or key patterns. + * + * Tags match exactly and are case-sensitive; a flag matches if it has any of the tags. Key patterns are + * whole-key globs where only `*` is special. Leading and trailing Unicode whitespace is trimmed from every + * value, empty values are dropped, and duplicates are removed. A null list is treated as absent. + * + * Each list accepts at most 31 values after duplicates are removed, and each tag and key pattern is at most + * 200 UTF-8 bytes after trimming. A selection over a limit, or one that contains a null value or an unpaired + * UTF-16 surrogate, is not sent, and the full flag configuration is loaded. The server may also ignore a very + * large selection (`too_many_values`), in which case the full flag configuration is loaded. Servers that do + * not support flag selection, or have it disabled, return the full flag configuration without a warning. + * A server reason that the SDK does not know is reported as `unknown`. With [useCustomFlagEndpoint], the proxy + * must forward the `DD-Flag-Selection-Ignored` response header for an ignored selection to be reported. + * + * When the server applies the selection, flags outside it are not loaded, so evaluating them returns the + * default value with [ErrorCode.FLAG_NOT_FOUND]. + * + * The selection is fixed when the feature is enabled and applies to every [FlagsClient]. Flags cached under a + * different selection are not loaded at startup; until the next successful fetch, a client starts without + * cached flags. Calling this method again replaces the previous selection. + * + * @param tags The flag tags to load, or null for none. + * @param keyPatterns The flag key patterns to load, or null for none. + * @return this [Builder] instance for method chaining. + */ + fun flagSelection(tags: List?, keyPatterns: List?): Builder = apply { + flagSelection = FlagSelection(tags?.toList(), keyPatterns?.toList()) + } + /** * Sets whether RUM evaluation logging is enabled. * This adds the result of evaluating a feature flag to the view. @@ -217,7 +253,8 @@ data class FlagsConfiguration internal constructor( evaluationFlushIntervalMs = evaluationFlushIntervalMs, rumIntegrationEnabled = rumIntegrationEnabled, gracefulModeEnabled = gracefulModeEnabled, - initializationTimeoutMs = initializationTimeoutMs + initializationTimeoutMs = initializationTimeoutMs, + flagSelection = flagSelection ) internal companion object { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt index 9eefa4b90d..fdb0cc3181 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagValueConverter.kt @@ -32,13 +32,10 @@ internal object FlagValueConverter { private val INT_MIN = BigDecimal(Int.MIN_VALUE) private val INT_MAX = BigDecimal(Int.MAX_VALUE) - // The longest decimal value that Int reads, and Double reads at or above 2^53, parse with BigDecimal. + // The longest decimal value that numeric reads parse with BigDecimal. // This bounds the BigDecimal work on each read. private const val MAX_DECIMAL_LENGTH = 64 - // 2^53. Below this magnitude, Double holds every whole value exactly. - private const val EXACT_DOUBLE_LIMIT = 9_007_199_254_740_992.0 - /** * Converts a flag's string value to the expected type. * @@ -49,7 +46,7 @@ internal object FlagValueConverter { * @return Result containing the converted value on success, * or a Failure with TypeMismatchException if types are incompatible * or a numeric value can't be read as Int without loss, - * or a whole value matches neither the Double nor its printed form, + * or a Double read overflows, underflows, or loses whole-number precision, * or a Failure with the parse exception if conversion failed */ @Suppress("UNCHECKED_CAST") @@ -155,22 +152,17 @@ internal object FlagValueConverter { } /** - * A whole value reads as Double only when the Double equals it, or when the Double prints as the same value, - * such as `1e300`. So `9007199254740993` is a type mismatch. A fractional value reads as the nearest Double, - * such as `1.2`. A value that can't be parsed returns null, which [convert] reports as a parse error. So does - * a value whose parsed Double is 2^53 or more in magnitude, when the value is longer than [MAX_DECIMAL_LENGTH] - * characters or [BigDecimal] can't parse it. + * Fractional values round to the nearest finite Double; original whole values must remain exact. + * Overflow and nonzero underflow are type mismatches. Decimal parsing is bounded on each read. */ private fun parseLosslessDouble(variationValue: String, variationType: String): Double? = - variationValue.toDoubleOrNull()?.let { double -> - if (double > -EXACT_DOUBLE_LIMIT && double < EXACT_DOUBLE_LIMIT) { - double - } else { - variationValue.takeIf { it.length <= MAX_DECIMAL_LENGTH } - ?.toBigDecimalOrNull() - ?.let { requireLosslessDouble(it, double, variationValue, variationType) } + variationValue.takeIf { it.length <= MAX_DECIMAL_LENGTH } + ?.toBigDecimalOrNull() + ?.let { decimal -> + variationValue.toDoubleOrNull()?.let { double -> + requireLosslessDouble(decimal, double, variationValue, variationType) + } } - } private fun requireLosslessDouble( decimal: BigDecimal, @@ -178,20 +170,23 @@ internal object FlagValueConverter { variationValue: String, variationType: String ): Double { - if (!isFractional(decimal) && !isExactDouble(decimal, double)) { + val underflows = double == 0.0 && decimal.signum() != 0 + val losesIntegerPrecision = !isFractional(decimal) && !isExactDouble(decimal, double) + if (!double.isFinite() || underflows || losesIntegerPrecision) { @Suppress("ThrowingInternalException") // Caught by runCatching in convert() throw TypeMismatchException( - "Flag has type '$variationType' with whole value '$variationValue' that Double can't hold exactly " + + "Flag has type '$variationType' with value '$variationValue' that Double can't represent " + "but Double was requested" ) } return double } - @Suppress("UnsafeThirdPartyFunctionCall") // Safe: BigDecimal(Double) and valueOf() only run for a finite Double - private fun isExactDouble(decimal: BigDecimal, double: Double): Boolean = - double != Double.POSITIVE_INFINITY && double != Double.NEGATIVE_INFINITY && - (decimal.compareTo(BigDecimal(double)) == 0 || decimal.compareTo(BigDecimal.valueOf(double)) == 0) + private fun isExactDouble(decimal: BigDecimal, double: Double): Boolean = try { + decimal.compareTo(BigDecimal(double)) == 0 + } catch (_: NumberFormatException) { + false + } /** * A value with a scale of 0 or less is whole. Check the scale first: it also prevents diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt index 436004e057..e129f9bd9c 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsFeature.kt @@ -21,6 +21,10 @@ import com.datadog.android.flags.FlagsConfiguration import com.datadog.android.flags.internal.evaluation.InitializationTimeoutScheduler import com.datadog.android.flags.internal.net.ExposuresRequestFactory import com.datadog.android.flags.internal.net.PrecomputedAssignmentsRequestFactory +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection +import com.datadog.android.flags.internal.selection.FlagSelectionCanonicalizer +import com.datadog.android.flags.internal.selection.FlagSelectionResult +import com.datadog.android.flags.internal.selection.logDroppedFlagSelection import com.datadog.android.flags.internal.storage.ExposureEventRecordWriter import com.datadog.android.flags.internal.storage.NoOpRecordWriter import com.datadog.android.flags.internal.storage.RecordWriter @@ -76,10 +80,16 @@ internal class FlagsFeature( // region Domain Objects + private val flagSelectionResult = FlagSelectionCanonicalizer.canonicalize(flagsConfiguration.flagSelection) + + internal val flagSelection: CanonicalFlagSelection? = + (flagSelectionResult as? FlagSelectionResult.Selected)?.selection + internal val precomputedRequestFactory = PrecomputedAssignmentsRequestFactory( internalLogger = sdkCore.internalLogger, - customFlagEndpoint = flagsConfiguration.customFlagEndpoint + customFlagEndpoint = flagsConfiguration.customFlagEndpoint, + flagSelection = flagSelection ) internal val initializationTimeoutScheduler = InitializationTimeoutScheduler { timeoutMs, action -> @@ -123,6 +133,9 @@ internal class FlagsFeature( } isDebugBuild = (appContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 isInitialized = true + (flagSelectionResult as? FlagSelectionResult.Dropped)?.let { + sdkCore.internalLogger.logDroppedFlagSelection(it.error) + } dataWriter = createDataWriter() processor = ExposureEventsProcessor( writer = dataWriter, diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt index 6b2a0db94c..3b9f3d22b9 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManager.kt @@ -26,6 +26,7 @@ import com.datadog.android.flags.internal.net.NetworkRequestFailedException import com.datadog.android.flags.internal.net.PrecomputedAssignmentsReader import com.datadog.android.flags.internal.repository.FlagsRepository import com.datadog.android.flags.internal.repository.net.PrecomputeMapper +import com.datadog.android.flags.internal.selection.logIgnoredFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.FlagsClientState import java.util.concurrent.Executor @@ -95,6 +96,7 @@ private class InitializationCompletion(private var callback: EvaluationContextCa * @param experimentationOptOut validates contexts and prevents superseded installations * @param callbackExecutor delivers customer callbacks outside state delivery and network operations */ +@Suppress("TooManyFunctions") // Each step of the fetch, install and completion lifecycle is a separate function. internal class EvaluationsManager( private val sdkCore: FeatureSdkCore, private val executorService: ExecutorService, @@ -200,15 +202,21 @@ internal class EvaluationsManager( { "Processing evaluation context: ${context.targetingKey}" } ) matchingCachedAssignments.set( - flagsRepository.hasFlags() && flagsRepository.getEvaluationContext() == context + flagsRepository.hasConfiguration() && flagsRepository.getEvaluationContext() == context ) val response = datadogContext?.let { assignmentsReader.readPrecomputedFlags(context, it, diagnostics) } val assignments = response?.let { rawResponse -> - precomputeMapper.map(rawResponse, request.takeIf { it.optedOut }) + precomputeMapper.map(rawResponse.body, request.takeIf { it.optedOut }) } ?.takeIf { it.obfuscation == null || supportsObfuscation } if (assignments != null) { - installFlags(request, assignments, initializationCompletion, callback) + installFlags( + request, + assignments, + response.flagSelectionIgnoredReason, + initializationCompletion, + callback + ) } else { handleRequestFailure( request, @@ -266,39 +274,55 @@ internal class EvaluationsManager( private fun installFlags( request: ExperimentationOptOut.Request, assignments: PrecomputedAssignments, + flagSelectionIgnoredReason: String?, initializationCompletion: InitializationCompletion?, callback: EvaluationContextCallback? ) { val context = request.context - synchronized(initializationTerminalLock) { - val installed = experimentationOptOut.ifCurrent(request) { - val installation = flagsRepository.installFlagsAndContext( - context, - assignments.flags, - assignments.obfuscation - ) - val result = takeCompletion(initializationCompletion, callback) - currentState = FlagsClientState.Ready - effects.enqueue { - installation.complete { - effects.notifySafely { flagStateManager.updateState(FlagsClientState.Ready) } - complete(result) - } - } + val installed = synchronized(initializationTerminalLock) { + val isCurrent = experimentationOptOut.ifCurrent(request) { + installCurrentFlags(context, assignments, initializationCompletion, callback) } - if (!installed) { + if (!isCurrent) { enqueueCompletion( takeCompletion(initializationCompletion, callback), FlagsContextSupersededException(SUPERSEDED_MESSAGE) ) } + isCurrent } effects.drain() - internalLogger.log( - InternalLogger.Level.DEBUG, - InternalLogger.Target.MAINTAINER, - { "Successfully processed context ${context.targetingKey} with ${assignments.flags.size} flags" } + // A superseded response is not installed, so its ignored reason would describe flags that never loaded. + if (installed) { + flagSelectionIgnoredReason?.let { internalLogger.logIgnoredFlagSelection(it) } + internalLogger.log( + InternalLogger.Level.DEBUG, + InternalLogger.Target.MAINTAINER, + { "Successfully processed context ${context.targetingKey} with ${assignments.flags.size} flags" } + ) + } + } + + // Callers hold initializationTerminalLock; the Ready notification and callback run later from effects.drain(). + private fun installCurrentFlags( + context: EvaluationContext, + assignments: PrecomputedAssignments, + initializationCompletion: InitializationCompletion?, + callback: EvaluationContextCallback? + ) { + val installation = flagsRepository.installFlagsAndContext( + context, + assignments.flags, + assignments.obfuscation ) + val result = takeCompletion(initializationCompletion, callback) + currentState = FlagsClientState.Ready + effects.enqueue { + installation.complete { + effects.notifySafely { flagStateManager.updateState(FlagsClientState.Ready) } + complete(result) + } + } } private fun startInitializationTimeout( @@ -327,7 +351,7 @@ internal class EvaluationsManager( if (currentState != FlagsClientState.Ready && currentState != FlagsClientState.Stale) { val hasMatchingCachedAssignments = (target === request && matchingCachedAssignments.get()) || - flagsRepository.hasLoadedFlagsForContext(target.context) + flagsRepository.hasConfigurationForContext(target.context) val timeoutState = if (hasMatchingCachedAssignments) { FlagsClientState.Stale } else { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt index b2de7aa2f0..48952ff9ad 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/FlagsStateEntry.kt @@ -6,11 +6,13 @@ package com.datadog.android.flags.internal.model +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext internal data class FlagsStateEntry( val evaluationContext: EvaluationContext, val flags: Map, val lastUpdateTimestamp: Long, - val obfuscation: FlagKeyObfuscation? = null + val obfuscation: FlagKeyObfuscation? = null, + val flagSelection: CanonicalFlagSelection? = null ) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt index fb32870b8b..578222adc1 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloader.kt @@ -10,6 +10,7 @@ import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.api.context.DatadogContext import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Diagnostics +import com.datadog.android.flags.internal.selection.FlagSelectionIgnoredReason import com.datadog.android.flags.model.EvaluationContext import okhttp3.Call import okhttp3.Request @@ -33,13 +34,13 @@ internal class PrecomputedAssignmentsDownloader( context: EvaluationContext, datadogContext: DatadogContext, diagnostics: Diagnostics - ): String? { + ): PrecomputedAssignmentsResponse? { val request = requestFactory.create(context, datadogContext, diagnostics) ?: return null return executeDownloadRequest(request) } @Suppress("TooGenericExceptionCaught") - private fun executeDownloadRequest(request: Request): String? = try { + private fun executeDownloadRequest(request: Request): PrecomputedAssignmentsResponse? = try { val response = callFactory.newCall(request).execute() handleResponse(response) } catch (e: Throwable) { @@ -52,9 +53,20 @@ internal class PrecomputedAssignmentsDownloader( null } - private fun handleResponse(response: Response): String? = if (response.isSuccessful) { + private fun handleResponse(response: Response): PrecomputedAssignmentsResponse? = if (response.isSuccessful) { @Suppress("UnsafeThirdPartyFunctionCall") // Safe: wrapped in outer try-catch - response.body?.use { it.string() } + response.body?.use { it.string() }?.let { body -> + PrecomputedAssignmentsResponse( + body = body, + flagSelectionIgnoredReason = if (requestFactory.sendsFlagSelection) { + response.header(HEADER_FLAG_SELECTION_IGNORED) + ?.takeIf(String::isNotEmpty) + ?.let(FlagSelectionIgnoredReason::sanitize) + } else { + null + } + ) + } } else { internalLogger.log( InternalLogger.Level.ERROR, @@ -74,4 +86,8 @@ internal class PrecomputedAssignmentsDownloader( null } + + private companion object { + const val HEADER_FLAG_SELECTION_IGNORED = "DD-Flag-Selection-Ignored" + } } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt index 3edc60b34b..0f5c7c7615 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsReader.kt @@ -19,5 +19,13 @@ internal interface PrecomputedAssignmentsReader { context: EvaluationContext, datadogContext: DatadogContext, diagnostics: Diagnostics - ): String? + ): PrecomputedAssignmentsResponse? } + +/** + * [flagSelectionIgnoredReason] is the sanitized `DD-Flag-Selection-Ignored` reason, or null when the header is absent. + */ +internal data class PrecomputedAssignmentsResponse( + val body: String, + val flagSelectionIgnoredReason: String? +) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt index 25ea88368d..d7665d0976 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactory.kt @@ -15,6 +15,7 @@ import com.datadog.android.flags.internal.evaluation.wireValue import com.datadog.android.flags.internal.getFlagsEndpoint import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.internal.rum.rumContext import okhttp3.Headers @@ -28,9 +29,12 @@ import okhttp3.RequestBody.Companion.toRequestBody internal class PrecomputedAssignmentsRequestFactory( private val internalLogger: InternalLogger, private val customFlagEndpoint: String?, + private val flagSelection: CanonicalFlagSelection?, private val experimentationOptOutMode: ExperimentationOptOutMode = ExperimentationOptOutMode.DISABLED ) { + val sendsFlagSelection: Boolean = flagSelection != null + /** * Creates an OkHttp Request for fetching precomputed flag assignments. * @@ -134,7 +138,10 @@ internal class PrecomputedAssignmentsRequestFactory( targetingAttributes = PrecomputedAssignmentsRequest.TargetingAttributes( additionalProperties = targetingAttributes ) - ) + ), + flagSelection = flagSelection?.let { + PrecomputedAssignmentsRequest.FlagSelection(tags = it.tags, keyPatterns = it.keyPatterns) + } ) ) ) diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt index e836d5cae4..766b6913a3 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsPersistenceManager.kt @@ -15,11 +15,13 @@ import com.datadog.android.flags.ExperimentationOptOutMode import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext internal class FlagsPersistenceManager( private val dataStore: DataStoreHandler, instanceName: String, + private val flagSelection: CanonicalFlagSelection?, private val internalLogger: InternalLogger, experimentationOptOutMode: ExperimentationOptOutMode = ExperimentationOptOutMode.DISABLED, onStateLoaded: (FlagsStateEntry?) -> Unit @@ -27,13 +29,16 @@ internal class FlagsPersistenceManager( private val serializer = FlagsStateSerializer(internalLogger) private val deserializer = FlagsStateDeserializer(internalLogger) - private val flagsStateKey: String = when (experimentationOptOutMode) { - ExperimentationOptOutMode.DISABLED -> "$FLAGS_STATE_KEY_PREFIX-$instanceName" - ExperimentationOptOutMode.IMMEDIATE -> "flags-immediate-v1-state-$instanceName" - ExperimentationOptOutMode.FUTURE_EXPERIMENTS_ONLY -> "flags-future-v1-state-$instanceName" - } + // SDK versions without flag selection read only the unselected key for their opt-out mode and ignore the + // flagSelection field, so selected state uses its own key and is never read by them as a full configuration. + private val unselectedStateKey = stateKey(instanceName, experimentationOptOutMode, selected = false) + private val selectedStateKey = stateKey(instanceName, experimentationOptOutMode, selected = true) + private val flagsStateKey: String = if (flagSelection == null) unselectedStateKey else selectedStateKey + private val staleStateKey: String = if (flagSelection == null) selectedStateKey else unselectedStateKey init { + // One persisted state per instance: changing to or from a selection discards the other key's state. + dataStore.removeValue(staleStateKey) loadFlagsState(onStateLoaded) } @@ -48,7 +53,8 @@ internal class FlagsPersistenceManager( evaluationContext = context, flags = flags, lastUpdateTimestamp = currentTimestamp, - obfuscation = obfuscation + obfuscation = obfuscation, + flagSelection = flagSelection ) dataStore.setValue( @@ -82,6 +88,14 @@ internal class FlagsPersistenceManager( } companion object { - private const val FLAGS_STATE_KEY_PREFIX = "flags-state" + internal fun stateKey(instanceName: String, mode: ExperimentationOptOutMode, selected: Boolean): String { + val selection = if (selected) "selection-" else "" + val modePart = when (mode) { + ExperimentationOptOutMode.DISABLED -> "" + ExperimentationOptOutMode.IMMEDIATE -> "immediate-v1-" + ExperimentationOptOutMode.FUTURE_EXPERIMENTS_ONLY -> "future-v1-" + } + return "flags-$selection${modePart}state-$instanceName" + } } } diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt index 1cfcc63235..3f74fd5d7a 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializer.kt @@ -10,6 +10,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.internal.serialization.FlagsJson import com.datadog.android.flags.model.EvaluationContext import com.google.gson.JsonElement @@ -48,7 +49,8 @@ internal class FlagsStateDeserializer(private val internalLogger: InternalLogger ), flags = flags, lastUpdateTimestamp = state.lastUpdateTimestamp, - obfuscation = obfuscation + obfuscation = obfuscation, + flagSelection = state.flagSelection?.let { CanonicalFlagSelection(it.tags, it.keyPatterns) } ) } } catch (e: JsonParseException) { diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt index c7aaa7c918..e700c9a03e 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializer.kt @@ -33,7 +33,8 @@ internal class FlagsStateSerializer(private val internalLogger: InternalLogger) encodedFlags = flags.takeIf { model.obfuscation != null }, lastUpdateTimestamp = model.lastUpdateTimestamp, obfuscated = true.takeIf { model.obfuscation != null }, - obfuscation = model.obfuscation?.let { FlagsState.Obfuscation(FlagKeyObfuscation.SCHEME, it.salt) } + obfuscation = model.obfuscation?.let { FlagsState.Obfuscation(FlagKeyObfuscation.SCHEME, it.salt) }, + flagSelection = model.flagSelection?.let { FlagsState.FlagSelection(it.tags, it.keyPatterns) } ).toJson().toString() } catch (e: JsonParseException) { internalLogger.log( diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt index 0e2a6be159..d930ea514c 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepository.kt @@ -15,6 +15,7 @@ import com.datadog.android.flags.ExperimentationOptOutMode import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.persistence.FlagsPersistenceManager +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.ResolutionReason import java.util.concurrent.CountDownLatch @@ -25,6 +26,7 @@ internal class DefaultFlagsRepository( private val featureSdkCore: FeatureSdkCore, instanceName: String, dataStore: DataStoreHandler, + flagSelection: CanonicalFlagSelection?, private val internalLogger: InternalLogger = featureSdkCore.internalLogger, private val persistenceLoadTimeoutMs: Long = PERSISTENCE_LOAD_TIMEOUT_MS, private val deliverFirstFlags: (Runnable) -> Unit = Runnable::run, @@ -52,9 +54,12 @@ internal class DefaultFlagsRepository( private val persistenceManager = FlagsPersistenceManager( dataStore = dataStore, instanceName = instanceName, + flagSelection = flagSelection, internalLogger = internalLogger, experimentationOptOutMode = experimentationOptOutMode - ) { persistedState -> + ) { loadedState -> + // State fetched with a different selection holds a different set of flags. + val persistedState = loadedState?.takeIf { it.flagSelection == flagSelection } val cachedState = try { persistedState?.let { FlagsState( @@ -181,15 +186,13 @@ internal class DefaultFlagsRepository( return readableState?.context } - override fun hasFlags(): Boolean { + override fun hasConfiguration(): Boolean { waitForPersistenceLoad() - return readableState?.flags?.isNotEmpty() ?: false + return readableState != null } - override fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean { - val state = readableState - return state?.context == context && state.flags.isNotEmpty() - } + override fun hasConfigurationForContext(context: EvaluationContext): Boolean = + readableState?.context == context override fun getPrecomputedFlagWithContext(key: String): Pair? { val state = readableState ?: return null diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt index 2810f66728..0eb1a7c6cc 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/FlagsRepository.kt @@ -39,8 +39,12 @@ internal interface FlagsRepository { // Nonblocking snapshot. Missing flags retain their context from the same atomic state read. fun getPrecomputedFlagWithContext(key: String): Pair? val firstFlags: FirstFlagsLatch - fun hasFlags(): Boolean - fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean + + /** True once a configuration is installed, including a valid configuration with zero flags. */ + fun hasConfiguration(): Boolean + + /** True when the installed configuration is for [context], including a valid configuration with zero flags. */ + fun hasConfigurationForContext(context: EvaluationContext): Boolean @Deprecated("Use getAssignmentsSnapshot instead") fun getFlagsSnapshot(): Map diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt index 4818398e1a..9c8ad09a0a 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/NoOpFlagsRepository.kt @@ -38,9 +38,9 @@ internal class NoOpFlagsRepository : FlagsRepository { override val firstFlags = FirstFlagsLatch() - override fun hasFlags(): Boolean = false + override fun hasConfiguration(): Boolean = false - override fun hasLoadedFlagsForContext(context: EvaluationContext): Boolean = false + override fun hasConfigurationForContext(context: EvaluationContext): Boolean = false override fun getAssignmentsSnapshot(): FlagsAssignmentsSnapshot? = null diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt index d7e7a34b92..64676de672 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapper.kt @@ -14,6 +14,7 @@ import com.datadog.android.flags.internal.model.AssignmentsResponse import com.datadog.android.flags.internal.model.PrecomputedAssignments import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.serialization.FlagsJson +import com.datadog.android.flags.internal.serialization.UnsupportedVariationTypeException import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParseException @@ -41,7 +42,14 @@ internal class PrecomputeMapper( val flags = attributes.flags.additionalProperties if (obfuscation != null) FlagsJson.validateKeys(flags.keys.iterator()) PrecomputedAssignments( - flags.mapValues { FlagsJson.readAssignment(FlagsJson.objectValue(it.value)) }, + flags.mapNotNull { (key, value) -> + try { + key to FlagsJson.readAssignment(FlagsJson.objectValue(value)) + } catch (_: UnsupportedVariationTypeException) { + warnUnsupportedType() + null + } + }.toMap(), obfuscation ) } else { @@ -80,6 +88,15 @@ internal class PrecomputeMapper( null } + private fun warnUnsupportedType() { + internalLogger.log( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.USER, + messageBuilder = { "Ignored a flag with an unsupported variationType in the precomputed response." }, + onlyOnce = true + ) + } + private fun acknowledgesOptOut(options: JsonObject?, request: ExperimentationOptOut.Request?): Boolean { val mode = options?.get("experimentation_opt_out_mode") val acknowledgedMode = mode?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }?.asString diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt new file mode 100644 index 0000000000..aaa9dddd59 --- /dev/null +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelection.kt @@ -0,0 +1,159 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +/** + * The flag selection as configured by the application, before canonicalization. + * Lists and elements are nullable because Java callers can pass nulls. A null list means the list is absent. + */ +internal data class FlagSelection( + val tags: List?, + val keyPatterns: List? +) + +/** + * A flag selection that was trimmed, deduplicated, sorted in UTF-8 byte order and validated. + */ +internal data class CanonicalFlagSelection( + val tags: List, + val keyPatterns: List +) + +/** + * Reasons a flag selection is dropped. The values are the reason codes that the server and the other Datadog SDKs use. + */ +internal enum class FlagSelectionError(val reason: String, val limit: String) { + MALFORMED_SELECTION( + "malformed_selection", + "tags and key patterns must be non-null strings without unpaired UTF-16 surrogates" + ), + TOO_MANY_VALUES( + "too_many_values", + "each list accepts at most ${FlagSelectionCanonicalizer.MAX_VALUES} values" + ), + INVALID_KEY_PATTERN( + "invalid_key_pattern", + "each key pattern is at most ${FlagSelectionCanonicalizer.MAX_KEY_PATTERN_BYTES} UTF-8 bytes" + ), + INVALID_TAG( + "invalid_tag", + "each tag is at most ${FlagSelectionCanonicalizer.MAX_TAG_BYTES} UTF-8 bytes" + ) +} + +/** + * Reasons the server reports in the `DD-Flag-Selection-Ignored` response header. + * The server validates with the same rules as the SDK, so validation failures reuse the [FlagSelectionError] codes. + * [TAG_LOAD_FAILED] is reported only by the server, so it is not a [FlagSelectionError]. + */ +internal enum class ServerIgnoredReason(val reason: String) { + MALFORMED_SELECTION(FlagSelectionError.MALFORMED_SELECTION.reason), + TOO_MANY_VALUES(FlagSelectionError.TOO_MANY_VALUES.reason), + INVALID_KEY_PATTERN(FlagSelectionError.INVALID_KEY_PATTERN.reason), + INVALID_TAG(FlagSelectionError.INVALID_TAG.reason), + TAG_LOAD_FAILED("tag_load_failed") +} + +/** + * Sanitizes the `DD-Flag-Selection-Ignored` response header, so a raw header value never reaches logs or telemetry. + * A [ServerIgnoredReason] is kept, and any other non-empty value becomes [UNKNOWN]. + */ +internal object FlagSelectionIgnoredReason { + + internal const val UNKNOWN = "unknown" + + // The longest known reason has 19 characters. The limit bounds the work on an oversized header. + private const val MAX_LENGTH = 32 + + fun sanitize(value: String): String { + if (value.length > MAX_LENGTH) return UNKNOWN + return ServerIgnoredReason.entries.firstOrNull { it.reason == value }?.reason ?: UNKNOWN + } +} + +internal sealed interface FlagSelectionResult { + object None : FlagSelectionResult + data class Selected(val selection: CanonicalFlagSelection) : FlagSelectionResult + data class Dropped(val error: FlagSelectionError) : FlagSelectionResult +} + +/** + * Canonicalizes and validates a flag selection with the same rules as the server. + */ +internal object FlagSelectionCanonicalizer { + + internal const val MAX_VALUES = 31 + internal const val MAX_TAG_BYTES = 200 + internal const val MAX_KEY_PATTERN_BYTES = 200 + + // The Unicode White_Space property, which the server trims. + // Kotlin's trim() differs: it also trims U+001C..U+001F and does not trim U+0085. + private const val WHITE_SPACE = "\u0009\u000A\u000B\u000C\u000D\u0020\u0085\u00A0\u1680" + + "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A" + + "\u2028\u2029\u202F\u205F\u3000" + + @Suppress("ReturnCount") + fun canonicalize(selection: FlagSelection?): FlagSelectionResult { + if (selection == null) return FlagSelectionResult.None + val rawTags = selection.tags.orEmpty().wellFormedOrNull() + val rawKeyPatterns = selection.keyPatterns.orEmpty().wellFormedOrNull() + if (rawTags == null || rawKeyPatterns == null) { + return FlagSelectionResult.Dropped(FlagSelectionError.MALFORMED_SELECTION) + } + + val tags = canonicalizeList(rawTags) + val keyPatterns = canonicalizeList(rawKeyPatterns) + + val error = when { + tags.size > MAX_VALUES || keyPatterns.size > MAX_VALUES -> FlagSelectionError.TOO_MANY_VALUES + keyPatterns.any { it.utf8Size() > MAX_KEY_PATTERN_BYTES } -> FlagSelectionError.INVALID_KEY_PATTERN + tags.any { it.utf8Size() > MAX_TAG_BYTES } -> FlagSelectionError.INVALID_TAG + else -> null + } + return when { + error != null -> FlagSelectionResult.Dropped(error) + tags.isEmpty() && keyPatterns.isEmpty() -> FlagSelectionResult.None + else -> FlagSelectionResult.Selected(CanonicalFlagSelection(tags, keyPatterns)) + } + } + + private fun canonicalizeList(values: List): List { + val distinct = values + .asSequence() + .map { value -> value.trim { it in WHITE_SPACE } } + .filter { it.isNotEmpty() } + .toList() + .distinct() + // An over-limit list is dropped as too_many_values, so its order does not matter. + if (distinct.size > MAX_VALUES) return distinct + return distinct + .map { it to it.utf8SortKey() } + .sortedBy { it.second } + .map { it.first } + } + + // Latin-1 maps each byte to one char, so comparing these strings compares unsigned UTF-8 bytes. + private fun String.utf8SortKey(): String = String(toByteArray(Charsets.UTF_8), Charsets.ISO_8859_1) + + private fun String.utf8Size(): Int = toByteArray(Charsets.UTF_8).size + + // A Java caller can pass a raw list, so check each element's type instead of trusting the generic type. + private fun List<*>.wellFormedOrNull(): List? = + if (all { it is String && !it.hasUnpairedSurrogate() }) filterIsInstance() else null + + private fun String.hasUnpairedSurrogate(): Boolean { + var index = 0 + var unpaired = false + while (index < length && !unpaired) { + val char = this[index] + val isPair = char.isHighSurrogate() && index + 1 < length && this[index + 1].isLowSurrogate() + unpaired = !isPair && (char.isHighSurrogate() || char.isLowSurrogate()) + index += if (isPair) 2 else 1 + } + return unpaired + } +} diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt new file mode 100644 index 0000000000..a8b9b4efb2 --- /dev/null +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionLogging.kt @@ -0,0 +1,48 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +import com.datadog.android.api.InternalLogger + +// Telemetry carries only the reason, never selection values, and is sent once per process. +private const val TELEMETRY_REASON_KEY = "reason" + +private const val FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE = "Flag selection dropped" +private const val FLAG_SELECTION_IGNORED_TELEMETRY_MESSAGE = "Flag selection ignored" + +internal fun InternalLogger.logDroppedFlagSelection(error: FlagSelectionError) { + log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { + "flagSelection was not sent (${error.reason}: ${error.limit}). " + + "Loading the full flag configuration." + } + ) + log( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + { FLAG_SELECTION_DROPPED_TELEMETRY_MESSAGE }, + additionalProperties = mapOf(TELEMETRY_REASON_KEY to error.reason), + onlyOnce = true + ) +} + +internal fun InternalLogger.logIgnoredFlagSelection(reason: String) { + log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "flagSelection was ignored by the server ($reason). Loaded the full flag configuration." } + ) + log( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + { FLAG_SELECTION_IGNORED_TELEMETRY_MESSAGE }, + additionalProperties = mapOf(TELEMETRY_REASON_KEY to reason), + onlyOnce = true + ) +} diff --git a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt index 1d5653f7db..e401c667e6 100644 --- a/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt +++ b/features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/serialization/FlagsJson.kt @@ -10,14 +10,18 @@ import com.datadog.android.flags.internal.model.Assignment import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.FlagsState import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.model.VariationType import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParseException import org.json.JSONObject +internal class UnsupportedVariationTypeException : JsonParseException("Unsupported assignment variation type") + /** Generated model adapters, legacy JSON conversions, and encoding validation. */ internal object FlagsJson { @Throws(JsonParseException::class) + @Suppress("ThrowsCount") // Validate required fields and isolate unsupported variation types. fun readAssignment(json: JsonObject): PrecomputedFlag { // Preserve JSONObject's string conversion, including JSON variants and explicit null. STRING_FIELDS.forEach { field -> @@ -34,8 +38,14 @@ internal object FlagsJson { val serialId = json.get("serialId") if (serialId?.isJsonPrimitive != true || !serialId.asJsonPrimitive.isNumber) json.remove("serialId") + if (!json.has("extraLogging")) json.add("extraLogging", JsonObject()) val assignment = Assignment.fromJsonObject(json) + val unsupportedType = VariationType.entries.none { it.value == assignment.variationType } + if (unsupportedType) { + throw UnsupportedVariationTypeException() + } + @Suppress("UnsafeThirdPartyFunctionCall") // Generated JSON is a valid object. val extraLogging = JSONObject(assignment.extraLogging.toJson().toString()) return PrecomputedFlag( diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt index fd2cfffb69..67219e211b 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsClientTest.kt @@ -11,14 +11,23 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.Feature.Companion.FLAGS_FEATURE_NAME import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.storage.datastore.DataStoreHandler +import com.datadog.android.api.storage.datastore.DataStoreReadCallback +import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.flags.internal.DatadogFlagsClient import com.datadog.android.flags.internal.FlagsFeature import com.datadog.android.flags.internal.NoOpFlagsClient +import com.datadog.android.flags.internal.model.FlagsStateEntry +import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Call import org.assertj.core.api.Assertions.assertThat +import org.json.JSONObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -28,12 +37,15 @@ import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.concurrent.ExecutorService @ExtendWith(MockitoExtension::class, ForgeExtension::class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -120,6 +132,67 @@ internal class FlagsClientTest { flagsFeature?.clearClients() } + @Test + fun `M key persisted flags by the configured selection W createInternal() { flag selection }`( + @StringForgery fakeFlagKey: String, + @StringForgery fakeClientName: String + ) { + // Given + val mockCore = mock() + val mockDataStore = mock() + val mockFeatureScope = mock() + whenever(mockCore.internalLogger).thenReturn(mockInternalLogger) + whenever(mockCore.timeProvider).thenReturn(mock()) + whenever(mockCore.createSingleThreadExecutorService(any())).thenReturn(mock()) + whenever(mockCore.createOkHttpCallFactory(any())).thenReturn(mock()) + whenever(mockFeatureScope.dataStore).thenReturn(mockDataStore) + whenever(mockCore.getFeature(FLAGS_FEATURE_NAME)).thenReturn(mockFeatureScope) + lateinit var readCallback: DataStoreReadCallback + doAnswer { + readCallback = it.getArgument(2) + null + }.whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) + val configuration = FlagsConfiguration.Builder() + .trackEvaluations(false) + .flagSelection(tags = listOf("mobile"), keyPatterns = null) + .build() + val client = FlagsClient.createInternal( + configuration = configuration, + featureSdkCore = mockCore, + flagsFeature = FlagsFeature(mockCore, configuration), + evaluationsFeature = null, + name = fakeClientName, + evaluationTelemetryOptOutParameterEnabled = false + ) as DatadogFlagsClient + + // When + readCallback.onSuccess( + DataStoreContent( + 1, + FlagsStateEntry( + evaluationContext = EvaluationContext("user"), + flags = mapOf( + fakeFlagKey to + PrecomputedFlag( + "boolean", + "true", + false, + "allocation", + "on", + JSONObject(), + "TARGETING_MATCH" + ) + ), + lastUpdateTimestamp = 0L, + flagSelection = CanonicalFlagSelection(tags = listOf("mobile"), keyPatterns = emptyList()) + ) + ) + ) + + // Then + assertThat(client.getFlagAssignmentsSnapshot()).containsOnlyKeys(fakeFlagKey) + } + // region Static Flag Resolution Methods @Test diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt index f0dc4b7136..b85c6338bb 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/FlagsConfigurationTest.kt @@ -6,6 +6,7 @@ package com.datadog.android.flags +import com.datadog.android.flags.internal.selection.FlagSelection import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeExtension @@ -30,6 +31,7 @@ internal class FlagsConfigurationTest { assertThat(configuration.customFlagEndpoint).isNull() assertThat(configuration.gracefulModeEnabled).isTrue() assertThat(configuration.initializationTimeoutMs).isEqualTo(5_000L) + assertThat(configuration.flagSelection).isNull() } @Test @@ -181,5 +183,63 @@ internal class FlagsConfigurationTest { assertThat(copiedConfiguration.initializationTimeoutMs).isEqualTo(2_500L) } + @Test + fun `M set the flag selection W flagSelection() { called twice }`() { + // Given + val builder = FlagsConfiguration.Builder() + .flagSelection(tags = listOf("web"), keyPatterns = listOf("web-*")) + + // When + val configuration = builder + .flagSelection(tags = listOf("mobile"), keyPatterns = emptyList()) + .build() + + // Then + assertThat(configuration.flagSelection) + .isEqualTo(FlagSelection(tags = listOf("mobile"), keyPatterns = emptyList())) + } + + @Test + fun `M copy the lists W flagSelection() { caller mutates the lists after the call }`() { + // Given + val tags = mutableListOf("mobile") + val keyPatterns = mutableListOf("checkout-*") + val builder = FlagsConfiguration.Builder().flagSelection(tags, keyPatterns) + + // When + tags += "web" + keyPatterns.clear() + + // Then + assertThat(builder.build().flagSelection) + .isEqualTo(FlagSelection(tags = listOf("mobile"), keyPatterns = listOf("checkout-*"))) + } + + @Test + fun `M keep null lists as absent W flagSelection() { null lists }`() { + // When + val configuration = FlagsConfiguration.Builder() + .flagSelection(tags = null, keyPatterns = listOf("checkout-*")) + .build() + + // Then + assertThat(configuration.flagSelection) + .isEqualTo(FlagSelection(tags = null, keyPatterns = listOf("checkout-*"))) + } + + @Test + fun `M preserve the flag selection W copy() { legacy parameters }`() { + // Given + val testedConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = listOf("mobile"), keyPatterns = listOf("checkout-*")) + .build() + + // When + val copiedConfiguration = testedConfiguration.copy(trackExposures = false) + + // Then + assertThat(copiedConfiguration.flagSelection).isEqualTo(testedConfiguration.flagSelection) + } + // endregion } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt index e6ccacc271..a56a54bf1e 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt @@ -206,6 +206,7 @@ internal class DatadogFlagsClientTest { featureSdkCore = mockFeatureSdkCore, instanceName = fakeJsonKey, dataStore = mock(), + flagSelection = null, persistenceLoadTimeoutMs = 0 ) ) @@ -853,7 +854,7 @@ internal class DatadogFlagsClientTest { "number, -2147483649", "number, -1e20", "number, 100e2147483647", - "number, 1e2147483648", + "number, 1e300", "integer, 2147483648", "float, 2147483648.0" ) @@ -1004,7 +1005,6 @@ internal class DatadogFlagsClientTest { "number, 9007199254740992, 9007199254740992.0", "number, -9223372036854775808, -9223372036854775808.0", "float, 18446744073709551616, 18446744073709551616.0", - "number, 1e300, 1e300", "number, 1.20000000000000001, 1.2", "float, 9007199254740993.5, 9007199254740994.0" ) @@ -1039,7 +1039,8 @@ internal class DatadogFlagsClientTest { "number, 9223372036854775807", "number, 9007199254740993.0", "number, 1e400", - "number, 1e2147483648", + "number, 1e300", + "number, 1e-400", "integer, 9007199254740993", "float, 9007199254740993" ) @@ -1066,8 +1067,8 @@ internal class DatadogFlagsClientTest { assertThat(result.value).isEqualTo(fakeDefaultValue) assertThat(result.errorCode).isEqualTo(ErrorCode.TYPE_MISMATCH) assertThat(result.errorMessage).isEqualTo( - "Flag '$fakeFlagKey': Flag has type '$fakeVariationType' with whole value " + - "'$fakeVariationValue' that Double can't hold exactly but Double was requested" + "Flag '$fakeFlagKey': Flag has type '$fakeVariationType' with value " + + "'$fakeVariationValue' that Double can't represent but Double was requested" ) verifyNoInteractions(mockProcessor) } @@ -1934,7 +1935,7 @@ internal class DatadogFlagsClientTest { val fakeFlagKey1 = forge.anAlphabeticalString() val fakeFlagKey2 = forge.anAlphabeticalString() val fakeFlagKey3 = forge.anAlphabeticalString() - val fakeFlagValue = forge.aDouble() + val fakeFlagValue = forge.aDouble(min = -1_000_000.0, max = 1_000_000.0) val fakeFlagIntValue = forge.anInt() val fakeDefaultValue = forge.aDouble() diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt index c4a0950c09..6f16c2a9ce 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsFeatureTest.kt @@ -15,6 +15,8 @@ import com.datadog.android.flags.ExperimentationOptOutMode import com.datadog.android.flags.FlagsClient import com.datadog.android.flags.FlagsConfiguration import com.datadog.android.flags.internal.evaluation.ExperimentationOptOut +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection +import com.datadog.android.flags.internal.selection.FlagSelectionError import com.datadog.android.flags.internal.storage.ExposureEventRecordWriter import com.datadog.android.flags.internal.storage.NoOpRecordWriter import com.datadog.android.utils.verifyLog @@ -26,7 +28,9 @@ import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.junit.jupiter.MockitoExtension @@ -37,6 +41,7 @@ import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -176,6 +181,91 @@ internal class FlagsFeatureTest { ).isNotNull() } + @Test + fun `M expose the canonical flag selection W constructor { valid selection }`() { + // When + val feature = FlagsFeature( + sdkCore = mockSdkCore, + flagsConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = listOf(" web", "mobile", "web"), keyPatterns = listOf("checkout-*")) + .build() + ) + + // Then + assertThat(feature.flagSelection) + .isEqualTo(CanonicalFlagSelection(tags = listOf("mobile", "web"), keyPatterns = listOf("checkout-*"))) + assertThat(feature.precomputedRequestFactory.sendsFlagSelection).isTrue() + } + + @ParameterizedTest(name = "{0}") + @MethodSource("selectionsWithoutValues") + fun `M send no selection and log nothing W onInitialize() { no values after canonicalization }`( + description: String, + tags: List?, + keyPatterns: List? + ) { + // Given + val feature = FlagsFeature( + sdkCore = mockSdkCore, + flagsConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = tags, keyPatterns = keyPatterns) + .build() + ) + + // When + feature.onInitialize(mockContext) + + // Then + assertThat(feature.flagSelection).describedAs(description).isNull() + assertThat(feature.precomputedRequestFactory.sendsFlagSelection).describedAs(description).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "flagSelection" in it }, + mode = never() + ) + FlagSelectionError.entries.forEach { + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + "Flag selection dropped", + mode = never(), + additionalProperties = mapOf("reason" to it.reason) + ) + } + } + + @Test + fun `M log the dropped flag selection W onInitialize() { selection over a limit }`() { + // Given + val feature = FlagsFeature( + sdkCore = mockSdkCore, + flagsConfiguration = FlagsConfiguration.Builder() + .flagSelection(tags = List(32) { "tag-$it" }, keyPatterns = emptyList()) + .build() + ) + + // When + feature.onInitialize(mockContext) + + // Then + assertThat(feature.flagSelection).isNull() + assertThat(feature.precomputedRequestFactory.sendsFlagSelection).isFalse() + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "flagSelection was not sent (too_many_values: each list accepts at most 31 values). " + + "Loading the full flag configuration." + ) + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + "Flag selection dropped", + onlyOnce = true, + additionalProperties = mapOf("reason" to "too_many_values") + ) + } + @Test fun `M reset dataWriter to NoOp W onStop`() { // Given @@ -468,4 +558,13 @@ internal class FlagsFeatureTest { } // endregion + + companion object { + @JvmStatic + fun selectionsWithoutValues() = listOf( + Arguments.of("empty lists", emptyList(), emptyList()), + Arguments.of("whitespace-only values", listOf(" ", "\u3000"), listOf("")), + Arguments.of("null lists", null, null) + ) + } } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt index 863c47d6fe..d9a7e3f10d 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/evaluation/EvaluationsManagerTest.kt @@ -21,6 +21,7 @@ import com.datadog.android.flags.FlagsInitializationTimeoutException import com.datadog.android.flags.FlagsInvalidContextException import com.datadog.android.flags.FlagsStateListener import com.datadog.android.flags.internal.FlagsStateManager +import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedAssignments import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Diagnostics @@ -30,6 +31,7 @@ import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Tr import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.internal.net.NetworkRequestFailedException import com.datadog.android.flags.internal.net.PrecomputedAssignmentsReader +import com.datadog.android.flags.internal.net.PrecomputedAssignmentsResponse import com.datadog.android.flags.internal.repository.DefaultFlagsRepository import com.datadog.android.flags.internal.repository.FlagsInstallation import com.datadog.android.flags.internal.repository.FlagsRepository @@ -39,6 +41,7 @@ import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.FlagsClientState import com.datadog.android.flags.utils.forge.ForgeConfigurator import com.datadog.android.internal.utils.DDCoreStateHolder +import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery @@ -62,6 +65,7 @@ import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeast import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow @@ -115,7 +119,7 @@ internal class EvaluationsManagerTest { whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), any(), any())).thenAnswer { readerEntered.countDown() check(releaseReader.await(5, TimeUnit.SECONDS)) - "response" + PrecomputedAssignmentsResponse("response", null) } whenever(mockPrecomputeMapper.map(eq("response"), anyOrNull())) .thenReturn(PrecomputedAssignments(emptyMap(), null)) @@ -414,7 +418,7 @@ internal class EvaluationsManagerTest { // Then inOrder(mockFlagsRepository, mockAssignmentsDownloader) { - verify(mockFlagsRepository).hasFlags() + verify(mockFlagsRepository).hasConfiguration() verify(mockAssignmentsDownloader).readPrecomputedFlags( eq(EvaluationContext(fakeTargetingKey, emptyMap())), eq(fakeDatadogContext), @@ -499,7 +503,8 @@ internal class EvaluationsManagerTest { whenever(mockSdkCore.internalLogger).thenReturn(mockInternalLogger) whenever(mockSdkCore.timeProvider).thenReturn(mock()) val store = mock() - val repository = DefaultFlagsRepository(mockSdkCore, fakeTargetingKey, store, persistenceLoadTimeoutMs = 0) + val repository = + DefaultFlagsRepository(mockSdkCore, fakeTargetingKey, store, null, persistenceLoadTimeoutMs = 0) val manager = EvaluationsManager( mockSdkCore, mockExecutorService, mockInternalLogger, repository, mockAssignmentsDownloader, mockPrecomputeMapper, mockFlagsStateManager, @@ -524,7 +529,7 @@ internal class EvaluationsManagerTest { } whenever( mockAssignmentsDownloader.readPrecomputedFlags(eq(context), eq(fakeDatadogContext), any()) - ).thenReturn("response") + ).thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever( mockPrecomputeMapper.map(eq("response"), anyOrNull()) ).thenReturn(PrecomputedAssignments(emptyMap(), null)) @@ -545,7 +550,8 @@ internal class EvaluationsManagerTest { whenever(mockSdkCore.internalLogger).thenReturn(mockInternalLogger) whenever(mockSdkCore.timeProvider).thenReturn(mock()) val store = mock() - val repository = DefaultFlagsRepository(mockSdkCore, fakeTargetingKey, store, persistenceLoadTimeoutMs = 0) + val repository = + DefaultFlagsRepository(mockSdkCore, fakeTargetingKey, store, null, persistenceLoadTimeoutMs = 0) val manager = EvaluationsManager( mockSdkCore, mockExecutorService, mockInternalLogger, repository, mockAssignmentsDownloader, mockPrecomputeMapper, mockFlagsStateManager, @@ -559,7 +565,7 @@ internal class EvaluationsManagerTest { }.whenever(store).setValue(any(), any(), any(), anyOrNull(), any()) whenever( mockAssignmentsDownloader.readPrecomputedFlags(any(), eq(fakeDatadogContext), any()) - ).thenReturn("response") + ).thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever( mockPrecomputeMapper.map(eq("response"), anyOrNull()) ).thenReturn(PrecomputedAssignments(emptyMap(), null)) @@ -605,7 +611,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn("response") + .thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever(mockPrecomputeMapper.map("response")).thenReturn(PrecomputedAssignments(emptyMap(), null)) // When @@ -664,7 +670,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(mockResponse) + .thenReturn(PrecomputedAssignmentsResponse(mockResponse, null)) whenever(mockPrecomputeMapper.map(mockResponse)).thenReturn(PrecomputedAssignments(expectedFlags)) // When @@ -725,7 +731,7 @@ internal class EvaluationsManagerTest { eq(fakeDatadogContext), isA() ) - ).thenReturn(invalidResponse) + ).thenReturn(PrecomputedAssignmentsResponse(invalidResponse, null)) whenever(mockPrecomputeMapper.map(invalidResponse)).thenReturn(null) // When @@ -778,7 +784,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(mockResponse) + .thenReturn(PrecomputedAssignmentsResponse(mockResponse, null)) whenever(mockPrecomputeMapper.map(mockResponse)).thenReturn(PrecomputedAssignments(expectedFlags)) // When @@ -796,7 +802,7 @@ internal class EvaluationsManagerTest { // Given val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) - whenever(mockFlagsRepository.hasFlags()).thenReturn(false) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(false) // When evaluationsManager.updateEvaluationsForContext(publicContext) @@ -813,7 +819,7 @@ internal class EvaluationsManagerTest { // Given val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) whenever(mockFlagsRepository.getEvaluationContext()).thenReturn(publicContext) // When @@ -832,7 +838,7 @@ internal class EvaluationsManagerTest { val requestedContext = EvaluationContext(fakeTargetingKey, emptyMap()) val cachedContext = EvaluationContext("different-user", emptyMap()) - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) whenever(mockFlagsRepository.getEvaluationContext()).thenReturn(cachedContext) whenever( mockAssignmentsDownloader.readPrecomputedFlags( @@ -868,7 +874,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(jsonResponse) + .thenReturn(PrecomputedAssignmentsResponse(jsonResponse, null)) whenever(mockPrecomputeMapper.map(jsonResponse)).thenReturn(PrecomputedAssignments(flagsMap)) // When @@ -887,7 +893,7 @@ internal class EvaluationsManagerTest { val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) val mockCallback = mock() - whenever(mockFlagsRepository.hasFlags()).thenReturn(false) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(false) // When evaluationsManager.updateEvaluationsForContext(publicContext, callback = mockCallback) @@ -905,7 +911,7 @@ internal class EvaluationsManagerTest { val publicContext = EvaluationContext(fakeTargetingKey, emptyMap()) val mockCallback = mock() - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) // When evaluationsManager.updateEvaluationsForContext(publicContext, callback = mockCallback) @@ -931,7 +937,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(jsonResponse) + .thenReturn(PrecomputedAssignmentsResponse(jsonResponse, null)) whenever(mockPrecomputeMapper.map(jsonResponse)).thenReturn(PrecomputedAssignments(flagsMap)) // When / Then - should not throw @@ -957,7 +963,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -1014,7 +1020,7 @@ internal class EvaluationsManagerTest { callbackError = error } } - whenever(mockFlagsRepository.hasFlags()).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(true) whenever(mockFlagsRepository.getEvaluationContext()).thenReturn(context) whenever( mockAssignmentsDownloader.readPrecomputedFlags( @@ -1055,8 +1061,8 @@ internal class EvaluationsManagerTest { onStateChanged = FlagsStateListener::onStateChanged ) ) - whenever(mockFlagsRepository.hasFlags()).thenReturn(false) - whenever(mockFlagsRepository.hasLoadedFlagsForContext(context)).thenReturn(true) + whenever(mockFlagsRepository.hasConfiguration()).thenReturn(false) + whenever(mockFlagsRepository.hasConfigurationForContext(context)).thenReturn(true) whenever( mockAssignmentsDownloader.readPrecomputedFlags( eq(context), @@ -1109,7 +1115,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val stateManager = FlagsStateManager( DDCoreStateHolder.create( @@ -1160,7 +1166,7 @@ internal class EvaluationsManagerTest { val callbackCompleted = CountDownLatch(1) var timeoutAction: (() -> Unit)? = null whenever(mockExecutorService.execute(any())).thenAnswer { null } - whenever(mockFlagsRepository.hasFlags()).thenAnswer { + whenever(mockFlagsRepository.hasConfiguration()).thenAnswer { cacheReadStarted.countDown() check(releaseCacheRead.await(1, TimeUnit.SECONDS)) false @@ -1272,7 +1278,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -1316,7 +1322,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -1349,7 +1355,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenAnswer { checkNotNull(timeoutAction).invoke() PrecomputedAssignments(emptyMap()) @@ -1384,7 +1390,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = 2_500L, @@ -1415,7 +1421,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val testedManager = createManager( initializationTimeoutMs = null, @@ -1476,7 +1482,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(EMPTY_FLAGS_RESPONSE_JSON) + .thenReturn(EMPTY_FLAGS_RESPONSE) whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) val stateManager = FlagsStateManager( DDCoreStateHolder.create( @@ -1607,7 +1613,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(jsonResponse) + .thenReturn(PrecomputedAssignmentsResponse(jsonResponse, null)) whenever(mockPrecomputeMapper.map(jsonResponse)).thenReturn(PrecomputedAssignments(flagsMap)) // When @@ -1622,7 +1628,7 @@ internal class EvaluationsManagerTest { // Given whenever(mockSdkCore.internalLogger).thenReturn(mockInternalLogger) whenever(mockSdkCore.timeProvider).thenReturn(mock()) - val repository = DefaultFlagsRepository(mockSdkCore, "late-success", mock(), persistenceLoadTimeoutMs = 1) + val repository = DefaultFlagsRepository(mockSdkCore, "late-success", mock(), null, persistenceLoadTimeoutMs = 1) val state = FlagsStateManager( DDCoreStateHolder.create(FlagsClientState.NotReady, FlagsStateListener::onStateChanged) ) @@ -1658,7 +1664,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn("response") + .thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever(mockPrecomputeMapper.map("response")) .thenReturn(PrecomputedAssignments(mapOf("enabled" to mock()))) @@ -1691,7 +1697,7 @@ internal class EvaluationsManagerTest { whenever(mockSdkCore.timeProvider).thenReturn(mock()) doThrow(storageError).whenever(store) .setValue(any(), any(), any(), anyOrNull(), any()) - val repository = DefaultFlagsRepository(mockSdkCore, "throwing", store, persistenceLoadTimeoutMs = 1) + val repository = DefaultFlagsRepository(mockSdkCore, "throwing", store, null, persistenceLoadTimeoutMs = 1) val state = FlagsStateManager( DDCoreStateHolder.create(FlagsClientState.NotReady, FlagsStateListener::onStateChanged) ) @@ -1731,7 +1737,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn("response") + .thenReturn(PrecomputedAssignmentsResponse("response", null)) whenever(mockPrecomputeMapper.map("response")).thenReturn(PrecomputedAssignments(emptyMap())) // When @@ -1766,7 +1772,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(ENCODED_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(ENCODED_RESPONSE_JSON, null)) // When testedManager.updateEvaluationsForContext(context, mockCallback) @@ -1783,7 +1789,7 @@ internal class EvaluationsManagerTest { eq(fakeDatadogContext), isA() ) - ).thenReturn(invalidResponse) + ).thenReturn(PrecomputedAssignmentsResponse(invalidResponse, null)) // When testedManager.updateEvaluationsForContext(context, mockCallback) @@ -1804,7 +1810,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(invalidResponse) + .thenReturn(PrecomputedAssignmentsResponse(invalidResponse, null)) // When testedManager.updateEvaluationsForContext(otherContext, mockCallback) @@ -1832,7 +1838,7 @@ internal class EvaluationsManagerTest { isA() ) ) - .thenReturn(ENCODED_RESPONSE_JSON) + .thenReturn(PrecomputedAssignmentsResponse(ENCODED_RESPONSE_JSON, null)) // When testedManager.updateEvaluationsForContext(context, mockCallback) @@ -1841,7 +1847,7 @@ internal class EvaluationsManagerTest { val failure = argumentCaptor() verify(mockCallback).onFailure(failure.capture()) assertThat(failure.firstValue.message).isEqualTo("Unable to read the feature flags response.") - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isFalse() assertThat(testedRepository.getAssignmentsSnapshot()?.flags.orEmpty()).isEmpty() } @@ -1861,11 +1867,12 @@ internal class EvaluationsManagerTest { val persistedEntry = FlagsStateEntry( evaluationContext = context, flags = mapOf("cached-flag" to flag), - lastUpdateTimestamp = 0L + lastUpdateTimestamp = 0L, + flagSelection = null ) // Configure DataStore to fire callback synchronously during DefaultFlagsRepository - // construction, so the persistence latch is counted down before hasFlags() is called. + // construction, so the persistence latch is counted down before hasConfiguration() is called. val mockDataStore = mock() whenever( mockDataStore.value( @@ -1886,6 +1893,7 @@ internal class EvaluationsManagerTest { val realRepository = DefaultFlagsRepository( featureSdkCore = mockSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "integration-test" ) @@ -1917,6 +1925,200 @@ internal class EvaluationsManagerTest { // endregion @ParameterizedTest + @ValueSource(strings = ["too_many_values", "tag_load_failed", "unknown"]) + fun `M log the reason and install W updateEvaluationsForContext() { flag selection ignored }`( + fakeIgnoredReason: String + ) { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever( + mockAssignmentsDownloader.readPrecomputedFlags( + eq(context), + eq(fakeDatadogContext), + isA() + ) + ) + .thenReturn(PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, fakeIgnoredReason)) + whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + "flagSelection was ignored by the server ($fakeIgnoredReason). Loaded the full flag configuration." + ) + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.TELEMETRY, + "Flag selection ignored", + onlyOnce = true, + additionalProperties = mapOf("reason" to fakeIgnoredReason) + ) + assertThat(capturedLogMessages()).contains("Successfully processed context $fakeTargetingKey with 0 flags") + verify(mockFlagsRepository).installFlagsAndContext(eq(context), eq(emptyMap()), anyOrNull()) + verify(mockFlagsStateManager).updateState(FlagsClientState.Ready) + } + + @Test + fun `M not log the ignored reason W updateEvaluationsForContext() { ignored reason, invalid body }`( + @StringForgery fakeInvalidBody: String + ) { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever( + mockAssignmentsDownloader.readPrecomputedFlags( + eq(context), + eq(fakeDatadogContext), + isA() + ) + ).thenReturn(PrecomputedAssignmentsResponse(fakeInvalidBody, "too_many_values")) + whenever(mockPrecomputeMapper.map(fakeInvalidBody)).thenReturn(null) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } + verify(mockFlagsRepository, never()).installFlagsAndContext(any(), any(), anyOrNull()) + verify(mockFlagsStateManager).updateState(isA()) + } + + @Test + fun `M not log the ignored reason W updateEvaluationsForContext() { ignored reason, unsupported encoding }`() { + // Given + fakeDatadogContext = fakeDatadogContext.copy(source = "react-native") + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever( + mockAssignmentsDownloader.readPrecomputedFlags( + eq(context), + eq(fakeDatadogContext), + isA() + ) + ).thenReturn(PrecomputedAssignmentsResponse(ENCODED_RESPONSE_JSON, "too_many_values")) + whenever(mockPrecomputeMapper.map(ENCODED_RESPONSE_JSON)).thenReturn( + PrecomputedAssignments(emptyMap(), FlagKeyObfuscation.fromSalt("000102030405060708090a0b0c0d0e0f")) + ) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } + verify(mockFlagsRepository, never()).installFlagsAndContext(any(), any(), anyOrNull()) + verify(mockFlagsStateManager).updateState(isA()) + } + + @ParameterizedTest + @EnumSource(ExperimentationOptOutMode::class, names = ["IMMEDIATE", "FUTURE_EXPERIMENTS_ONLY"]) + fun `M not log the ignored reason W updateEvaluationsForContext() { ignored reason, superseded request }`( + mode: ExperimentationOptOutMode, + forge: Forge + ) { + // Given + val tasks = mutableListOf() + val testedManager = queuedManager(mode, tasks) + val first = EvaluationContext(forge.anAlphabeticalString()) + val firstCallback = mock() + whenever(mockAssignmentsDownloader.readPrecomputedFlags(eq(first), any(), any())) doReturn + PrecomputedAssignmentsResponse("response", "too_many_values") + whenever(mockPrecomputeMapper.map(any(), anyOrNull())) doReturn PrecomputedAssignments(emptyMap(), null) + testedManager.updateEvaluationsForContext(first, firstCallback) + testedManager.updateEvaluationsForContext(EvaluationContext(forge.anAlphabeticalString())) + + // When + tasks[0].run() + + // Then + verify(firstCallback).onFailure(isA()) + assertThat(capturedLogMessages()).noneMatch { + "flagSelection" in it || "Flag selection" in it || "Successfully processed" in it + } + } + + @Test + fun `M not log a flag selection message W updateEvaluationsForContext() { no ignored reason }`() { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + whenever( + mockAssignmentsDownloader.readPrecomputedFlags( + eq(context), + eq(fakeDatadogContext), + isA() + ) + ) + .thenReturn(EMPTY_FLAGS_RESPONSE) + whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) + + // When + evaluationsManager.updateEvaluationsForContext(context) + + // Then + assertThat(capturedLogMessages()).noneMatch { "flagSelection" in it || "Flag selection" in it } + verify(mockFlagsRepository).installFlagsAndContext(eq(context), eq(emptyMap()), anyOrNull()) + } + + @Test + fun `M notify STALE W updateEvaluationsForContext() { failed fetch after an empty configuration }`() { + // Given + val context = EvaluationContext(fakeTargetingKey, emptyMap()) + val mockDataStore = mock() + whenever( + mockDataStore.value( + key = any(), + version = anyOrNull(), + callback = any(), + deserializer = any() + ) + ).doAnswer { + it.getArgument>(2).onFailure() + null + } + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.timeProvider) doReturn mock() + val realRepository = DefaultFlagsRepository( + featureSdkCore = mockSdkCore, + dataStore = mockDataStore, + flagSelection = null, + instanceName = "empty-configuration-test" + ) + whenever( + mockAssignmentsDownloader.readPrecomputedFlags( + eq(context), + eq(fakeDatadogContext), + isA() + ) + ) + .thenReturn(EMPTY_FLAGS_RESPONSE, null) + whenever(mockPrecomputeMapper.map(EMPTY_FLAGS_RESPONSE_JSON)).thenReturn(PrecomputedAssignments(emptyMap())) + val testedManager = EvaluationsManager( + sdkCore = mockSdkCore, + executorService = mockExecutorService, + internalLogger = mockInternalLogger, + flagsRepository = realRepository, + assignmentsReader = mockAssignmentsDownloader, + precomputeMapper = mockPrecomputeMapper, + flagStateManager = mockFlagsStateManager, + initializationTimeoutMs = null, + initializationTimeoutScheduler = { _, _ -> {} }, + experimentationOptOut = ExperimentationOptOut() + ) + + // When + testedManager.updateEvaluationsForContext(context) + testedManager.updateEvaluationsForContext(context) + + // Then + inOrder(mockFlagsStateManager) { + verify(mockFlagsStateManager).updateState(FlagsClientState.Reconciling) + verify(mockFlagsStateManager).updateState(FlagsClientState.Ready) + verify(mockFlagsStateManager).updateState(FlagsClientState.Reconciling) + verify(mockFlagsStateManager).updateState(FlagsClientState.Stale) + } + } + @CsvSource("IMMEDIATE,true", "IMMEDIATE,false", "FUTURE_EXPERIMENTS_ONLY,true", "FUTURE_EXPERIMENTS_ONLY,false") fun `M fail superseded callback W updateEvaluationsForContext() {superseded fetch fails}`( mode: ExperimentationOptOutMode, @@ -1930,7 +2132,7 @@ internal class EvaluationsManagerTest { val firstCallback = mock() val newerCallback = mock() whenever(mockAssignmentsDownloader.readPrecomputedFlags(eq(first), any(), any())) doReturn - if (networkFailed) null else "unreadable" + if (networkFailed) null else PrecomputedAssignmentsResponse("unreadable", null) testedManager.updateEvaluationsForContext(first, firstCallback) testedManager.updateEvaluationsForContext(EvaluationContext(forge.anAlphabeticalString()), newerCallback) @@ -2000,7 +2202,8 @@ internal class EvaluationsManagerTest { var cancellations = 0 val testedManager = queuedManager(mode, tasks, timeouts, onCancel = { cancellations++ }) val firstCallback = mock() - whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), any(), any())) doReturn "response" + whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), any(), any())) doReturn + PrecomputedAssignmentsResponse("response", null) whenever(mockPrecomputeMapper.map(any(), anyOrNull())) doReturn PrecomputedAssignments(emptyMap(), null) testedManager.updateEvaluationsForContext(EvaluationContext(forge.anAlphabeticalString()), firstCallback) testedManager.updateEvaluationsForContext(EvaluationContext(forge.anAlphabeticalString())) @@ -2037,7 +2240,8 @@ internal class EvaluationsManagerTest { val testedManager = queuedManager(mode, tasks, timeouts) val callback = mock() if (succeeds) { - whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), any(), any())) doReturn "response" + whenever(mockAssignmentsDownloader.readPrecomputedFlags(any(), any(), any())) doReturn + PrecomputedAssignmentsResponse("response", null) whenever(mockPrecomputeMapper.map(any(), anyOrNull())) doReturn PrecomputedAssignments(emptyMap(), null) } testedManager.updateEvaluationsForContext(EvaluationContext(forge.anAlphabeticalString()), callback) @@ -2138,7 +2342,7 @@ internal class EvaluationsManagerTest { doAnswer { it.getArgument>(2).onFailure() }.whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) - return DefaultFlagsRepository(mockSdkCore, fakeTargetingKey, mockDataStore) + return DefaultFlagsRepository(mockSdkCore, fakeTargetingKey, mockDataStore, null) } private fun createManagerWithRepository(repository: FlagsRepository): EvaluationsManager = EvaluationsManager( @@ -2171,6 +2375,19 @@ internal class EvaluationsManagerTest { experimentationOptOut = ExperimentationOptOut() ) + private fun capturedLogMessages(): List { + val messages = argumentCaptor<() -> String>() + verify(mockInternalLogger, atLeast(0)).log( + any(), + any(), + messages.capture(), + anyOrNull(), + any(), + anyOrNull() + ) + return messages.allValues.map { it() } + } + companion object { private const val QUEUED_TIMEOUT_MS = 1_000L private val ENCODED_RESPONSE_JSON = """ @@ -2185,6 +2402,7 @@ internal class EvaluationsManagerTest { }}} """.trimIndent() private const val EMPTY_FLAGS_RESPONSE_JSON = "{\"data\": {\"attributes\": {\"flags\": {}}}}" + private val EMPTY_FLAGS_RESPONSE = PrecomputedAssignmentsResponse(EMPTY_FLAGS_RESPONSE_JSON, null) } @ParameterizedTest @@ -2200,9 +2418,10 @@ internal class EvaluationsManagerTest { lateinit var testedManager: EvaluationsManager whenever(mockAssignmentsDownloader.readPrecomputedFlags(eq(first), any(), any())).thenAnswer { testedManager.updateEvaluationsForContext(newer, newerCallback) - "first-response" + PrecomputedAssignmentsResponse("first-response", null) } - whenever(mockAssignmentsDownloader.readPrecomputedFlags(eq(newer), any(), any())) doReturn "newer-response" + whenever(mockAssignmentsDownloader.readPrecomputedFlags(eq(newer), any(), any())) doReturn + PrecomputedAssignmentsResponse("newer-response", null) whenever(mockPrecomputeMapper.map(any(), anyOrNull())) doReturn PrecomputedAssignments(emptyMap(), null) testedManager = EvaluationsManager( mockSdkCore, mockExecutorService, mockInternalLogger, mockFlagsRepository, @@ -2229,7 +2448,7 @@ internal class EvaluationsManagerTest { // Given doAnswer { null }.whenever(mockExecutorService).execute(any()) val fakeNewer = EvaluationContext("newer-subject") - whenever(mockFlagsRepository.hasLoadedFlagsForContext(fakeNewer)) doReturn cached + whenever(mockFlagsRepository.hasConfigurationForContext(fakeNewer)) doReturn cached lateinit var timeout: () -> Unit val manager = EvaluationsManager( mockSdkCore, @@ -2260,6 +2479,6 @@ internal class EvaluationsManagerTest { } else { verify(mockFlagsStateManager).updateState(isA()) } - verify(mockFlagsRepository).hasLoadedFlagsForContext(fakeNewer) + verify(mockFlagsRepository).hasConfigurationForContext(fakeNewer) } } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt index 2d97e41432..1348a73465 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsDownloaderTest.kt @@ -18,6 +18,7 @@ import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import okhttp3.Call +import okhttp3.Headers import okhttp3.MediaType.Companion.toMediaType import okhttp3.Protocol import okhttp3.Request @@ -29,6 +30,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -110,10 +113,111 @@ internal class PrecomputedAssignmentsDownloaderTest { val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) // Then - assertThat(result).isEqualTo(fakeResponseBody) + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, null)) verify(mockRequestFactory).create(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) } + @ParameterizedTest + @ValueSource( + strings = ["malformed_selection", "too_many_values", "invalid_key_pattern", "invalid_tag", "tag_load_failed"] + ) + fun `M return the known reason W readPrecomputedFlags() { selection sent, ignored header }`( + fakeIgnoredReason: String, + forge: Forge + ) { + // Given + val fakeResponseBody = forge.anAlphabeticalString() + val fakeUrl = "https://${forge.anAlphabeticalString()}.com/precompute" + stubSuccessfulCall(fakeResponseBody, fakeUrl, ignoredReason = fakeIgnoredReason) + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, fakeIgnoredReason)) + } + + @ParameterizedTest + @ValueSource(strings = ["Some Future-Reason", "Too_Many_Values", "too_many_values\u0001", "too_many_values\u00E9"]) + fun `M return the fallback reason W readPrecomputedFlags() { selection sent, unknown ignored header }`( + fakeIgnoredReason: String, + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + val fakeRequest = Request.Builder().url(fakeUrl).build() + val fakeResponse = createSuccessfulResponse(fakeResponseBody, fakeUrl) + .newBuilder() + .headers(Headers.Builder().addUnsafeNonAscii("DD-Flag-Selection-Ignored", fakeIgnoredReason).build()) + .build() + whenever(mockRequestFactory.create(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics)) + .doReturn(fakeRequest) + whenever(mockCallFactory.newCall(fakeRequest)).doReturn(mockCall) + whenever(mockCall.execute()).doReturn(fakeResponse) + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, "unknown")) + } + + @Test + fun `M return the ignored reason W readPrecomputedFlags() { selection sent, lowercase header name }`( + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + stubSuccessfulCall( + fakeResponseBody, + fakeUrl, + ignoredReason = "too_many_values", + headerName = "dd-flag-selection-ignored" + ) + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, "too_many_values")) + } + + @Test + fun `M return no ignored reason W readPrecomputedFlags() { selection sent, empty ignored header }`( + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + stubSuccessfulCall(fakeResponseBody, fakeUrl, ignoredReason = "") + whenever(mockRequestFactory.sendsFlagSelection) doReturn true + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, null)) + } + + @Test + fun `M return no ignored reason W readPrecomputedFlags() { no selection sent, ignored header }`( + @StringForgery fakeResponseBody: String, + @StringForgery(regex = "[a-z_]+") fakeIgnoredReason: String, + @StringForgery(regex = "https://[a-z]+\\.(com|net)/[a-z]+") fakeUrl: String + ) { + // Given + stubSuccessfulCall(fakeResponseBody, fakeUrl, ignoredReason = fakeIgnoredReason) + whenever(mockRequestFactory.sendsFlagSelection) doReturn false + + // When + val result = testedDownloader.readPrecomputedFlags(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics) + + // Then + assertThat(result).isEqualTo(PrecomputedAssignmentsResponse(fakeResponseBody, null)) + } + @Test fun `M call factory with correct parameters W readPrecomputedFlags()`( @StringForgery fakeResponseBody: String, @@ -398,6 +502,23 @@ internal class PrecomputedAssignmentsDownloaderTest { // region Helper methods + private fun stubSuccessfulCall( + body: String, + url: String, + ignoredReason: String, + headerName: String = "DD-Flag-Selection-Ignored" + ) { + val fakeRequest = Request.Builder().url(url).build() + val fakeResponse = createSuccessfulResponse(body, url) + .newBuilder() + .header(headerName, ignoredReason) + .build() + whenever(mockRequestFactory.create(fakeEvaluationContext, fakeDatadogContext, fakeDiagnostics)) + .doReturn(fakeRequest) + whenever(mockCallFactory.newCall(fakeRequest)) doReturn mockCall + whenever(mockCall.execute()) doReturn fakeResponse + } + private fun createSuccessfulResponse(body: String, url: String): Response = Response.Builder() .request(Request.Builder().url(url).build()) .protocol(Protocol.HTTP_1_1) diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt index 2ebd857fbf..ccbe3fabbb 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/net/PrecomputedAssignmentsRequestFactoryTest.kt @@ -15,6 +15,7 @@ import com.datadog.android.flags.internal.evaluation.ExperimentationOptOut import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Diagnostics import com.datadog.android.flags.internal.model.PrecomputedAssignmentsRequest.Trigger +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.utils.forge.ForgeConfigurator import com.datadog.android.internal.rum.RumContext @@ -33,7 +34,9 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -67,7 +70,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { ) ).withRumContext(fakeRumContext) - testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null) + testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null, null) } @Test @@ -199,7 +202,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { ) { // Given val fakeCustomEndpoint = "https://custom-proxy.example.com/flags/assignments" - testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, fakeCustomEndpoint) + testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, fakeCustomEndpoint, null) val context = EvaluationContext( targetingKey = fakeTargetingKey, attributes = emptyMap() @@ -220,7 +223,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { @EnumSource(ExperimentationOptOutMode::class) fun `M omit evaluation options W create() {timestamp absent}`(mode: ExperimentationOptOutMode) { // Given - testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null, mode) + testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null, null, mode) val context = EvaluationContext("subject") // When @@ -238,7 +241,7 @@ internal class PrecomputedAssignmentsRequestFactoryTest { mode: ExperimentationOptOutMode ) { // Given - testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null, mode) + testedFactory = PrecomputedAssignmentsRequestFactory(mockInternalLogger, null, null, mode) val context = EvaluationContext( "subject", mapOf(ExperimentationOptOut.ATTRIBUTE to "2026-10-01T00:00:00Z") @@ -324,6 +327,111 @@ internal class PrecomputedAssignmentsRequestFactoryTest { assertThat(targetingAttributes.length()).isEqualTo(0) } + @ParameterizedTest + @ValueSource(booleans = [true, false]) + fun `M add flag_selection to attributes W create() { flag selection configured }`( + useCustomEndpoint: Boolean, + @StringForgery fakeTargetingKey: String + ) { + // Given + val fakeCustomEndpoint = "https://custom-proxy.example.com/flags/assignments".takeIf { useCustomEndpoint } + testedFactory = PrecomputedAssignmentsRequestFactory( + mockInternalLogger, + fakeCustomEndpoint, + CanonicalFlagSelection(tags = listOf("mobile", "web"), keyPatterns = listOf("checkout-*")) + ) + val context = EvaluationContext(targetingKey = fakeTargetingKey, attributes = emptyMap()) + + // When + val request = testedFactory.create(context, fakeDatadogContext, fakeDiagnostics) + + // Then + checkNotNull(request) + val flagSelection = JSONObject(extractRequestBodyAsString(request)) + .getJSONObject("data") + .getJSONObject("attributes") + .getJSONObject("flag_selection") + assertThat(flagSelection.keys().asSequence().toList()).containsExactlyInAnyOrder("tags", "key_patterns") + assertThat(flagSelection.getJSONArray("tags").toString()).isEqualTo("""["mobile","web"]""") + assertThat(flagSelection.getJSONArray("key_patterns").toString()).isEqualTo("""["checkout-*"]""") + assertThat(testedFactory.sendsFlagSelection).isTrue() + } + + @ParameterizedTest + @CsvSource("IMMEDIATE,immediate", "FUTURE_EXPERIMENTS_ONLY,future_experiments_only") + fun `M send flag_selection and evaluation_options W create() { selection and opt-out }`( + mode: ExperimentationOptOutMode, + expectedWireMode: String + ) { + // Given + testedFactory = PrecomputedAssignmentsRequestFactory( + mockInternalLogger, + null, + CanonicalFlagSelection(tags = listOf("mobile"), keyPatterns = listOf("checkout-*")), + mode + ) + val context = EvaluationContext( + "subject", + mapOf(ExperimentationOptOut.ATTRIBUTE to "2026-10-01T00:00:00Z") + ) + + // When + val request = checkNotNull(testedFactory.create(context, fakeDatadogContext, fakeDiagnostics)) + + // Then + val attributes = JSONObject(extractRequestBodyAsString(request)) + .getJSONObject("data").getJSONObject("attributes") + val flagSelection = attributes.getJSONObject("flag_selection") + assertThat(flagSelection.getJSONArray("tags").toString()).isEqualTo("""["mobile"]""") + assertThat(flagSelection.getJSONArray("key_patterns").toString()).isEqualTo("""["checkout-*"]""") + assertThat(attributes.getJSONObject("evaluation_options").getString("experimentation_opt_out_mode")) + .isEqualTo(expectedWireMode) + } + + @Test + fun `M send both lists W create() { flag selection with key patterns only }`( + @StringForgery fakeTargetingKey: String + ) { + // Given + testedFactory = PrecomputedAssignmentsRequestFactory( + mockInternalLogger, + null, + CanonicalFlagSelection(tags = emptyList(), keyPatterns = listOf("checkout-*")) + ) + val context = EvaluationContext(targetingKey = fakeTargetingKey, attributes = emptyMap()) + + // When + val request = testedFactory.create(context, fakeDatadogContext, fakeDiagnostics) + + // Then + checkNotNull(request) + val flagSelection = JSONObject(extractRequestBodyAsString(request)) + .getJSONObject("data") + .getJSONObject("attributes") + .getJSONObject("flag_selection") + assertThat(flagSelection.getJSONArray("tags").length()).isZero() + assertThat(flagSelection.getJSONArray("key_patterns").toString()).isEqualTo("""["checkout-*"]""") + } + + @Test + fun `M not add flag_selection W create() { no flag selection }`( + @StringForgery fakeTargetingKey: String + ) { + // Given + val context = EvaluationContext(targetingKey = fakeTargetingKey, attributes = emptyMap()) + + // When + val request = testedFactory.create(context, fakeDatadogContext, fakeDiagnostics) + + // Then + checkNotNull(request) + val attributes = JSONObject(extractRequestBodyAsString(request)) + .getJSONObject("data") + .getJSONObject("attributes") + assertThat(attributes.keys().asSequence().toList()).containsExactlyInAnyOrder("env", "source", "subject") + assertThat(testedFactory.sendsFlagSelection).isFalse() + } + // endregion // region create() - Error cases diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt index 7c39cebf7e..df9cedd096 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateDeserializerTest.kt @@ -12,6 +12,7 @@ import com.google.gson.JsonParser import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat +import org.json.JSONArray import org.json.JSONObject import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -358,6 +359,33 @@ internal class FlagsStateDeserializerTest { assertThat(result.evaluationContext.attributes["valid_boolean"]).isEqualTo(validBoolean) } + @Test + fun `M return no flag selection W deserialize() { state persisted without a flag selection }`(forge: Forge) { + // Given + val json = buildStateJson(forge.anAlphabeticalString(), JSONObject().put("flag1", buildFlagJson())) + + // When + val result = testedDeserializer.deserialize(json) + + // Then + checkNotNull(result) + assertThat(result.flagSelection).isNull() + } + + @Test + fun `M return null W deserialize() { malformed flag selection }`(forge: Forge) { + // Given + val json = JSONObject(buildStateJson(forge.anAlphabeticalString(), JSONObject())) + .put("flagSelection", JSONObject().put("tags", JSONArray())) + .toString() + + // When + val result = testedDeserializer.deserialize(json) + + // Then + assertThat(result).isNull() + } + @ParameterizedTest @ValueSource(strings = ["null", "[]", "true", "0", "\"text\""]) fun `M reject nonobject cache W deserialize() { generated model }`(json: String) { diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt index f774526dd0..106ebfae07 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/persistence/FlagsStateSerializerTest.kt @@ -10,6 +10,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.flags.internal.model.FlagKeyObfuscation import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection import com.datadog.android.flags.model.EvaluationContext import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.LongForgery @@ -188,4 +189,44 @@ internal class FlagsStateSerializerTest { SerializedFlagsStateAssert.assertThatSerializedFlagsState(serialized) .hasEmptyFlags() } + + @Test + fun `M round trip the flag selection W serialize() + deserialize()`(forge: Forge) { + // Given + val flagSelection = CanonicalFlagSelection( + tags = listOf("mobile", "\u00E9t\u00E9"), + keyPatterns = listOf("checkout-*") + ) + val flagsState = FlagsStateEntry( + EvaluationContext(forge.anAlphabeticalString(), emptyMap()), + emptyMap(), + fakeTimestamp, + flagSelection = flagSelection + ) + + // When + val serialized = testedSerializer.serialize(flagsState) + val deserialized = FlagsStateDeserializer(mockInternalLogger).deserialize(serialized) + + // Then + checkNotNull(deserialized) + assertThat(deserialized.flagSelection).isEqualTo(flagSelection) + } + + @Test + fun `M omit the flag selection W serialize() { no flag selection }`(forge: Forge) { + // Given + val flagsState = FlagsStateEntry( + EvaluationContext(forge.anAlphabeticalString(), emptyMap()), + emptyMap(), + fakeTimestamp, + null + ) + + // When + val serialized = testedSerializer.serialize(flagsState) + + // Then + assertThat(JSONObject(serialized).has("flagSelection")).isFalse() + } } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt index 0031ac0edf..d0fc7c3914 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryObfuscationTest.kt @@ -91,7 +91,7 @@ internal class DefaultFlagsRepositoryObfuscationTest { whenever(mockSdkCore.source) doReturn "android" doAnswer { it.getArgument>(2).onFailure() } .whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) - testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore) + testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore, null) } @Test @@ -102,7 +102,7 @@ internal class DefaultFlagsRepositoryObfuscationTest { .whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) // When - val testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore) + val testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore, null) // Then val (flag, context) = checkNotNull(testedRepository.getPrecomputedFlagWithContext(fakeFlagKey)) @@ -125,7 +125,7 @@ internal class DefaultFlagsRepositoryObfuscationTest { .whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) whenever(mockSdkCore.source) doReturn source val entry = cachedEntry(encoded) - testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore) + testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore, null) val firstFlags = mutableListOf>() testedRepository.firstFlags.whenComplete { firstFlags.add(it) } @@ -136,8 +136,8 @@ internal class DefaultFlagsRepositoryObfuscationTest { // Then val readable = !encoded || source == "android" assertThat(result?.variationValue).isEqualTo(if (readable) fakeFlag.variationValue else null) - assertThat(testedRepository.hasFlags()).isEqualTo(readable) - assertThat(testedRepository.hasLoadedFlagsForContext(fakeContext)).isEqualTo(readable) + assertThat(testedRepository.hasConfiguration()).isEqualTo(readable) + assertThat(testedRepository.hasConfigurationForContext(fakeContext)).isEqualTo(readable) assertThat(testedRepository.getEvaluationContext()).isEqualTo(if (readable) fakeContext else null) assertThat(testedRepository.getPrecomputedFlagWithContext(fakeFlagKey) != null).isEqualTo(readable) assertThat(testedRepository.getFlagsSnapshot().isEmpty()).isEqualTo(encoded) @@ -170,7 +170,7 @@ internal class DefaultFlagsRepositoryObfuscationTest { whenever(mockSdkCore.source) doReturn "react-native" doAnswer { it.getArgument>(2).onSuccess(DataStoreContent(0, entry)) } .whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) - testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore) + testedRepository = DefaultFlagsRepository(mockSdkCore, fakeInstanceName, mockDataStore, null) val firstFlags = mutableListOf>() testedRepository.firstFlags.whenComplete { firstFlags.add(it) } assertThat(firstFlags).isEmpty() diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt index 2c57bb9858..d6bee059e3 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/DefaultFlagsRepositoryTest.kt @@ -11,13 +11,19 @@ import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.api.storage.datastore.DataStoreReadCallback import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.flags.ExperimentationOptOutMode import com.datadog.android.flags.internal.model.FlagsStateEntry import com.datadog.android.flags.internal.model.PrecomputedAssignments import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.internal.selection.CanonicalFlagSelection +import com.datadog.android.flags.internal.selection.FlagSelection +import com.datadog.android.flags.internal.selection.FlagSelectionCanonicalizer +import com.datadog.android.flags.internal.selection.FlagSelectionResult import com.datadog.android.flags.model.EvaluationContext import com.datadog.android.flags.model.ResolutionReason import com.datadog.android.flags.utils.forge.ForgeConfigurator import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -27,6 +33,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension @@ -36,6 +43,7 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -90,10 +98,11 @@ internal class DefaultFlagsRepositoryTest { testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "default" ) - // Setup test fixtures for hasFlags tests + // Setup test fixtures for hasConfiguration tests testContext = EvaluationContext(forge.anAlphabeticalString(), emptyMap()) singleFlagMap = mapOf( @@ -154,6 +163,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = forge.anAlphabeticalString(), persistenceLoadTimeoutMs = TimeUnit.SECONDS.toMillis(10) ) @@ -204,7 +214,7 @@ internal class DefaultFlagsRepositoryTest { } @Test - fun `M not wait for persistence W hasLoadedFlagsForContext() { persistence is loading }`(forge: Forge) { + fun `M not wait for persistence W hasConfigurationForContext() { persistence is loading }`(forge: Forge) { // Given doAnswer { null }.whenever(mockDataStore).value( key = any(), @@ -215,6 +225,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "loading", persistenceLoadTimeoutMs = TimeUnit.SECONDS.toMillis(10) ) @@ -222,7 +233,7 @@ internal class DefaultFlagsRepositoryTest { val completed = CountDownLatch(1) val context = EvaluationContext(forge.anAlphabeticalString(), emptyMap()) val readThread = Thread { - result.set(testedRepository.hasLoadedFlagsForContext(context)) + result.set(testedRepository.hasConfigurationForContext(context)) completed.countDown() }.apply { isDaemon = true } @@ -255,6 +266,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "async" ) val asyncThread = Thread { @@ -287,6 +299,7 @@ internal class DefaultFlagsRepositoryTest { val testedRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "timeout", persistenceLoadTimeoutMs = 1L ) @@ -411,7 +424,7 @@ internal class DefaultFlagsRepositoryTest { // Then assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) assertThat(testedRepository.getFlagsSnapshot()).isEmpty() - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isTrue() verify(mockDataStore, never()).setValue( key = any(), data = any(), @@ -456,26 +469,336 @@ internal class DefaultFlagsRepositoryTest { assertThat(entry.firstValue.flags).isEmpty() } - private fun preparePersistenceLoad(forge: Forge): DataStoreReadCallback { + @ParameterizedTest + @ValueSource(booleans = [true, false]) + fun `M ignore persisted state W initial disk read completes { different flag selection }`( + configureSelection: Boolean, + forge: Forge + ) { + // Given + val configuredSelection = CanonicalFlagSelection(listOf("mobile"), emptyList()).takeIf { configureSelection } + val persistedSelection = CanonicalFlagSelection(listOf("web"), emptyList()) + val flags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) + val callback = preparePersistenceLoad(forge, configuredSelection) + + // When + callback.onSuccess( + DataStoreContent( + forge.anInt(), + FlagsStateEntry(forge.getForgery(), flags, forge.aLong(), flagSelection = persistedSelection) + ) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isNull() + assertThat(testedRepository.hasConfiguration()).isFalse() + } + + @Test + fun `M ignore persisted state W initial disk read completes { state saved without a flag selection }`( + forge: Forge + ) { + // Given + val flags = mapOf(forge.anAlphabeticalString() to forge.getForgery()) + val callback = preparePersistenceLoad(forge, CanonicalFlagSelection(listOf("mobile"), emptyList())) + + // When + callback.onSuccess( + DataStoreContent(forge.anInt(), FlagsStateEntry(forge.getForgery(), flags, forge.aLong())) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isNull() + assertThat(testedRepository.hasConfiguration()).isFalse() + } + + @Test + fun `M install persisted state W initial disk read completes { same flag selection }`(forge: Forge) { + // Given + val context = forge.getForgery() + val flagKey = forge.anAlphabeticalString() + val callback = preparePersistenceLoad(forge, CanonicalFlagSelection(listOf("mobile"), listOf("checkout-*"))) + + // When + callback.onSuccess( + DataStoreContent( + forge.anInt(), + FlagsStateEntry( + context, + mapOf(flagKey to forge.getForgery()), + forge.aLong(), + flagSelection = CanonicalFlagSelection(listOf("mobile"), listOf("checkout-*")) + ) + ) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) + assertThat(testedRepository.getPrecomputedFlag(flagKey)).isNotNull() + } + + @Test + fun `M install persisted state W initial disk read completes { equivalent configured selection }`(forge: Forge) { + // Given + val context = forge.getForgery() + val flagKey = forge.anAlphabeticalString() + val persistedSelection = canonical(FlagSelection(tags = listOf("checkout"), keyPatterns = null)) + val configuredSelection = canonical(FlagSelection(tags = listOf(" checkout", "checkout"), keyPatterns = null)) + val callback = preparePersistenceLoad(forge, configuredSelection) + + // When + callback.onSuccess( + DataStoreContent( + forge.anInt(), + FlagsStateEntry( + context, + mapOf(flagKey to forge.getForgery()), + forge.aLong(), + flagSelection = persistedSelection + ) + ) + ) + + // Then + assertThat(testedRepository.getEvaluationContext()).isEqualTo(context) + assertThat(testedRepository.getPrecomputedFlag(flagKey)).isNotNull() + } + + @Test + fun `M persist the state under the selection key W setFlagsAndContext() { flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String, + forge: Forge + ) { + // Given + testedRepository = DefaultFlagsRepository( + mockFeatureSdkCore, + fakeInstanceName, + mockDataStore, + CanonicalFlagSelection(listOf("mobile"), emptyList()) + ) + + // When + testedRepository.setFlagsAndContext(forge.getForgery(), emptyMap()) + + // Then + verify(mockDataStore).setValue( + key = eq("flags-selection-state-$fakeInstanceName"), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + verify(mockDataStore, never()).setValue( + key = eq("flags-state-$fakeInstanceName"), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + } + + @Test + fun `M persist the state under the unselected key W setFlagsAndContext() { no flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String, + forge: Forge + ) { + // Given + testedRepository = DefaultFlagsRepository(mockFeatureSdkCore, fakeInstanceName, mockDataStore, null) + + // When + testedRepository.setFlagsAndContext(forge.getForgery(), emptyMap()) + + // Then + verify(mockDataStore).setValue( + key = eq("flags-state-$fakeInstanceName"), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + } + + @Test + fun `M read the selection key and remove the unselected state W init { flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String + ) { + // When + testedRepository = DefaultFlagsRepository( + mockFeatureSdkCore, + fakeInstanceName, + mockDataStore, + CanonicalFlagSelection(listOf("mobile"), emptyList()) + ) + + // Then + verify(mockDataStore).value( + key = eq("flags-selection-state-$fakeInstanceName"), + version = anyOrNull(), + callback = any(), + deserializer = any() + ) + verify(mockDataStore).removeValue(eq("flags-state-$fakeInstanceName"), anyOrNull()) + } + + @Test + fun `M read the unselected key and remove the selected state W init { no flag selection }`( + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String + ) { + // When + testedRepository = DefaultFlagsRepository(mockFeatureSdkCore, fakeInstanceName, mockDataStore, null) + + // Then + verify(mockDataStore).value( + key = eq("flags-state-$fakeInstanceName"), + version = anyOrNull(), + callback = any(), + deserializer = any() + ) + verify(mockDataStore).removeValue(eq("flags-selection-state-$fakeInstanceName"), anyOrNull()) + } + + @ParameterizedTest + @CsvSource( + "DISABLED,false,flags-state,flags-selection-state", + "DISABLED,true,flags-selection-state,flags-state", + "IMMEDIATE,false,flags-immediate-v1-state,flags-selection-immediate-v1-state", + "IMMEDIATE,true,flags-selection-immediate-v1-state,flags-immediate-v1-state", + "FUTURE_EXPERIMENTS_ONLY,false,flags-future-v1-state,flags-selection-future-v1-state", + "FUTURE_EXPERIMENTS_ONLY,true,flags-selection-future-v1-state,flags-future-v1-state" + ) + fun `M use the opt-out mode key for the selection W init and setFlagsAndContext()`( + mode: ExperimentationOptOutMode, + selected: Boolean, + expectedKeyPrefix: String, + siblingKeyPrefix: String, + @StringForgery(regex = "fake-[a-z]{4,8}") fakeInstanceName: String, + forge: Forge + ) { + // Given + val expectedKey = "$expectedKeyPrefix-$fakeInstanceName" + val siblingKey = "$siblingKeyPrefix-$fakeInstanceName" + testedRepository = DefaultFlagsRepository( + featureSdkCore = mockFeatureSdkCore, + instanceName = fakeInstanceName, + dataStore = mockDataStore, + flagSelection = if (selected) CanonicalFlagSelection(listOf("mobile"), emptyList()) else null, + experimentationOptOutMode = mode + ) + + // When + testedRepository.setFlagsAndContext(forge.getForgery(), emptyMap()) + + // Then + verify(mockDataStore).value( + key = eq(expectedKey), + version = anyOrNull(), + callback = any(), + deserializer = any() + ) + verify(mockDataStore).removeValue(eq(siblingKey), anyOrNull()) + verify(mockDataStore).setValue( + key = eq(expectedKey), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + verify(mockDataStore, never()).setValue( + key = eq(siblingKey), + data = any(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + } + + @Test + fun `M persist the configured flag selection W setFlagsAndContext()`(forge: Forge) { + // Given + val flagSelection = CanonicalFlagSelection(listOf("mobile"), listOf("checkout-*")) + preparePersistenceLoad(forge, flagSelection) + + // When + testedRepository.setFlagsAndContext(forge.getForgery(), emptyMap()) + + // Then + val entry = argumentCaptor() + verify(mockDataStore).setValue( + key = any(), + data = entry.capture(), + version = any(), + callback = anyOrNull(), + serializer = any() + ) + assertThat(entry.firstValue.flagSelection).isEqualTo(flagSelection) + } + + @Test + fun `M return no flag with the context W getPrecomputedFlagWithContext() { empty flags map }`(forge: Forge) { + // Given + val context = forge.getForgery() + testedRepository.setFlagsAndContext(context, emptyMap()) + + // When + val result = testedRepository.getPrecomputedFlagWithContext(forge.anAlphabeticalString()) + + // Then + assertThat(result).isEqualTo(null to context) + } + + @Test + fun `M return true W hasConfigurationForContext() { empty flags map for the same context }`(forge: Forge) { + // Given + val context = forge.getForgery() + testedRepository.setFlagsAndContext(context, emptyMap()) + + // When / Then + assertThat(testedRepository.hasConfigurationForContext(context)).isTrue() + } + + @Test + fun `M return false W hasConfigurationForContext() { different context }`(forge: Forge) { + // Given + val context = forge.getForgery() + testedRepository.setFlagsAndContext(context, singleFlagMap) + + // When / Then + assertThat( + testedRepository.hasConfigurationForContext(context.copy(targetingKey = context.targetingKey + "-other")) + ).isFalse() + } + + private fun canonical(selection: FlagSelection): CanonicalFlagSelection = + checkNotNull(FlagSelectionCanonicalizer.canonicalize(selection) as? FlagSelectionResult.Selected).selection + + private fun preparePersistenceLoad( + forge: Forge, + flagSelection: CanonicalFlagSelection? = null + ): DataStoreReadCallback { lateinit var callback: DataStoreReadCallback doAnswer { callback = it.getArgument(2) null }.whenever(mockDataStore).value(any(), anyOrNull(), any(), any()) - testedRepository = DefaultFlagsRepository(mockFeatureSdkCore, forge.anAlphabeticalString(), mockDataStore) + testedRepository = DefaultFlagsRepository( + mockFeatureSdkCore, + forge.anAlphabeticalString(), + mockDataStore, + flagSelection + ) return callback } - // region hasFlags + // region hasConfiguration @Test - fun `M return false W hasFlags() { no state set }`() { + fun `M return false W hasConfiguration() { no state set }`() { // When + Then - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isFalse() } @Test - fun `M return false W hasFlags() { empty flags map }`(forge: Forge) { + fun `M return true W hasConfiguration() { empty flags map }`(forge: Forge) { // Given testedRepository.setFlagsAndContext( EvaluationContext(forge.anAlphabeticalString(), emptyMap()), @@ -483,34 +806,35 @@ internal class DefaultFlagsRepositoryTest { ) // When + Then - assertThat(testedRepository.hasFlags()).isFalse() + assertThat(testedRepository.hasConfiguration()).isTrue() } @Test - fun `M return true W hasFlags() { single flag }`() { + fun `M return true W hasConfiguration() { single flag }`() { // Given testedRepository.setFlagsAndContext(testContext, singleFlagMap) // When + Then - assertThat(testedRepository.hasFlags()).isTrue() + assertThat(testedRepository.hasConfiguration()).isTrue() } @Test - fun `M return true W hasFlags() { multiple flags }`() { + fun `M return true W hasConfiguration() { multiple flags }`() { // Given testedRepository.setFlagsAndContext(testContext, multipleFlagsMap) // When + Then - assertThat(testedRepository.hasFlags()).isTrue() + assertThat(testedRepository.hasConfiguration()).isTrue() } @Test - fun `M return true W hasFlags() { persistence loads with non-empty flags }`() { + fun `M return true W hasConfiguration() { persistence loads with non-empty flags }`() { // Given val persistedEntry = FlagsStateEntry( flags = singleFlagMap, evaluationContext = testContext, - lastUpdateTimestamp = 0L + lastUpdateTimestamp = 0L, + flagSelection = null ) doAnswer { it.getArgument>(2) @@ -525,15 +849,16 @@ internal class DefaultFlagsRepositoryTest { val repository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "with-flags" ) // When + Then - assertThat(repository.hasFlags()).isTrue() + assertThat(repository.hasConfiguration()).isTrue() } @Test - fun `M return false W hasFlags() { persistence loads with no data }`() { + fun `M return false W hasConfiguration() { persistence loads with no data }`() { // Given doAnswer { it.getArgument>(2) @@ -548,15 +873,16 @@ internal class DefaultFlagsRepositoryTest { val repository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "no-data" ) // When + Then - assertThat(repository.hasFlags()).isFalse() + assertThat(repository.hasConfiguration()).isFalse() } @Test - fun `M return false W hasFlags() { persistence callback never fires within timeout }`() { + fun `M return false W hasConfiguration() { persistence callback never fires within timeout }`() { // Given doAnswer { // Never call the callback @@ -570,12 +896,13 @@ internal class DefaultFlagsRepositoryTest { val timeoutRepository = DefaultFlagsRepository( featureSdkCore = mockFeatureSdkCore, dataStore = mockDataStore, + flagSelection = null, instanceName = "timeout", persistenceLoadTimeoutMs = 1L ) // When - val result = timeoutRepository.hasFlags() + val result = timeoutRepository.hasConfiguration() // Then assertThat(result).isFalse() diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt index b09729cf71..1be385adc9 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/FirstFlagsInstallationTest.kt @@ -209,7 +209,7 @@ internal class FirstFlagsInstallationTest { assertThat(tested.getPrecomputedFlag("unknown")).isNull() assertThat(tested.getEvaluationContext()).isNull() assertThat(tested.getFlagsSnapshot()).isEmpty() - assertThat(tested.hasFlags()).isFalse() + assertThat(tested.hasConfiguration()).isFalse() assertThat(tested.getPrecomputedFlagWithContext("unknown")).isNull() } if (missing) disk.onSuccess(null) else disk.onFailure() @@ -366,7 +366,7 @@ internal class FirstFlagsInstallationTest { disk = it.getArgument(2) null }.whenever(store).value(any(), anyOrNull(), any(), any()) - return DefaultFlagsRepository(core, "first", store, persistenceLoadTimeoutMs = timeoutMs).also { + return DefaultFlagsRepository(core, "first", store, null, persistenceLoadTimeoutMs = timeoutMs).also { it.firstFlags.whenComplete { keys -> events.add(keys) } } } diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapperTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapperTest.kt index 82000ab113..a3c759545c 100644 --- a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapperTest.kt +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/repository/net/PrecomputeMapperTest.kt @@ -9,6 +9,7 @@ package com.datadog.android.flags.internal.repository.net import com.datadog.android.api.InternalLogger import com.datadog.android.flags.internal.model.VariationType import com.datadog.android.flags.utils.forge.ForgeConfigurator +import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.DoubleForgery @@ -71,6 +72,26 @@ internal class PrecomputeMapperTest { testedMapper = PrecomputeMapper(mockInternalLogger) } + @Test + fun `M warn once and preserve valid flags W map() {unsupported variation type}`() { + // Given + val unknown = buildFlagJson("future-type", true) + val valid = buildFlagJson(VariationType.BOOLEAN.value, true) + val json = buildValidJson(mapOf("unknown" to unknown, fakeFlagKey to valid)) + + // When + val result = testedMapper.map(json) + + // Then + assertThat(result?.flags?.keys).containsExactly(fakeFlagKey) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + "Ignored a flag with an unsupported variationType in the precomputed response.", + onlyOnce = true + ) + } + // region Valid JSON Parsing @ParameterizedTest diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt new file mode 100644 index 0000000000..5026efe905 --- /dev/null +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionCanonicalizerTest.kt @@ -0,0 +1,486 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.Locale + +internal class FlagSelectionCanonicalizerTest { + + @Test + fun `M return None W canonicalize() { no selection }`() { + // When / Then + assertThat(FlagSelectionCanonicalizer.canonicalize(null)).isEqualTo(FlagSelectionResult.None) + } + + @Test + fun `M return None W canonicalize() { lists empty after canonicalization }`() { + // Given + val selection = FlagSelection(tags = listOf("", " \t"), keyPatterns = listOf("\u3000")) + + // When / Then + assertThat(FlagSelectionCanonicalizer.canonicalize(selection)).isEqualTo(FlagSelectionResult.None) + } + + @Test + fun `M return None W canonicalize() { both lists null }`() { + // Given + val selection = FlagSelection(tags = null, keyPatterns = null) + + // When / Then + assertThat(FlagSelectionCanonicalizer.canonicalize(selection)).isEqualTo(FlagSelectionResult.None) + } + + @Test + fun `M treat a null list as empty W canonicalize() { one list null }`() { + // Given + val tagsOnly = FlagSelection(tags = listOf("mobile"), keyPatterns = null) + val keyPatternsOnly = FlagSelection(tags = null, keyPatterns = listOf("checkout-*")) + + // When + val tagsOnlyResult = FlagSelectionCanonicalizer.canonicalize(tagsOnly) + val keyPatternsOnlyResult = FlagSelectionCanonicalizer.canonicalize(keyPatternsOnly) + + // Then + assertThat(tagsOnlyResult).isEqualTo( + FlagSelectionResult.Selected(CanonicalFlagSelection(tags = listOf("mobile"), keyPatterns = emptyList())) + ) + assertThat(keyPatternsOnlyResult).isEqualTo( + FlagSelectionResult.Selected(CanonicalFlagSelection(tags = emptyList(), keyPatterns = listOf("checkout-*"))) + ) + } + + @Test + fun `M trim the Unicode White_Space set W canonicalize()`() { + // Given + val selection = FlagSelection( + tags = listOf("${UNICODE_WHITE_SPACE}mobile$UNICODE_WHITE_SPACE"), + keyPatterns = listOf("\uFEFFcheckout\uFEFF", "\u001Cpayment\u001C") + ) + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).isEqualTo( + FlagSelectionResult.Selected( + CanonicalFlagSelection( + tags = listOf("mobile"), + keyPatterns = listOf("\u001Cpayment\u001C", "\uFEFFcheckout\uFEFF") + ) + ) + ) + } + + @Test + fun `M keep interior Unicode White_Space W canonicalize()`() { + assertThat(UNICODE_WHITE_SPACE).hasSize(25) + UNICODE_WHITE_SPACE.forEach { whiteSpace -> + // Given + val selection = tags("${whiteSpace}a${whiteSpace}b$whiteSpace") + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result) + .describedAs("U+%04X", whiteSpace.code) + .isEqualTo(selected(listOf("a${whiteSpace}b"), emptyList())) + } + } + + @Test + fun `M drop empty values and dedupe after trimming W canonicalize()`() { + // Given + val selection = FlagSelection( + tags = listOf("web", " mobile", "", "mobile ", "web"), + keyPatterns = emptyList() + ) + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).isEqualTo( + FlagSelectionResult.Selected( + CanonicalFlagSelection(tags = listOf("mobile", "web"), keyPatterns = emptyList()) + ) + ) + } + + @Test + fun `M sort in UTF-8 byte order W canonicalize() { UTF-16 order differs }`() { + // Given + val fullwidthTilde = "\uFF5E" + val grinningFace = "\uD83D\uDE00" + val selection = FlagSelection( + tags = listOf(grinningFace, fullwidthTilde, "a"), + keyPatterns = listOf("b*", "B*", "\u00E9*") + ) + + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).isEqualTo( + FlagSelectionResult.Selected( + CanonicalFlagSelection( + tags = listOf("a", fullwidthTilde, grinningFace), + keyPatterns = listOf("B*", "b*", "\u00E9*") + ) + ) + ) + } + + @ParameterizedTest(name = "{0}") + @MethodSource("acceptedSelections") + fun `M keep the selection W canonicalize() { at a limit }`( + description: String, + selection: FlagSelection + ) { + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).describedAs(description).isInstanceOf(FlagSelectionResult.Selected::class.java) + } + + @ParameterizedTest(name = "{0}") + @MethodSource("droppedSelections") + fun `M drop the selection W canonicalize() { invalid selection }`( + description: String, + selection: FlagSelection, + expectedError: FlagSelectionError + ) { + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).describedAs(description).isEqualTo(FlagSelectionResult.Dropped(expectedError)) + } + + @ParameterizedTest(name = "{0}") + @MethodSource("sharedVectorSelections") + fun `M match the shared selection vector W canonicalize()`( + vectorName: String, + selection: FlagSelection, + expectedResult: FlagSelectionResult + ) { + // When + val result = FlagSelectionCanonicalizer.canonicalize(selection) + + // Then + assertThat(result).describedAs(vectorName).isEqualTo(expectedResult) + } + + companion object { + private const val GRINNING_FACE = "\uD83D\uDE00" + + // https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt lists 25 White_Space code points. + private const val UNICODE_WHITE_SPACE = "\t\n\u000B\u000C\r \u0085\u00A0\u1680" + + "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000" + + private fun numbered(prefix: String, count: Int) = List(count) { "$prefix%02d".format(Locale.US, it + 1) } + + private fun selected(tags: List, keyPatterns: List) = + FlagSelectionResult.Selected(CanonicalFlagSelection(tags, keyPatterns)) + + private fun values(count: Int) = List(count) { "value-$it" } + + // Java callers can pass a raw list, so the elements are not checked to be strings. + @Suppress("UNCHECKED_CAST") + private fun uncheckedStrings(vararg values: Any?) = values.toList() as List + + private fun tags(vararg tags: String?) = FlagSelection(tags = tags.toList(), keyPatterns = emptyList()) + + private fun keyPatterns(vararg keyPatterns: String?) = + FlagSelection(tags = emptyList(), keyPatterns = keyPatterns.toList()) + + @JvmStatic + fun acceptedSelections() = listOf( + Arguments.of("31 tags", FlagSelection(values(31), emptyList())), + Arguments.of("31 key patterns", FlagSelection(emptyList(), values(31))), + Arguments.of("32 tags with one duplicate", FlagSelection(values(31) + "value-0", emptyList())), + Arguments.of("200 one-byte characters", tags("a".repeat(200))), + Arguments.of("100 two-byte characters", tags("\u00E9".repeat(100))), + Arguments.of("66 three-byte characters and 2 bytes", tags("\u20AC".repeat(66) + "ab")), + Arguments.of("50 four-byte characters", keyPatterns(GRINNING_FACE.repeat(50))), + Arguments.of("200 bytes after trimming", keyPatterns(" ${"a".repeat(200)}\u3000")) + ) + + // Every case with an `sdk` expectation in DataDog/ffe-system-test-data@e8777633 + // flag-selection/selection-vectors.json, in file order and under the same name. An empty + // expected selection is no selection on Android. + @JvmStatic + fun sharedVectorSelections() = listOf( + Arguments.of( + "no parameters selects every flag", + FlagSelection(emptyList(), emptyList()), + FlagSelectionResult.None + ), + Arguments.of( + "tag selects flags having any of the tags", + FlagSelection(listOf("team:search", "mobile"), emptyList()), + selected(listOf("mobile", "team:search"), emptyList()) + ), + Arguments.of( + "tags and key patterns are combined with OR", + FlagSelection(listOf("mobile"), listOf("checkout.*")), + selected(listOf("mobile"), listOf("checkout.*")) + ), + Arguments.of( + "key pattern must match the whole key", + FlagSelection(emptyList(), listOf("checkout")), + selected(emptyList(), listOf("checkout")) + ), + Arguments.of( + "key pattern without a star selects exactly that key", + FlagSelection(emptyList(), listOf("checkout.new-flow")), + selected(emptyList(), listOf("checkout.new-flow")) + ), + Arguments.of( + "star matches an empty run and multiple stars are allowed", + FlagSelection(emptyList(), listOf("*search*auto*complete*")), + selected(emptyList(), listOf("*search*auto*complete*")) + ), + Arguments.of( + "leading star", + FlagSelection(emptyList(), listOf("*-v2")), + selected(emptyList(), listOf("*-v2")) + ), + Arguments.of( + "lone star matches every key", + FlagSelection(emptyList(), listOf("*")), + selected(emptyList(), listOf("*")) + ), + Arguments.of( + "question mark and brackets are literal", + FlagSelection(emptyList(), listOf("search.ranking-v?", "[a]*", "{a,b}")), + selected(emptyList(), listOf("[a]*", "search.ranking-v?", "{a,b}")) + ), + Arguments.of( + "braces are literal even when an alternative names a flag", + FlagSelection(emptyList(), listOf("{mobile.dark-mode,missing}")), + selected(emptyList(), listOf("{mobile.dark-mode,missing}")) + ), + Arguments.of( + "tags are never globs", + FlagSelection(listOf("team:*"), emptyList()), + selected(listOf("team:*"), emptyList()) + ), + Arguments.of( + "tags match case-sensitively", + FlagSelection(listOf("Team:Payments", "MOBILE"), emptyList()), + selected(listOf("MOBILE", "Team:Payments"), emptyList()) + ), + Arguments.of( + "key patterns match case-sensitively", + FlagSelection(emptyList(), listOf("CHECKOUT.*", "Mobile.Dark-Mode")), + selected(emptyList(), listOf("CHECKOUT.*", "Mobile.Dark-Mode")) + ), + Arguments.of( + "values are trimmed, empties dropped and duplicates removed", + FlagSelection(listOf(" mobile ", "mobile", "", " "), emptyList()), + selected(listOf("mobile"), emptyList()) + ), + Arguments.of( + "lists that are empty after normalization select every flag", + FlagSelection(listOf("", " "), listOf(" ")), + FlagSelectionResult.None + ), + Arguments.of( + "values are trimmed of Unicode white space", + FlagSelection(listOf("\u00A0mobile\u3000", "\u0085ui\u2028", "\u0009 team:search \u000A"), emptyList()), + selected(listOf("mobile", "team:search", "ui"), emptyList()) + ), + Arguments.of( + "characters outside Unicode White_Space are not trimmed", + FlagSelection(listOf("\uFEFFmobile", "mobile\u200B"), emptyList()), + selected(listOf("mobile\u200B", "\uFEFFmobile"), emptyList()) + ), + Arguments.of( + "values sort by UTF-8 bytes", + FlagSelection(listOf(GRINNING_FACE, "\uFF5E", "a", "Z"), emptyList()), + selected(listOf("Z", "a", "\uFF5E", GRINNING_FACE), emptyList()) + ), + Arguments.of( + "partial match serves only the matching flags", + FlagSelection(listOf("mobile", "missing"), listOf("nothing.*")), + selected(listOf("missing", "mobile"), listOf("nothing.*")) + ), + Arguments.of( + "31 values in one list are allowed", + FlagSelection(numbered("t", 31), emptyList()), + selected(numbered("t", 31), emptyList()) + ), + Arguments.of( + "32 values in one list are rejected", + FlagSelection(emptyList(), numbered("k", 32)), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), + Arguments.of( + "31 values in each list are allowed", + FlagSelection(numbered("t", 31), numbered("p", 31)), + selected(numbered("t", 31), numbered("p", 31)) + ), + Arguments.of( + "32 values in one list are rejected even when the other is within the limit", + FlagSelection(numbered("t", 31), numbered("p", 32)), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), + Arguments.of( + "duplicates do not count toward the limit", + FlagSelection(numbered("t", 31) + listOf("t01", " t02 "), emptyList()), + selected(numbered("t", 31), emptyList()) + ), + Arguments.of( + "key pattern of 200 bytes is allowed", + FlagSelection(emptyList(), listOf("*" + "x".repeat(199))), + selected(emptyList(), listOf("*" + "x".repeat(199))) + ), + Arguments.of( + "key pattern of 201 bytes is invalid", + FlagSelection(emptyList(), listOf("*" + "x".repeat(200))), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) + ), + Arguments.of( + "key pattern length is measured after trimming", + FlagSelection(emptyList(), listOf(" *" + "x".repeat(199) + " ")), + selected(emptyList(), listOf("*" + "x".repeat(199))) + ), + Arguments.of( + "key pattern length counts UTF-8 bytes: 100 two-byte characters are allowed", + FlagSelection(emptyList(), listOf("\u00E9".repeat(100))), + selected(emptyList(), listOf("\u00E9".repeat(100))) + ), + Arguments.of( + "key pattern length counts UTF-8 bytes: 201 bytes are invalid", + FlagSelection(emptyList(), listOf("x" + "\u00E9".repeat(100))), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) + ), + Arguments.of( + "tag of 200 bytes is allowed", + FlagSelection(listOf("a".repeat(200)), emptyList()), + selected(listOf("a".repeat(200)), emptyList()) + ), + Arguments.of( + "tag of 201 bytes is invalid", + FlagSelection(listOf("a".repeat(201)), emptyList()), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_TAG) + ), + Arguments.of( + "tag length is measured after trimming", + FlagSelection(listOf(" " + "a".repeat(200) + " "), emptyList()), + selected(listOf("a".repeat(200)), emptyList()) + ), + Arguments.of( + "tag length counts UTF-8 bytes: 100 two-byte characters are allowed", + FlagSelection(listOf("\u00E9".repeat(100)), emptyList()), + selected(listOf("\u00E9".repeat(100)), emptyList()) + ), + Arguments.of( + "tag length counts UTF-8 bytes: 201 bytes are invalid", + FlagSelection(listOf("x" + "\u00E9".repeat(100)), emptyList()), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_TAG) + ), + Arguments.of( + "a long tag invalidates the selection even when other tags match", + FlagSelection(listOf("mobile", "a".repeat(201)), emptyList()), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_TAG) + ), + Arguments.of( + "too_many_values takes precedence over invalid_key_pattern", + FlagSelection(numbered("t", 32), listOf("*" + "x".repeat(200))), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), + Arguments.of( + "too_many_values takes precedence over invalid_tag", + FlagSelection(numbered("t", 31) + "a".repeat(201), emptyList()), + FlagSelectionResult.Dropped(FlagSelectionError.TOO_MANY_VALUES) + ), + Arguments.of( + "invalid_key_pattern takes precedence over invalid_tag", + FlagSelection(listOf("a".repeat(201)), listOf("a".repeat(201))), + FlagSelectionResult.Dropped(FlagSelectionError.INVALID_KEY_PATTERN) + ) + ) + + @JvmStatic + fun droppedSelections() = listOf( + Arguments.of("32 tags", FlagSelection(values(32), emptyList()), FlagSelectionError.TOO_MANY_VALUES), + Arguments.of( + "32 key patterns", + FlagSelection(emptyList(), values(32)), + FlagSelectionError.TOO_MANY_VALUES + ), + Arguments.of("201-byte tag", tags("a".repeat(201)), FlagSelectionError.INVALID_TAG), + Arguments.of( + "201-byte tag with two-byte characters", + tags("\u00E9".repeat(100) + "a"), + FlagSelectionError.INVALID_TAG + ), + Arguments.of( + "201-byte tag with three-byte characters", + tags("\u20AC".repeat(67)), + FlagSelectionError.INVALID_TAG + ), + Arguments.of( + "201-byte key pattern with four-byte characters", + keyPatterns(GRINNING_FACE.repeat(50) + "*"), + FlagSelectionError.INVALID_KEY_PATTERN + ), + Arguments.of("lone high surrogate", tags("a\uD83D"), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of("lone low surrogate", keyPatterns("\uDE00a"), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of( + "reversed surrogate pair", + tags("\uDE00\uD83D"), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of("null tag", tags("mobile", null), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of("null key pattern", keyPatterns(null), FlagSelectionError.MALFORMED_SELECTION), + Arguments.of( + "non-string tag from an unchecked cast", + FlagSelection(tags = uncheckedStrings("mobile", 1), keyPatterns = emptyList()), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "non-string key pattern from an unchecked cast", + FlagSelection(tags = emptyList(), keyPatterns = uncheckedStrings(true)), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "malformed before too many values", + FlagSelection(values(32) + null, emptyList()), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "malformed before invalid key pattern", + FlagSelection(listOf("a\uD83D"), listOf("a".repeat(201))), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "malformed before invalid tag", + FlagSelection(listOf("a".repeat(201)), listOf(null)), + FlagSelectionError.MALFORMED_SELECTION + ), + Arguments.of( + "too many values before invalid key pattern", + FlagSelection(values(32), listOf("a".repeat(201))), + FlagSelectionError.TOO_MANY_VALUES + ), + Arguments.of( + "invalid key pattern before invalid tag", + FlagSelection(listOf("a".repeat(201)), listOf("a".repeat(201))), + FlagSelectionError.INVALID_KEY_PATTERN + ) + ) + } +} diff --git a/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt new file mode 100644 index 0000000000..4a51f18fbf --- /dev/null +++ b/features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/selection/FlagSelectionIgnoredReasonTest.kt @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.internal.selection + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource + +internal class FlagSelectionIgnoredReasonTest { + + @ParameterizedTest + @ValueSource( + strings = ["malformed_selection", "too_many_values", "invalid_key_pattern", "invalid_tag", "tag_load_failed"] + ) + fun `M keep the reason W sanitize() { known reason }`(reason: String) { + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason)).isEqualTo(reason) + } + + @ParameterizedTest + @EnumSource(ServerIgnoredReason::class) + fun `M keep the reason W sanitize() { any server ignored reason }`(reason: ServerIgnoredReason) { + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason.reason)).isEqualTo(reason.reason) + } + + @ParameterizedTest + @ValueSource( + strings = [ + "Some Future-Reason", + "unknown_reason", + "Too_Many_Values", + "TAG_LOAD_FAILED", + " too_many_values", + "too_many_values\u0001", + "invalid_tag\n", + "too_many_values\u00E9", + "\uD83D\uDE00" + ] + ) + fun `M return the fallback W sanitize() { unknown reason }`(reason: String) { + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason)).isEqualTo("unknown") + } + + @ParameterizedTest + @ValueSource(ints = [33, 200, 10_000]) + fun `M return the fallback W sanitize() { value longer than 32 characters }`(length: Int) { + // Given + val reason = "too_many_values".padEnd(length, '_') + + // When / Then + assertThat(FlagSelectionIgnoredReason.sanitize(reason)).isEqualTo("unknown") + } +} diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt index 91f4f4f312..3bb9d21992 100644 --- a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/CombinedLogHandlerTest.kt @@ -6,8 +6,8 @@ package com.datadog.android.log.internal.logger -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt index 7b46450f89..36b8d5a3e2 100644 --- a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/logger/ConditionalLogHandlerTest.kt @@ -6,8 +6,8 @@ package com.datadog.android.log.internal.logger -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.forge.Configurator +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery diff --git a/features/dd-sdk-android-rum/build.gradle.kts b/features/dd-sdk-android-rum/build.gradle.kts index e86fe5e4b8..346a852fed 100644 --- a/features/dd-sdk-android-rum/build.gradle.kts +++ b/features/dd-sdk-android-rum/build.gradle.kts @@ -95,14 +95,6 @@ dependencies { // Test Fixtures testFixturesImplementation(testFixtures(project(":dd-sdk-android-core"))) testFixturesImplementation(testFixtures(project(":dd-sdk-android-internal"))) - testFixturesImplementation(project(":tools:unit")) { - attributes { - attribute( - com.android.build.api.attributes.ProductFlavorAttr.of("platform"), - objects.named("jvm") - ) - } - } testFixturesImplementation(libs.kotlinRuntime) testFixturesImplementation(libs.bundles.jUnit5) testFixturesImplementation(libs.okHttp) diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/config/DatadogSingletonTestConfiguration.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/config/DatadogSingletonTestConfiguration.kt similarity index 72% rename from dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/config/DatadogSingletonTestConfiguration.kt rename to features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/config/DatadogSingletonTestConfiguration.kt index 7caa50ac0c..7e5ab87204 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/config/DatadogSingletonTestConfiguration.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/config/DatadogSingletonTestConfiguration.kt @@ -4,20 +4,22 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.tests.config +package com.datadog.android.rum.utils.config import com.datadog.android.Datadog import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance import com.datadog.tools.unit.extensions.config.MockTestConfiguration import fr.xgouchet.elmyr.Forge -class DatadogSingletonTestConfiguration : +internal class DatadogSingletonTestConfiguration : MockTestConfiguration(InternalSdkCore::class.java) { override fun setUp(forge: Forge) { super.setUp(forge) - Datadog.registry.register(null, mockInstance) + Datadog.registerInstance(null, mockInstance) } override fun tearDown(forge: Forge) { @@ -26,6 +28,6 @@ class DatadogSingletonTestConfiguration : } fun clearRegistry() { - Datadog.registry.clear() + Datadog.clearInstances() } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt index fc3cbdc953..b3b345efe6 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/TelemetryUsageEventForgeryFactory.kt @@ -7,7 +7,7 @@ package com.datadog.android.rum.utils.forge import com.datadog.android.telemetry.model.TelemetryUsageEvent -import com.datadog.android.tests.elmyr.exhaustiveAttributes +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt index e7cd96139e..0ae02ffcce 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/sqlite/DatadogDatabaseErrorHandlerTest.kt @@ -13,8 +13,8 @@ import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumMonitor +import com.datadog.android.rum.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.utils.forge.Configurator -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt index 641cdacac5..2bf094bce2 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ActionEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.ActionEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt index d9d6eb1f34..4e45eccdcd 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt @@ -6,10 +6,10 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.aThrowable +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.rum.model.ErrorEvent -import com.datadog.tools.unit.forge.aThrowable -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt index c2299a4bdf..b76258f5b3 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/LongTaskEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.LongTaskEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt index 68ac7e11cc..96cd423f1a 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ResourceEventForgeryFactory.kt @@ -6,6 +6,7 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.domain.scope.connect import com.datadog.android.rum.internal.domain.scope.dns @@ -13,7 +14,6 @@ import com.datadog.android.rum.internal.domain.scope.download import com.datadog.android.rum.internal.domain.scope.firstByte import com.datadog.android.rum.internal.domain.scope.ssl import com.datadog.android.rum.model.ResourceEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt index ec3df2dd51..4cc12bab6c 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewEventForgeryFactory.kt @@ -6,9 +6,9 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.ViewEvent import com.datadog.android.rum.model.ViewEvent.Accessibility -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt index 4032af598e..1ccf486d6a 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/ViewUpdateEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.ViewUpdateEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt index 65e397c199..e0a4eb16e8 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalAppLaunchEventForgeryFactory.kt @@ -6,8 +6,8 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.VitalAppLaunchEvent -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt index 322eac6cc1..367825fbd4 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/utils/forge/VitalOperationStepEventForgeryFactory.kt @@ -6,10 +6,10 @@ package com.datadog.android.rum.utils.forge +import com.datadog.android.internal.tests.elmyr.exhaustiveAttributes import com.datadog.android.rum.model.VitalOperationStepEvent import com.datadog.android.rum.model.VitalOperationStepEvent.FailureReason import com.datadog.android.rum.model.VitalOperationStepEvent.StepType -import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory import fr.xgouchet.elmyr.jvm.ext.aTimestamp diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt index 74a2f8fdca..77813346d6 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/CronetIntegrationPluginTest.kt @@ -8,9 +8,9 @@ package com.datadog.android.cronet import com.datadog.android.api.InternalLogger import com.datadog.android.cronet.internal.DatadogCronetEngine +import com.datadog.android.cronet.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.ExperimentalRumApi import com.datadog.android.rum.configuration.RumNetworkInstrumentationConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.aHostName import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration import com.datadog.android.trace.ApmNetworkTracingScope diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/tests/config/DatadogSingletonTestConfiguration.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/tests/config/DatadogSingletonTestConfiguration.kt new file mode 100644 index 0000000000..0c2267cfcf --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/tests/config/DatadogSingletonTestConfiguration.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.cronet.tests.config + +import com.datadog.android.Datadog +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import fr.xgouchet.elmyr.Forge + +internal class DatadogSingletonTestConfiguration : + MockTestConfiguration(InternalSdkCore::class.java) { + + override fun setUp(forge: Forge) { + super.setUp(forge) + + Datadog.registerInstance(null, mockInstance) + } + + override fun tearDown(forge: Forge) { + clearRegistry() + super.tearDown(forge) + } + + fun clearRegistry() { + Datadog.clearInstances() + } +} diff --git a/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt index a042bcddf1..fb3f05f80b 100644 --- a/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt +++ b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/DatadogRUMUncaughtThrowableStrategyTest.kt @@ -6,10 +6,10 @@ package com.datadog.android.glide +import com.datadog.android.glide.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumMonitor -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/tests/config/DatadogSingletonTestConfiguration.kt b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/tests/config/DatadogSingletonTestConfiguration.kt new file mode 100644 index 0000000000..084667135a --- /dev/null +++ b/integrations/dd-sdk-android-glide/src/test/kotlin/com/datadog/android/glide/tests/config/DatadogSingletonTestConfiguration.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.glide.tests.config + +import com.datadog.android.Datadog +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import fr.xgouchet.elmyr.Forge + +internal class DatadogSingletonTestConfiguration : + MockTestConfiguration(InternalSdkCore::class.java) { + + override fun setUp(forge: Forge) { + super.setUp(forge) + + Datadog.registerInstance(null, mockInstance) + } + + override fun tearDown(forge: Forge) { + clearRegistry() + super.tearDown(forge) + } + + fun clearRegistry() { + Datadog.clearInstances() + } +} diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt index b19a813f27..5967cfc633 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogEventListenerFactoryTest.kt @@ -7,7 +7,7 @@ package com.datadog.android.okhttp import com.datadog.android.okhttp.internal.buildResourceId -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt index d33e7047cf..164e0c71f4 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt @@ -19,13 +19,13 @@ import com.datadog.android.okhttp.trace.newSpanBuilderMock import com.datadog.android.okhttp.trace.newSpanContextMock import com.datadog.android.okhttp.trace.newSpanMock import com.datadog.android.okhttp.trace.newTracerMock +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceAttributesProvider import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.resource.ResourceId -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.api.propagation.DatadogPropagation diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt index 67cb29ae76..b114a8fd84 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/OkHttpIntegrationPluginTest.kt @@ -10,9 +10,9 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.okhttp.internal.ApmInstrumentationOkHttpAdapter import com.datadog.android.okhttp.internal.RegistryTrackingEventListener import com.datadog.android.okhttp.internal.RumInstrumentationOkHttpAdapter +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.rum.ExperimentalRumApi import com.datadog.android.rum.configuration.RumNetworkInstrumentationConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration import com.datadog.android.trace.ApmNetworkTracingScope import com.datadog.android.trace.ExperimentalTraceApi diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt index 863f443f08..78cb2795dc 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpRequestInfoTest.kt @@ -10,10 +10,10 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.instrumentation.network.HttpBodySnapshot import com.datadog.android.okhttp.internal.OkHttpRequestInfo.Companion.ERROR_PEEK_REQUEST_BODY import com.datadog.android.tests.elmyr.URL_FORGERY_PATTERN -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.verifyLog import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.forge.anHttpHeaderMap import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.annotation.StringForgery @@ -83,7 +83,7 @@ internal class OkHttpRequestInfoTest { @Test fun `M delegate W headers property`(forge: Forge) { // Given - val fakeHeaders = forge.exhaustiveAttributes().mapValues { listOf(it.value.toString()) } + val fakeHeaders = forge.anHttpHeaderMap().mapValues { listOf(it.value) } val mockHeaders = mock { on { toMultimap() } doReturn fakeHeaders } val mockRequest = mock { on { headers } doReturn mockHeaders } diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt index c6690dd534..6f3fc08802 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt @@ -10,10 +10,10 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.instrumentation.network.HttpBodySnapshot import com.datadog.android.internal.network.HttpSpec import com.datadog.android.okhttp.internal.OkHttpResponseInfo.Companion.ERROR_PEEK_BODY -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.android.utils.verifyLog import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.forge.anHttpHeaderMap import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery @@ -88,7 +88,7 @@ internal class OkHttpResponseInfoTest { @Test fun `M delegate W headers property`(forge: Forge) { // Given - val fakeHeaders = forge.exhaustiveAttributes().mapValues { listOf(it.value.toString()) } + val fakeHeaders = forge.anHttpHeaderMap().mapValues { listOf(it.value) } val mockHeaders = mock { on { toMultimap() } doReturn fakeHeaders } val mockResponse = mock { on { headers } doReturn mockHeaders } diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt index 29fcfbd32a..512028ba6b 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/RumResourceAttributesProviderCompatibilityAdapterTest.kt @@ -12,10 +12,10 @@ import com.datadog.android.api.instrumentation.network.HttpRequestInfo import com.datadog.android.api.instrumentation.network.HttpResponseInfo import com.datadog.android.core.SdkReference import com.datadog.android.rum.RumResourceAttributesProvider -import com.datadog.android.tests.elmyr.exhaustiveAttributes import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.forge.BaseConfigurator import com.datadog.tools.unit.forge.aThrowable +import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt index ce289f682e..77f1244528 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt @@ -13,8 +13,8 @@ import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.network.HttpSpec import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.internal.utils.forge.OkHttpConfigurator +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.DeterministicTraceSampler import com.datadog.android.trace.TraceContextInjection diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt index df4c46c6da..82fe36ba9b 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt @@ -13,7 +13,7 @@ import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.network.HttpSpec import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt index f73b248443..6ecb379623 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt @@ -13,7 +13,7 @@ import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.network.HttpSpec import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.DeterministicTraceSampler import com.datadog.android.trace.TraceContextInjection diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt index 6e9a294291..70f7ed83d3 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt @@ -12,9 +12,9 @@ import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeReso import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.RumResourceMethod -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt index 2a3f712d6d..f357289b70 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt @@ -20,8 +20,8 @@ import com.datadog.android.okhttp.internal.utils.forge.OkHttpConfigurator import com.datadog.android.okhttp.trace.TracingInterceptor.Companion.OKHTTP_INTERCEPTOR_HEADER_TYPES import com.datadog.android.okhttp.trace.TracingInterceptor.Companion.OKHTTP_INTERCEPTOR_SAMPLE_RATE import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat +import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.android.tests.elmyr.anOkHttpResponse import com.datadog.android.trace.DeterministicTraceSampler import com.datadog.android.trace.TraceContextInjection diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/DatadogSingletonTestConfiguration.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/DatadogSingletonTestConfiguration.kt new file mode 100644 index 0000000000..97ee2fa2bd --- /dev/null +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/DatadogSingletonTestConfiguration.kt @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.okhttp.utils.config + +import com.datadog.android.Datadog +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.utils.clearInstances +import com.datadog.android.utils.registerInstance +import com.datadog.tools.unit.extensions.config.MockTestConfiguration +import fr.xgouchet.elmyr.Forge + +internal class DatadogSingletonTestConfiguration : + MockTestConfiguration(InternalSdkCore::class.java) { + + override fun setUp(forge: Forge) { + super.setUp(forge) + + Datadog.registerInstance(null, mockInstance) + } + + override fun tearDown(forge: Forge) { + clearRegistry() + super.tearDown(forge) + } + + fun clearRegistry() { + Datadog.clearInstances() + } +} diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt index fa11cf657f..809e220eb1 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/utils/config/GlobalRumMonitorTestConfiguration.kt @@ -10,7 +10,6 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor -import com.datadog.android.tests.config.DatadogSingletonTestConfiguration import com.datadog.tools.unit.extensions.config.MockTestConfiguration import fr.xgouchet.elmyr.Forge import org.mockito.kotlin.mock diff --git a/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/CanonicalPrecomputedTest.kt b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/CanonicalPrecomputedTest.kt new file mode 100644 index 0000000000..539df78c33 --- /dev/null +++ b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/CanonicalPrecomputedTest.kt @@ -0,0 +1,191 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.flags.integration + +import com.datadog.android.api.feature.Feature +import com.datadog.android.flags.Flags +import com.datadog.android.flags.FlagsClient +import com.datadog.android.flags.FlagsConfiguration +import com.datadog.android.flags.integration.FlagsClientTest.Companion.createStubSdkCore +import com.datadog.android.flags.integration.tests.elmyr.FlagsIntegrationForgeConfigurator +import com.datadog.android.flags.integration.tests.utils.httpResponse +import com.datadog.android.flags.model.EvaluationContext +import com.datadog.android.flags.model.FlagsClientState +import com.datadog.android.flags.model.ResolutionReason +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@ExtendWith(MockitoExtension::class, ForgeExtension::class) +@ForgeConfiguration(FlagsIntegrationForgeConfigurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class CanonicalPrecomputedTest { + @ParameterizedTest(name = "{0}, restored={1}") + @MethodSource("fixtures") + fun `M satisfy canonical contract W setEvaluationContext() and resolve()`( + path: String, + restored: Boolean, + forge: Forge + ) { + // Given + val fixture = read(path).asJsonObject + assumeTrue(applies(fixture), "Canonical platform exclusion: $path") + val context = fixture.getAsJsonObject("context") + val targetingKey = context["targetingKey"] + require(targetingKey != null && targetingKey.isJsonPrimitive && targetingKey.asJsonPrimitive.isString) { + "$path: Android requires a string targetingKey; null-context fixtures must exclude android upstream" + } + var core = createStubSdkCore(forge) + val store = core.stubDataStore(Feature.FLAGS_FEATURE_NAME) + core.stubNetworkResponses { httpResponse(it, body = fixture["response"].toString()) } + Flags.enable( + FlagsConfiguration.Builder().useCustomFlagEndpoint("https://fixture.test/precompute-assignments").build(), + core + ) + var client = FlagsClient.Builder(sdkCore = core).build() + val attributes = context.getAsJsonObject("attributes").entrySet().associate { it.key to it.value.asString } + + // When + try { + client.setEvaluationContext(EvaluationContext(targetingKey.asString, attributes)) + + // Then + assertThat(core.requestsSent()).hasSize(1) + if (fixture.has("expectedResponseError")) { + assertThat(fixture["expectedResponseError"].asString).isEqualTo("INVALID_RESPONSE") + assertThat(client.state.getCurrentState()).isInstanceOf(FlagsClientState.Error::class.java) + } else { + assertThat(client.state.getCurrentState()).isEqualTo(FlagsClientState.Ready) + if (restored) { + core.stopFeatures() + core = createStubSdkCore(forge) + core.stubDataStore(Feature.FLAGS_FEATURE_NAME, store) + core.stubNetworkResponses { null } + Flags.enable(FlagsConfiguration.Builder().build(), core) + client = FlagsClient.Builder(sdkCore = core).build() + assertThat(core.requestsSent()).isEmpty() + } + fixture.getAsJsonArray("evaluations").map { it.asJsonObject }.filter(::applies).forEach { evaluation -> + val key = evaluation["flag"].asString + val fallback = evaluation["defaultValue"] + val result = when (evaluation["variationType"].asString) { + "boolean" -> client.resolve(key, fallback.asBoolean) + "string" -> client.resolve(key, fallback.asString) + "integer" -> client.resolve(key, fallback.asInt) + "float" -> client.resolve(key, fallback.asDouble) + "object" -> client.resolve(key, Gson().fromJson(fallback, Map::class.java)) + else -> error("Unsupported canonical getter: $evaluation") + } + if (restored && result.errorCode == null) { + assertThat(result.reason).isEqualTo(ResolutionReason.CACHED) + } + val actual = JsonObject().apply { + add("value", Gson().toJsonTree(result.value)) + addProperty("reason", if (result.errorCode != null) "ERROR" else result.reason.toString()) + addProperty("errorCode", result.errorCode?.toString()) + addProperty("variantKey", result.variant) + } + evaluation.getAsJsonObject("expectedResult").entrySet().forEach { (field, expected) -> + if (restored && field == "reason" && result.errorCode == null) return@forEach + assertThat(actual[field]).describedAs("$path: $key: $field").isEqualTo(expected) + } + } + } + core.stopFeatures() + val expectations = fixture.getAsJsonObject("expectations") + assertEvents( + core.eventsWritten(Feature.FLAGS_FEATURE_NAME).map { + JsonParser.parseString(it.eventData) + }, + expectations, + "exposures" + ) + assertEvents( + core.eventsWritten(Feature.FLAGS_EVALUATIONS_FEATURE_NAME).map { + JsonParser.parseString(it.eventData) + }, + expectations, + "evaluationEvents" + ) + } finally { + core.stopFeatures() + } + } + + private fun assertEvents(events: List, expectations: JsonObject, field: String) { + val remaining = events.toMutableList() + expectations.getAsJsonArray(field)?.map { it.asJsonObject }?.filter(::applies)?.forEach { matcher -> + val fields = JsonObject().apply { + matcher.entrySet().filterNot { it.key.startsWith("_") }.forEach { add(it.key, it.value) } + } + repeat(matcher["_count"].asInt) { + val index = remaining.indexOfFirst { matches(it, fields) } + assertThat( + index + ).describedAs("Missing event for $matcher; remaining $remaining").isGreaterThanOrEqualTo(0) + remaining.removeAt(index) + } + } + if (expectations["noUnmatchedEvents"]?.asBoolean == true) { + assertThat(remaining).describedAs("Unmatched $field").isEmpty() + } + } + + private fun matches(actual: JsonElement?, expected: JsonElement): Boolean = when { + expected.isJsonNull -> actual == null || actual.isJsonNull + expected.isJsonObject -> actual?.isJsonObject == true && expected.asJsonObject.entrySet().all { + matches(actual.asJsonObject[it.key], it.value) + } + else -> actual == expected + } + + companion object { + private fun read(path: String): JsonElement = JsonParser.parseString( + checkNotNull(CanonicalPrecomputedTest::class.java.getResource("/precomputed/$path")) { + "Missing fixture: $path" + }.readText() + ) + + private fun applies(value: JsonObject): Boolean { + fun contains(field: String) = + value.getAsJsonArray(field)?.any { it.asJsonObject["platform"].asString == "android" } == true + return !contains("_skip") && (!value.has("_include") || contains("_include")) + } + + @JvmStatic + fun fixtures(): List { + val paths = read("index.json").asJsonArray.map { it.asString } + check(paths.isNotEmpty()) { "Empty canonical fixture corpus" } + check(paths.distinct().size == paths.size) { "Duplicate fixture index entries" } + check(paths.any { applies(read(it).asJsonObject) }) { "All canonical fixtures excluded" } + return paths.flatMap { path -> + val fixture = read(path).asJsonObject + val modes = if (applies(fixture) && + !fixture.has("expectedResponseError") + ) { + listOf(false, true) + } else { + listOf(false) + } + modes.map { Arguments.of(path, it) } + } + } + } +} diff --git a/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt index 5af4a80210..6c62c70e5b 100644 --- a/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt +++ b/reliability/single-fit/flags/src/test/kotlin/com/datadog/android/flags/integration/FlagsEvaluationTest.kt @@ -30,6 +30,7 @@ import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat +import org.json.JSONArray import org.json.JSONObject import org.junit.jupiter.api.AutoClose import org.junit.jupiter.api.BeforeEach @@ -150,6 +151,51 @@ class FlagsEvaluationTest { .containsEntry(fakeStructureKey, fakeStructureValue) } + @Test + fun `M ignore dependent fields W resolve() {legacy client}`( + @BoolForgery fakeValue: Boolean + ) { + // Given + val response = JSONObject( + precomputedAssignmentsBody(mapOf(fakeFlagKey to assignment("boolean", fakeValue))) + ) + val attributes = response.getJSONObject("data").getJSONObject("attributes") + val dependencyEvaluation = JSONObject() + .put("flagKey", ENCRYPTED_DEPENDENCY_KEY) + .put("isPresentInAssignment", true) + attributes.getJSONObject("flags").getJSONObject(fakeFlagKey) + .put("dependencyEvaluations", JSONArray().put(dependencyEvaluation)) + attributes.put( + "flagEvaluationErrors", + JSONObject().put( + "failed-flag", + JSONObject() + .put("reason", "ERROR") + .put("errorCode", "GENERAL") + .put("dependencyEvaluations", JSONArray().put(dependencyEvaluation)) + ) + ) + stubSdkCore.stubNetworkResponses { httpResponse(it, body = response.toString()) } + val client = enableFlagsAndBuildClient() + client.setEvaluationContext(EvaluationContext(fakeTargetingKey)) + + // When + val details = client.resolve(fakeFlagKey, !fakeValue) + stubSdkCore.stopFeatures() + + // Then + assertThat(details.value).isEqualTo(fakeValue) + assertThat(details.errorCode).isNull() + val exposureEvents = stubSdkCore.eventsWritten(Feature.FLAGS_FEATURE_NAME) + val evaluationEvents = stubSdkCore.eventsWritten(Feature.FLAGS_EVALUATIONS_FEATURE_NAME) + assertThat(exposureEvents).hasSize(1) + assertThat(evaluationEvents).hasSize(1) + assertThat(JSONObject(exposureEvents.single().eventData).getJSONObject("flag").getString("key")) + .isEqualTo(fakeFlagKey) + assertThat(JSONObject(evaluationEvents.single().eventData).getJSONObject("flag").getString("key")) + .isEqualTo(fakeFlagKey) + } + @Test fun `M provide resolution details W resolve()`( @BoolForgery fakeValue: Boolean, @@ -480,5 +526,9 @@ class FlagsEvaluationTest { ) } + private companion object { + const val ENCRYPTED_DEPENDENCY_KEY = "Eb_cST-0-FZKdvcpKLHbDDG_CX2hK4Xfrw7h5o-47Gp6GyFntOGTZO3wmLg" + } + // endregion } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/LICENSE b/reliability/single-fit/flags/src/test/resources/precomputed/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/NOTICE b/reliability/single-fit/flags/src/test/resources/precomputed/NOTICE new file mode 100644 index 0000000000..cec0e71a28 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/NOTICE @@ -0,0 +1,4 @@ +Datadog ffe-system-test-data +Copyright 2026-Present Datadog, Inc. + +This product includes software developed at Datadog (https://www.datadoghq.com/). diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/README.md b/reliability/single-fit/flags/src/test/resources/precomputed/README.md new file mode 100644 index 0000000000..1bf7d8c89f --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/README.md @@ -0,0 +1,309 @@ +# Precomputed Assignment Fixtures (`precomputed-assignments/`) + +These fixtures cover the Datadog precompute assignment API consumed by mobile +and browser SDKs which do not run the evaluator locally. + +## Directory structure + +``` +precomputed-assignments/ + cases/ + evaluator/ # Core value resolution, types, defaults, errors + emissions/ # Exposure and evaluation event behavior + parsing/ # Malformed data handling, isolation (future) + README.md +``` + +Test runners should recursively discover `*.json` files under `cases/`. + +## Fixture structure + +Each fixture file is a single JSON object: + +```json +{ + "name": "fixture-name", + "description": "What this fixture tests.", + "context": { + "targetingKey": "user-id", + "attributes": {} + }, + "response": { ... }, + "evaluations": [ ... ], + "expectations": { ... } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | yes | Fixture identifier; must match filename stem | +| `description` | string | yes | What the fixture tests | +| `context` | object | yes | `targetingKey` and `attributes` for the evaluation context | +| `response` | object | yes | Mocked precomputed-assignments response (open-ended) | +| `evaluations` | array | yes | Typed flag evaluations to run | +| `expectations` | object | yes | Event matchers (see below) | +| `_skip` | array | no | Skip entire fixture for listed platforms | +| `_include` | array | no | Run fixture only for listed platforms | + +## Evaluations + +Each evaluation calls a typed getter on the SDK and asserts the result: + +```json +{ + "flag": "flag-key", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } +} +``` + +`defaultValue` and `expectedResult.value` must use the JSON type declared by +`variationType`: boolean, string, integer, number for `float`, or object. +`float` accepts any JSON number; `integer` accepts only integral JSON numbers +(including `1.0`). Arrays and null are not valid for either field. + +Evaluations may carry `_skip` or `_include` to control per-platform applicability. + +## Expectations and event matchers + +The `expectations` block uses ordered, exclusive matchers to assert on emitted +exposure and evaluation events. + +```json +"expectations": { + "noUnmatchedEvents": true, + "exposures": [ + {"_count": 1, "flag": "my-flag"} + ], + "evaluationEvents": [ + {"_count": 1, "evaluation_count": 2} + ] +} +``` + +### Matcher semantics + +Matchers are processed **in order**. Each received event is consumed by the +first matcher whose non-`_` fields are a subset of the event. Once consumed, +the event is unavailable to later matchers. + +- `_count` *(required)*: how many events this matcher should consume. +- All other non-`_` keys are match fields compared against event data. +- A match field set to `null` asserts the field is either absent or null on + the actual event. +- A matcher with only `_count` (no match fields) matches any event. + +### `noUnmatchedEvents` + +When `true`, the test runner asserts that all received events (both exposures +and evaluations) are consumed by matchers. No extra events allowed. Applies to +both event types. + +### Control fields + +All fields prefixed with `_` are control fields, not match fields: + +| Field | Description | +|-------|-------------| +| `_count` | Number of events to consume (required on matchers) | +| `_skip` | Skip this matcher/evaluation/fixture for listed platforms | +| `_include` | Apply this matcher/evaluation/fixture only for listed platforms | + +Test runners in dynamic languages should filter keys starting with `_` to +separate control from match fields. Typed runners can use the known set. + +## `_skip` and `_include` + +Platform filters are always an array of `{platform, reason?}` objects, +regardless of where they appear (case, evaluation, or matcher level). + +```json +"_skip": [ + {"platform": "web", "reason": "Browser SDK aggregates differently."} +] + +"_include": [ + {"platform": "web"} +] +``` + +- **Default** (neither present): applies to all platforms. +- `_skip`: applies to all platforms except listed. +- `_include`: applies only to listed platforms. +- **Mutually exclusive**: specifying both is invalid. +- `reason` is optional. + +Valid platform identifiers: `web`, `ios`, `android`, `react-native`, `flutter`, +`maui`, `kotlin-multiplatform`, `electron`, `unity`. + +## How to implement a test runner + +1. Recursively discover `*.json` files under `precomputed-assignments/cases/`. +2. For each fixture, check `_skip`/`_include` at case level. Skip if the + current platform is excluded. +3. Initialize the SDK with `context`. Preserve every numeric literal in `response` + exactly as written in the fixture. Feed `response` as the mocked server + payload. +4. Walk `evaluations` in order. For each evaluation not skipped for the current + platform, call the typed getter (e.g., `getBooleanValue`) with `flag`, + `defaultValue`. Assert against `expectedResult`. +5. Flush the SDK. +6. Collect emitted exposure and evaluation events. +7. Walk `expectations.exposures` matchers in order. For each applicable matcher + (not skipped for the current platform), greedily consume up to `_count` + events whose fields are a superset of the matcher's non-`_` fields. Fail if + fewer than `_count` matched. +8. Repeat step 7 for `expectations.evaluationEvents`. +9. If `noUnmatchedEvents` is `true`, fail if any unconsumed events remain. + +## Field naming + +- All schema field names use **camelCase** (`expectedResult`, `variationType`). +- Snake_case in matchers is only for wire-format fields that match emitted + event schemas (e.g., `evaluation_count`, `serial_id`). +- Fixture filenames use **kebab-case**. + +## Schema and validation + +The fixture schema is defined in `schemas/precomputed-assignment.schema.json`. +Validate fixtures locally with: + +```bash +python3 ci/validate-precomputed-fixtures.py +``` + +## Mobile conformance and numeric types + +Numeric reads depend on the value, not the `integer`, `float`, or `number` label. +Integer getters accept only mathematically whole original JSON values within the +accessor range. A fractional token must return the default with `TYPE_MISMATCH`, +even if a JSON decoder would round it to an integer (for example, +`1.00000000000000001` or `4503599627370497.5`). + +Double getters round fractional values to the nearest finite Double: for example, +`1.20000000000000001` resolves to `1.2`. Whole values succeed only if the Double +represents that original integer exactly. Overflow and nonzero values that +underflow to zero return the default with `TYPE_MISMATCH`. Signed zero is +numerically equivalent to zero. These rules apply before and after cache restore. + +No global safe-integer limit is imposed on native Integer getters. React Native +and web expose one Number getter; native Integer cases use explicit platform +filters. Boundary fixtures cover native 32-bit and 64-bit accessors separately. + +The precompute service retains `number`. No numeric capability is introduced. +See FFL-3568 and FFL-3564 for the numeric conversion decision. + +Discover cases recursively. Do not maintain a filename list. Fail when the +corpus is missing or empty. Report the fixture revision, executed evaluations, +and exclusions. New JSON files require a snapshot refresh, not runner edits. +The generated `index.json` supports environments that cannot enumerate packaged +resources. It must be regenerated from the complete directory, never edited. + +`expectedResponseError: "INVALID_RESPONSE"` requires rejection of the complete +response before installation. Such a case has no evaluations or emitted events; +require `noUnmatchedEvents: true` and empty event matchers. Assert rejection +explicitly; do not catch every exception as a successful expected rejection. + +The obfuscation cases use `flag-key-sha256-v1`. Hash the UTF-8 domain prefix +`datadog.feature-flags.flag-key.v1\0`, the decoded 16-byte salt, and the unchanged +UTF-8 flag key. Digest vectors are fixed JSON data. Never derive an expected +digest using the SDK being tested. Lookup uses the caller's plaintext key; +emitted events retain that plaintext identity. Missing encoded lookups must not +retry plaintext lookup. Unsupported consumers must reject encoded responses. + +The corpus checks duplicate evaluations after one flush. Every consumer must +aggregate them into one event with `evaluation_count: 2`. Exposure deduplication is independent. +Runners must inspect real event records, not synthesize them from expectations. +Feed `response` to the SDK with every number spelled as in the fixture file. +Parsing and re-encoding through floating-point values can change `5.0` to `5` +or `9007199254740993.5` to `9007199254740994`, hiding decoder defects. +Keep native bridge and deployed-backend proof separate from mocked conformance. + +## Plain and obfuscated coverage + +Every plain response fixture has a generated obfuscated companion. This checks +that encoding preserves public evaluation results and emitted events. Dedicated +malformed-encoding cases, unsupported-consumer cases, and fixed hash vectors +remain separate. They do not require plain counterparts. + +Edit the plain fixture, then regenerate its companions: + +```bash +python3 ci/precomputed_obfuscation.py --write +python3 ci/precomputed_obfuscation.py --check +python3 ci/validate-precomputed-fixtures.py +``` + +The generator discovers plain JSON files recursively. It changes response-map +keys and encoding metadata. Public lookup keys, values, results, and event +expectations stay unchanged. The generated platform filter intersects the +source filter with consumers that support obfuscation. Evaluation and event +filters remain unchanged. + +Generated files are under `cases/obfuscation/generated/`, preserving source +categories and nested paths. Do not edit these files directly. CI compares them +with freshly generated content and rejects missing, stale, or orphaned files. +Adding a normal case needs no generator list or SDK runner change. + +When encoding does not apply, add the source path and a reason to +`obfuscation-exceptions.json`. CI rejects blank reasons and stale paths. An SDK +failure is not a reason for an exception. All current plain fixtures have +companions; the exception map is empty. + +The independent digest vectors remain fixed. Generator regression tests check +those known digests, including Unicode, whitespace, and embedded NUL keys. + +## Snapshot maintenance + +### Preview client compatibility in fixture PRs + +The [mobile compatibility workflow](../.github/workflows/mobile-compatibility.yml) +runs the iOS canonical XCTest suite on an iOS Simulator and the Android canonical +JVM unit suite on Linux. Each job checks out its SDK once, records the resolved +commit, and runs that same code against the fixture merge base and proposed head. +Snapshots are imported into disposable CI checkouts; SDK repositories are not +updated or published. The SDK refs temporarily select the development branches +containing the canonical runners. After those land, change the workflow refs to +the SDKs' default branches (`develop`). + +The PR's Actions summaries show both fixture revisions, the SDK commit, test +counts, and failures. Artifacts contain separate logs, JUnit XML or Xcode result +bundles, and `comparison.json`. Test mismatches are advisory. Missing tests, +snapshot failures, and build/runner failures make the preview incomplete and are +reported separately. When both revisions fail, the result is inconclusive, not +proof that the proposed fixtures introduce no additional failures. + +These jobs run the checked-out SDK runners unchanged. Their coverage and numeric +parsing limitations still apply: a green run does not prove preservation of raw +numeric literals, native-device behavior, or deployed-backend compatibility. + +### Refresh consumer snapshots + +Consumers can copy `ci/sync-precomputed-fixtures.py` and configure the destination +in `ci/precomputed-fixtures.json`. The importer copies committed source bytes, +records the full SHA and file digests in `SOURCE.json`, generates `index.json`, +and writes byte-preserving `responses/cases/...json` payloads for native runners. +The response files preserve original numeric tokens, including overflow values. +The importer refreshes the complete snapshot +and removes deleted cases. `--check` compares the complete snapshot with its +recorded source commit. `--source` permits testing a committed local fixture +branch before publication. Tests use the checked-in snapshot without networking. + +Fixture PRs can expose existing SDK defects. Record those failures rather than +changing expectations or adding local skips. Platform filters describe API +applicability, not temporary implementation failures. `unity` is the identifier +for the Unity Flags implementation. + +`--verify` checks local file digests and the discovery index without networking. +Use it in consumer CI. `--check` also verifies upstream provenance. For Unity, +set `unityMetadata: true` to preserve existing asset GUIDs and create missing metadata. + +Unsupported variation types are omitted independently. Their lookups return +`FLAG_NOT_FOUND`. A supported flag read with the wrong getter returns `TYPE_MISMATCH`. +Missing `extraLogging` is equivalent to an empty object. diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json new file mode 100644 index 0000000000..24f69fca06 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/SOURCE.json @@ -0,0 +1,137 @@ +{ + "repository": "https://github.com/DataDog/ffe-system-test-data.git", + "commit": "9afc3f75c3fef9643a9c1b5e2b1e5a9adac48177", + "files": { + "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "NOTICE": "a5245f10dbda39cc65bad11ab3a6022b9e5c05b259da3f7aa0302d932f31083e", + "README.md": "fa341eba8a1bd3dc5c20e167193b2b080e1bb0a6b709a5a74f8e89020b1b57a1", + "cases/emissions/all-doLog-false.json": "1902637564cde9a7105a80730bfbeb9ce57178a59910c905fad03d7a1118b4de", + "cases/emissions/duplicate-evaluation.json": "88c1dbdcf3c1bb1ee8dd9a0842123447921b9d9fe806f26d24cc41a7d73bb0cc", + "cases/emissions/serial-id-on-exposure.json": "83a7504e6658b875ececdd1972c04386130feb31c607ccf7b61cd45ce46c1fc0", + "cases/evaluator/all-types-success.json": "ff22e47d5bda8a1fef19892c853e4396ccdb8509a3741888eb69169847fc4fde", + "cases/evaluator/defaults-and-emission-gates.json": "fd9c306086b7ce87cacd7f69d2f2c7af03921fffa9b43455dcc89dc17f3c3bfd", + "cases/evaluator/empty-flags-response.json": "f9ddb39e6a425b2c3f7884c7bdf92516e5ebac5b0ed4d5b7be6aa5e38950ea99", + "cases/evaluator/falsy-values.json": "93f26466d4536563c38619499275e2efe9f6c98ce2bc10c55ad57b15292b6647", + "cases/evaluator/integer-precision-native-only.json": "96c4bed183bfeeab7b59209dcdf5b36726e417f1035a28198ef95642ea673622", + "cases/evaluator/multi-flag-set.json": "b1c76977764c1bc11f430a50d33a06bb33daa60ed0d184547250362559f34a0b", + "cases/evaluator/multiple-reasons.json": "812b48f5131814fa7e13b3779b47f8805e59a7b063b5737b17139d01111c0134", + "cases/evaluator/numeric-boundaries-float.json": "f986d58267e590f1be98dc9fe4b5151852c9565028469735b30885a6f369eaa9", + "cases/evaluator/numeric-boundaries-integer.json": "4569c35d977fff5c77134b9e0342b740b7a18e4cf01e461f0c8b5ed9c250708b", + "cases/evaluator/numeric-boundaries-number.json": "ab1e965ac0b335bf56442c0bfa262ba62214b3118a1c1cda6512f38b38717294", + "cases/evaluator/numeric-decimal-float.json": "b761ce8c7581c96799e30a07f13f5e7492e4610d9270eaf3cfbf276bca6c66cd", + "cases/evaluator/numeric-decimal-integer.json": "ddd1cb6c938c0c63effbb672ea7bef979eed3d424cd156d512b71b50091cc246", + "cases/evaluator/numeric-decimal-number.json": "1bd3efccd7a25667e17048868577180b78e51024af1036cbb39cdf79f30f4499", + "cases/evaluator/numeric-float.json": "8a61435912d1ad7c25dc34bfecb030f4d7583f5b4c40ad4f8659b9cce50d93fc", + "cases/evaluator/numeric-integer.json": "b90b486688aa5462028175fe661d4d90bcf933089b63b8092c0004cf181f895f", + "cases/evaluator/numeric-number.json": "36401b78a6ddb09e4fb81c93729325330d090ef58e4be2dc3531e30b8d8da43d", + "cases/evaluator/object-nested-deep.json": "22f8f0c516cfe0456d34697f29bbf39d7736559e0b188de4671d0fee6545fd3d", + "cases/evaluator/special-characters-in-keys.json": "07e24ba1a407ef9604af0bc5ab3f8faafe199dd83db94d96d0e3d689430054ce", + "cases/evaluator/type-mismatch-all-types.json": "0904425126ae64794a8c67d13d4bab759a88abc75c27c99f71c514babb9279a3", + "cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json": "daa6440ae7975580da89b38d71d9c60228858c022df3cd53ba0d906ef335881e", + "cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json": "8a98ce923ea0f56b6c1473a70c2cef2cd7b1892425b068a8e16ac127f3fe6089", + "cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json": "c7d24f386159b73bca86a2c10a58c0afc0fe6b17a557739d5b7f051c3a6aa205", + "cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json": "57bdf315572c3be3e2d997bc516a1332746a465c0e0a370d25f253947075b584", + "cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json": "8eff282ef31291bd85fe33d373711dc470adb450e11eac10da8c465f52a317b8", + "cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json": "cbf05f9e2d24d028a8fe9fceb5ad0d4d3e3162fdfdf51958662a7ef0943f4ce3", + "cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json": "54f2db7cc521fe33c21de4acb84d6dcbb6b625f2634c6fd7ccb7ec82121c8ff6", + "cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json": "f8f7470298fb30ee4566292ca9eb96c543b7009fec513199f5e676ab910efb88", + "cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json": "ffb46863e42fb3161e4c46c4c7c30f556c69c4c3f6172ff98541f3e37e34f1f7", + "cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json": "2955d89877e2a45a7301a7c00cadc2f7826b2e1fea083941a6928f88c171f48d", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json": "3ad1a96dffe7d376e8c381b6b089fb7cd30423b39d8f7f4006fa86fdab3a78bd", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json": "01269d0992978348fe0cba509978ba4b10e65a7bb03284847efb1be1a014d7da", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json": "df03044f0726dfe353b172ae75daf205fa4280a76ec05ac4dfc310f51ca186f6", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json": "d59ce0b4a9600afe68bf7e834dbd0cae7c1db07f147aaf217c148c31e4c0de5b", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json": "bd009c03bf86286d6d613854a2e2472f99e4ef24daf6a7c806cdf01c3e69a20d", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json": "6d2d9129357f864048532ee17d30af88aac4f522aabce844af7fc1c38ae72ff0", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json": "c79fcdca8b38fc24098489de8c85ba78412a8fccbe731b89eae6ac885642bb22", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json": "aa1936cf515645f546de081c9769f9f67b99ff68a178eaa50df482a1b7ecf1d0", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json": "50aee440e18d7192fe2550dc9782229edcb22535122cca84e3891def76594b0e", + "cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json": "ff2ba07eb266db0669f0f86926060d268f3242d437264b5d4663f259317d6bc1", + "cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json": "fd4f9efb1eb115e505c81a30ae82c51766747cbc26ae5a36dda961a4cfeb2cd8", + "cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json": "6dc2320ceb7013825dbda712ed789689a0bd3b36294a741a8bc23371bbe7db91", + "cases/obfuscation/obfuscation-keys.json": "3af487d59d5a7064e0e8e6508343db21689180d7d6e862b055934036e1716643", + "cases/obfuscation/obfuscation-reject-false-with-descriptor.json": "452e57fbcec5a254c65295014ae11675d406a87567e6887933ba493a14109093", + "cases/obfuscation/obfuscation-reject-missing-descriptor.json": "f0105da777c3b35ff10c7082e62ddd1f7ce6a4bc15dae27302f279b18b3bfc29", + "cases/obfuscation/obfuscation-reject-missing-marker.json": "4fb270faacdcfe92477120f9bf9ea56f98e1503cf992c72da71ecc6fbca5e0dc", + "cases/obfuscation/obfuscation-reject-missing-salt.json": "3f570926fd565dbcbdffa0ba83c7142fbffa85e1092d9562f190194b6e746a59", + "cases/obfuscation/obfuscation-reject-nonhex-digest.json": "64f7a96b1ee94cc978fd6ba5dcf4fe10652402ca0cab49c7091c5529b26e9bde", + "cases/obfuscation/obfuscation-reject-nonhex-salt.json": "4813bf66119b890770029039d70ce8dbb8e7749bae808ab3cf953b6ddbf40625", + "cases/obfuscation/obfuscation-reject-null-descriptor.json": "85010c74f15be5a2188c3f534539e82e4138ba1b182bf94b8036348753e3dfa8", + "cases/obfuscation/obfuscation-reject-null-marker.json": "c11bcc39b8235f278d95c9848c47d43ecda5649fe2c6156f37266ffe710eb375", + "cases/obfuscation/obfuscation-reject-null-salt.json": "c7c93a8fe80b4ba3704112124d95b58caff95ac3c07cff232031b4a87626bc61", + "cases/obfuscation/obfuscation-reject-plaintext.json": "5487e40a247337ed0c4ed7921bd62b7232b28309839de91ecfa5ba3d39fd4bf4", + "cases/obfuscation/obfuscation-reject-short-digest.json": "90e7aa899491117416d10989589f574bb5e530394fbaa4b460924083941ee29f", + "cases/obfuscation/obfuscation-reject-short-salt.json": "3e71c820b5eb0e19da5dbd0af088b43b9a854a7f367066576d096ef33770a1be", + "cases/obfuscation/obfuscation-reject-string-marker.json": "a22240089a26b1528ca07f1f7c958d8f7535438e2b47ad97fb717944eef73ad9", + "cases/obfuscation/obfuscation-reject-unknown-scheme.json": "ba4297333f5b8eed888e74a64dacb83d8db7ac111bc5465d61ec07b523331fc5", + "cases/obfuscation/obfuscation-reject-uppercase-digest.json": "a7e8b2de615fc55e5d39cca5ea42d1dfe14dc7a15b40db0d012a80992cb5943e", + "cases/obfuscation/obfuscation-reject-uppercase-salt.json": "65f4a35f6042863cbec5f19634fa9526da1e6c66ddecf3aad488f4455e26ec7e", + "cases/obfuscation/obfuscation-rotated-salt.json": "a97bf1f0d57ad66d122a4286433ba216469d8de2824e03b2d79fd860b87e277d", + "cases/obfuscation/obfuscation-unsupported-consumer.json": "d632485351b4b6cb7b550769cde55c46960ebee7ac811059a5147f143524554f", + "index.json": "70bc5b6ec0800a5bddb9b92eb5b57ec4a89fb2492489dd4fd8759d02015222a6", + "responses/cases/emissions/all-doLog-false.json": "89c1d35aeade0441f7686c355f5871bf1730807e0146a99dcd2e65ae3fe05599", + "responses/cases/emissions/duplicate-evaluation.json": "177cd825091d152be16510ed54fcdd35c8b50b96dbc52f57220bd470d5a9c703", + "responses/cases/emissions/serial-id-on-exposure.json": "99da3ed72a4884137614540a3023d02c6e0dc38f277786d3b377a02465fc0a9e", + "responses/cases/evaluator/all-types-success.json": "5d291b27cca2bc090fcd0c261b3ac601057d1cf210da1b24f57dc12c3e2c7541", + "responses/cases/evaluator/defaults-and-emission-gates.json": "6f77d538af3d603fd33e056a2c777854a01a8fbb8b960baf32005e37cbe488ef", + "responses/cases/evaluator/empty-flags-response.json": "ed293d7bbd20289fd8c2391172cd969e0491c14b0e7dba4060a044a56c1e4248", + "responses/cases/evaluator/falsy-values.json": "f661eec595c65d62ad460bf33c86034cbdd2ff8b8e1e059535c1b025fc6baa7e", + "responses/cases/evaluator/integer-precision-native-only.json": "77aae5ed5dd4c657731b96b750d597624c5d6795d3e397138fae490b4aa03b87", + "responses/cases/evaluator/multi-flag-set.json": "91ebff0413103b47c79279c1d4eab1c6faf7fc173de4ba5c7bb8a7b94364b0c9", + "responses/cases/evaluator/multiple-reasons.json": "5aba3636f4b45ed7da75ea911df19761ad85c9afa35c1eab224a26eab78c1e6b", + "responses/cases/evaluator/numeric-boundaries-float.json": "aa829adbbc19079c36116edf4fde5f8ec1948b082053bf318e7e283784ac0334", + "responses/cases/evaluator/numeric-boundaries-integer.json": "0546104138d058f1b637cbeac016c2e89ab90a9da70ec569e30b234c83f6d96f", + "responses/cases/evaluator/numeric-boundaries-number.json": "055efaaa62bca30b42a8592bf5e58e7b33ef9656435dd877274164f738fb7e48", + "responses/cases/evaluator/numeric-decimal-float.json": "6eebc0d9901064acf93819c951ee3069b3988e3ed3520be6c201d4a3dbff6392", + "responses/cases/evaluator/numeric-decimal-integer.json": "59c1608fb24f99161e65083f97535eb6aba8f3ec5b0a01141803af5fde87d479", + "responses/cases/evaluator/numeric-decimal-number.json": "42d570275fcdd93fd176b51ddf3750770e7378d875fe0bea3f422de4ea000356", + "responses/cases/evaluator/numeric-float.json": "1f4044ba42e73dc10d414fe7ebe8bcdc689ea3c7d6f8bbf0e33cfd3ac2959a59", + "responses/cases/evaluator/numeric-integer.json": "dc09c454e1032e2a6d4e39275c3e1f53ee1f49a79477948bb2eb057059acd60e", + "responses/cases/evaluator/numeric-number.json": "abd8f7d330f539461a0831d5bc8e9ccf5131f82ecfb62486f19fd0fa3345ed6b", + "responses/cases/evaluator/object-nested-deep.json": "4640392a8ae063d097aff33c91a2dddbb0e9061326913e4cad517b8e55fe834c", + "responses/cases/evaluator/special-characters-in-keys.json": "ec8fd2f9a8e8e3006c0330bd2904876805351644ca2e7daec50b0a398cb92739", + "responses/cases/evaluator/type-mismatch-all-types.json": "56f5847e621556e8a5cd531c1fa972dd71a83a9ecfe9cbcf59a7dfa345bccdad", + "responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json": "c148ca5cd2fb46458ea2e194c4049f67a460866061bc5b0f18c68f139b613559", + "responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json": "be559d52b7968786708fd85714c29d085d10f446a43228ba6560593df0a179c7", + "responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json": "d85bef9f0499c391a3c49c1c905f340070f28b6ad2c5e71870113af474a2caf4", + "responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json": "e978fea7702d085debc89e87c0fe9fc446dededf55ecc2fa1780ec5a68dbb5c0", + "responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json": "8e4d4cf240791dec6e558fdf69e23be8dd6a390ae0b914deeacfbd609ea53c4a", + "responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json": "0171cf6510766ffc8d2a2f46bbdf63e4e50fd52e4cf562b1e94f979092ea2c2f", + "responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json": "041147351c6425b41cb45bad5e2b5756b9df2f6b1744c1017353efd97124c167", + "responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json": "9f10390cd804cee826ae5c3afb5255696d8923618166674c132937f9899a8443", + "responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json": "39939e1f0c0546f77e0b7e31f33e95394a92930d52140ff3fa39c4fe432856ae", + "responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json": "430c59292f0477f35803a06f0c36073dadb5e8798f22236a1073d0a09a23524d", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json": "990785d2552476e95123605287e48d29f2f359643bcda9613ab41038061fd3db", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json": "49009b3df826c49e944a0eb3445e5ea0f962e92a9b5a1d6149008dd87a1ed6a4", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json": "657761bef657fd4e659a57f7c8444b602f20f8e1da138057ff6c49b1d7e39e54", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json": "fa89b884af2932d9e4bb7511ed25b2dbd64d2d1e4bae89bddda45d8efeda3337", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json": "d06a3937bf86f7b72598ae04fa2270415ba743442d6e998bf50d9488fc0ee6cd", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json": "8799cd38515b94f6a5ef65069e082e17e4b1a7fc34724f3bcc0958ba45c82f1e", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json": "293398fae9b54a63119bafe0ed0b57734142d2b770510d32c06e17fbb247a7c3", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json": "38d2d7308b4b53f1c481b38222169213ea8bcfd0cacb82352082ccdbd9325138", + "responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json": "0647d09fd2d5f855260807de730efb9f04b5ffd050a18e2ed81b59e2ba45af48", + "responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json": "46816998e484b406fa07926dd5d176918dadfc9d45130682a20a066566e0b0c3", + "responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json": "3e413184859e660e92d85d945001ed10e20b8849e672286c3335220dcdd61598", + "responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json": "bf80790c19597c6b9b53a365363bab1c2b68ae226567e3e11f037dc8fbe39747", + "responses/cases/obfuscation/obfuscation-keys.json": "6a448f288efbd6046cf99abb7f37c9ec17d6de33f35d572df5b3ccb6b4b56a91", + "responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json": "5591fc19a3c234d1bd8b338a130d01a4fac0fc61751bbac58b6da884b30bfe57", + "responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json": "d68791162facae870c8137c709a74b3d50ba6a6cce886cad2c1c514daabc62ae", + "responses/cases/obfuscation/obfuscation-reject-missing-marker.json": "a4c992b2b5b84c158172b351589078367ad6cca8e1f8a5401bc769414428b378", + "responses/cases/obfuscation/obfuscation-reject-missing-salt.json": "8ab59be9c51554b478bd19c801a2eacdf835f61cd271385554952fcf58cd25ac", + "responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json": "3821060eced10838b7853dad305a1e3d42e497276763d503558dfa54a4af4ca4", + "responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json": "f63a963101303f8853c302970190898d578ece967e3152691b31259c1c55ce5c", + "responses/cases/obfuscation/obfuscation-reject-null-descriptor.json": "7dc0425326051934b462745496647f8b58db9508f9230571bf5e7a2eb17c55da", + "responses/cases/obfuscation/obfuscation-reject-null-marker.json": "ca2784e1b92713cb783a806691573661efe012e6a1c55affc6683211cc7c4949", + "responses/cases/obfuscation/obfuscation-reject-null-salt.json": "5cd3e4586774d1647f3c61fa8c69e6e1f5bd69118895df8a67e085eb8872a1df", + "responses/cases/obfuscation/obfuscation-reject-plaintext.json": "f1c6acea2f48e670695a8027aa9b63caed06ae2f78638ce9bb465a38865d5cb6", + "responses/cases/obfuscation/obfuscation-reject-short-digest.json": "eb1975ac97f4a783a9a28ef45f320dc074de260b3aad183b5008cc5776f8e0b9", + "responses/cases/obfuscation/obfuscation-reject-short-salt.json": "42ace4d0cb12d42ea45d0e2564202e95165010a505657b3c8e0f392a409d803e", + "responses/cases/obfuscation/obfuscation-reject-string-marker.json": "6db02000ec578fb36a22d61377b543f5a47827f4890f07ba80ec1a93212c3a5e", + "responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json": "8480004116f463a8003b9de2eeb799f9aed19996f51190464b4213bf48503f97", + "responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json": "18251f8d995ba1ac1ac18a045f37207a6ef047f915af503f710c101d4654bee7", + "responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json": "6bc9c2051caeaf47ef04ae5516da71d732aaa24cf1b99ac617bad83a7f76009d", + "responses/cases/obfuscation/obfuscation-rotated-salt.json": "5611a85ccd64179d38a6fc18bfd70704eabea85d87e16ab14b7dafc31bfcfbc8", + "responses/cases/obfuscation/obfuscation-unsupported-consumer.json": "3cc8fd716bdc34bf00526461f97366f9278fdab0a9340b1f62ea1ac133d89070", + "schema.json": "cb3575d02c1d9cc8f85847b86436cc0aa38e1426c78f797ed28352ffe7ac6db7" + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/all-doLog-false.json new file mode 100644 index 0000000000..701d304e25 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/all-doLog-false.json @@ -0,0 +1,87 @@ +{ + "name": "all-doLog-false", + "description": "All flags have doLog=false. Evaluations succeed but zero exposure events should be emitted.", + "context": { + "targetingKey": "silent-user", + "attributes": { + "plan": "internal" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.silent.bool": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.string": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.int": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.silent.bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "fixture.silent.string", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "hello", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "greeting" + } + }, + { + "flag": "fixture.silent.int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 10, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-ten" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 3 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/duplicate-evaluation.json new file mode 100644 index 0000000000..0e26bc826c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/duplicate-evaluation.json @@ -0,0 +1,63 @@ +{ + "name": "duplicate-evaluation", + "description": "The same flag is evaluated twice. Validates that the SDK returns consistent values and that exposure dedup produces only one exposure event. Web SDK's FlagEvaluationAggregator aggregates duplicate evaluations into one event with evaluation_count=2; the web override and expectations.events matcher verify this. Non-aggregating platforms emit two separate events.", + "context": { + "targetingKey": "dedup-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.dedup.flag": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + }, + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1, + "evaluation_count": 2 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/serial-id-on-exposure.json new file mode 100644 index 0000000000..11e9bcfed6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/emissions/serial-id-on-exposure.json @@ -0,0 +1,109 @@ +{ + "name": "serial-id-on-exposure", + "description": "Validates that serialId from the precomputed assignment is forwarded as serial_id on the exposure event. Covers: present serialId, absent serialId, serialId of 0 (must not be treated as absent), and null serialId.", + "context": { + "targetingKey": "serial-id-test-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "flags": { + "serial.id.flag": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "no.serial.id.flag": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "zero.serial.id.flag": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "null.serial.id.flag": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + } + } + } + }, + "evaluations": [ + { + "flag": "serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "no.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "zero.serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "null.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + {"_count": 1, "serial_id": 340132}, + {"_count": 1, "serial_id": null}, + {"_count": 1, "serial_id": 0}, + {"_count": 1, "serial_id": null} + ], + "evaluationEvents": [ + {"_count": 4} + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/all-types-success.json new file mode 100644 index 0000000000..b2d1b3d608 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/all-types-success.json @@ -0,0 +1,142 @@ +{ + "name": "all-types-success", + "description": "Successful precomputed assignments for all supported Flutter value types.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "pro", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.enabled": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.title": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.limit": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.ratio": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.config": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "enabled" + } + }, + { + "flag": "flutter.fixture.title", + "variationType": "string", + "defaultValue": "Fallback title", + "expectedResult": { + "value": "Datadog Flags", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "copy-a" + } + }, + { + "flag": "flutter.fixture.limit", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-five" + } + }, + { + "flag": "flutter.fixture.ratio", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "half" + } + }, + { + "flag": "flutter.fixture.config", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "object-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/defaults-and-emission-gates.json new file mode 100644 index 0000000000..bf906705df --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/defaults-and-emission-gates.json @@ -0,0 +1,111 @@ +{ + "name": "defaults-and-emission-gates", + "description": "Precomputed assignments that validate exposure gates, unknown variation isolation, and default/error flagevaluation payloads. Unsupported variation types are omitted; lookup returns FLAG_NOT_FOUND.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "free", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.silent": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "flutter.fixture.string": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.unknown": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.silent", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "silent-on" + } + }, + { + "flag": "flutter.fixture.string", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.unknown", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 4, + "_skip": [ + { + "platform": "web" + } + ] + }, + { + "_count": 1, + "_include": [ + { + "platform": "web" + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/empty-flags-response.json new file mode 100644 index 0000000000..0114c621c3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/empty-flags-response.json @@ -0,0 +1,64 @@ +{ + "name": "empty-flags-response", + "description": "Server returns an empty flags object. All evaluations must return their default values with a flagNotFound error.", + "context": { + "targetingKey": "empty-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {} + } + } + }, + "evaluations": [ + { + "flag": "fixture.bool.missing", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.string.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.int.missing", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 0, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 3, + "_skip": [ + { + "platform": "web" + } + ] + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/falsy-values.json new file mode 100644 index 0000000000..ecb04d7640 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/falsy-values.json @@ -0,0 +1,129 @@ +{ + "name": "falsy-values", + "description": "Flags whose variation values are falsy (false, 0, 0.0, empty string, empty object). SDK must return these as real values, not fall back to defaults.", + "context": { + "targetingKey": "falsy-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.falsy.bool": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.int": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.float": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.string": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.object": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.falsy.bool", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "fixture.falsy.int", + "variationType": "integer", + "defaultValue": 99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero" + } + }, + { + "flag": "fixture.falsy.float", + "variationType": "float", + "defaultValue": 1.0, + "expectedResult": { + "value": 0.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero-f" + } + }, + { + "flag": "fixture.falsy.string", + "variationType": "string", + "defaultValue": "non-empty", + "expectedResult": { + "value": "", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty" + } + }, + { + "flag": "fixture.falsy.object", + "variationType": "object", + "defaultValue": { + "fallback": true + }, + "expectedResult": { + "value": {}, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty-obj" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/integer-precision-native-only.json new file mode 100644 index 0000000000..a938cc6942 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/integer-precision-native-only.json @@ -0,0 +1,61 @@ +{ + "name": "integer-precision-native-only", + "description": "Validates that SDKs with 64-bit integer flag evaluation preserve values outside JavaScript's safe integer range (Number.MAX_SAFE_INTEGER = 2^53-1).", + "context": { + "targetingKey": "precomputed-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large-int": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large-int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "large" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "The native integer API preserves signed 64-bit integers. Browser Dart must report separate applicability." + }, + { + "platform": "flutter", + "reason": "The native integer API preserves signed 64-bit integers. Browser Dart must report separate applicability." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multi-flag-set.json new file mode 100644 index 0000000000..303b816d5a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multi-flag-set.json @@ -0,0 +1,471 @@ +{ + "name": "multi-flag-set", + "description": "Precomputed response with 50 flags of mixed types. Validates that SDK parsers handle moderate flag sets without truncation or memory errors. Only a sample of flags are evaluated; the remainder are parsed but not asserted.", + "context": { + "targetingKey": "large-set-user", + "attributes": { + "plan": "enterprise" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large.flag-01": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-02": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-03": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-04": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-05": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-06": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.large.flag-07": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-08": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-09": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-10": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-11": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-12": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-13": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "fixture.large.flag-14": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-15": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-16": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-17": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-18": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-19": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-20": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.021": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.022": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.023": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.024": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.025": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.026": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.027": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.028": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.029": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.030": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.031": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.032": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.033": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.034": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.035": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.036": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.037": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.038": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.039": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.040": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.041": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.042": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.043": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.044": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.045": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.046": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.047": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.048": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.049": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.050": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large.flag-01", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v1" + } + }, + { + "flag": "fixture.large.flag-10", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v10" + } + }, + { + "flag": "fixture.large.flag-20", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v20" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multiple-reasons.json new file mode 100644 index 0000000000..6d507829ae --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/multiple-reasons.json @@ -0,0 +1,91 @@ +{ + "name": "multiple-reasons", + "description": "Flags with different evaluation reasons in a single response: TARGETING_MATCH, DEFAULT, and SPLIT.", + "context": { + "targetingKey": "reason-user", + "attributes": { + "plan": "pro" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.reason.targeted": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.reason.default": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "fixture.reason.split": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.reason.targeted", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "targeted-on" + } + }, + { + "flag": "fixture.reason.default", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "baseline", + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "default-val" + } + }, + { + "flag": "fixture.reason.split", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 42, + "reason": "SPLIT", + "errorCode": null, + "variantKey": "bucket-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-float.json new file mode 100644 index 0000000000..c7c4335c2b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-float.json @@ -0,0 +1,1093 @@ +{ + "name": "numeric-boundaries-float", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-float-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-float-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "unity", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-integer.json new file mode 100644 index 0000000000..4177018bd3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-integer.json @@ -0,0 +1,1093 @@ +{ + "name": "numeric-boundaries-integer", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-integer-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-integer-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "unity", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-number.json new file mode 100644 index 0000000000..4be914128c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-boundaries-number.json @@ -0,0 +1,1093 @@ +{ + "name": "numeric-boundaries-number", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-number-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-number-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "android", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "flutter", + "reason": "Requires native numeric precision or supported obfuscation." + }, + { + "platform": "unity", + "reason": "Requires native numeric precision or supported obfuscation." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-float.json new file mode 100644 index 0000000000..a32ef2e084 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-float.json @@ -0,0 +1,487 @@ +{ + "name": "numeric-decimal-float", + "description": "Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-integer.json new file mode 100644 index 0000000000..b7710857dc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-integer.json @@ -0,0 +1,487 @@ +{ + "name": "numeric-decimal-integer", + "description": "Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-number.json new file mode 100644 index 0000000000..a0a3b234cc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-decimal-number.json @@ -0,0 +1,487 @@ +{ + "name": "numeric-decimal-number", + "description": "Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json new file mode 100644 index 0000000000..43e5f87663 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-float.json @@ -0,0 +1,503 @@ +{ + "name": "numeric-float", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-float-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-float-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-3", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-float-7", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-float-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-float-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-float-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-11" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-float-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-11" + }, + "evaluation_count": 1 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json new file mode 100644 index 0000000000..2b4e63cfb3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-integer.json @@ -0,0 +1,503 @@ +{ + "name": "numeric-integer", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-integer-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-integer-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-3", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-integer-7", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-integer-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-integer-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-11" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-integer-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-11" + }, + "evaluation_count": 1 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json new file mode 100644 index 0000000000..498ff4afe8 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/numeric-number.json @@ -0,0 +1,503 @@ +{ + "name": "numeric-number", + "description": "Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-number-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-number-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-3", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-number-7", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-number-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-number-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-number-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-11" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-number-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-11" + }, + "evaluation_count": 1 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/object-nested-deep.json new file mode 100644 index 0000000000..86bd89aab3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/object-nested-deep.json @@ -0,0 +1,87 @@ +{ + "name": "object-nested-deep", + "description": "Object flag with deeply nested structure. SDK must preserve full object hierarchy without flattening or truncating.", + "context": { + "targetingKey": "nested-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.config.deep": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.config.deep", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "deep-config" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/special-characters-in-keys.json new file mode 100644 index 0000000000..00a4666dd7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/special-characters-in-keys.json @@ -0,0 +1,108 @@ +{ + "name": "special-characters-in-keys", + "description": "Flag keys contain dots, hyphens, and underscores. SDK must handle these without mangling the key during lookup or emission.", + "context": { + "targetingKey": "special-key-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "com.example.feature.enabled": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "my-team--feature-toggle": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "legacy_flag_name_v2": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "mix.ed-key_style.v1": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "com.example.feature.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "my-team--feature-toggle", + "variationType": "string", + "defaultValue": "no", + "expectedResult": { + "value": "yes", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "active" + } + }, + { + "flag": "legacy_flag_name_v2", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 100, + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "v2-val" + } + }, + { + "flag": "mix.ed-key_style.v1", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 2.718, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "mixed-val" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 4 + } + ], + "evaluationEvents": [ + { + "_count": 4 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json new file mode 100644 index 0000000000..801ecd1a47 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/evaluator/type-mismatch-all-types.json @@ -0,0 +1,129 @@ +{ + "name": "type-mismatch-all-types", + "description": "Wrong nonnumeric getters return TYPE_MISMATCH; an exact numeric conversion succeeds.", + "context": { + "targetingKey": "mismatch-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.mismatch.bool-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.string-as-int": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.int-as-float": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.float-as-bool": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.object-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.mismatch.bool-as-string", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.string-as-int", + "variationType": "integer", + "defaultValue": -1, + "expectedResult": { + "value": -1, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.int-as-float", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 7, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "var-int" + } + }, + { + "flag": "fixture.mismatch.float-as-bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.object-as-string", + "variationType": "string", + "defaultValue": "none", + "expectedResult": { + "value": "none", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + } +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json new file mode 100644 index 0000000000..a8fb1e7fa3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json @@ -0,0 +1,106 @@ +{ + "name": "obfuscation-all-doLog-false", + "description": "Generated obfuscated companion: All flags have doLog=false. Evaluations succeed but zero exposure events should be emitted.", + "context": { + "targetingKey": "silent-user", + "attributes": { + "plan": "internal" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "84a2ac6cb08b06570e965cb98d55a0a83b415e1810ef3f4e103cd8fcde14ad7d": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "2fa983b9b25a656f30c3c1ca3547327d0c91abb8dd5a968cbf737fe8b4def524": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "94501ab4c2a58e69a11081f73e5f61a1f37c9c88fa95e61555a9d14ee1dd4024": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.silent.bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "fixture.silent.string", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "hello", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "greeting" + } + }, + { + "flag": "fixture.silent.int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 10, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-ten" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [ + { + "_count": 3 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json new file mode 100644 index 0000000000..c5a6e355d6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json @@ -0,0 +1,82 @@ +{ + "name": "obfuscation-duplicate-evaluation", + "description": "Generated obfuscated companion: The same flag is evaluated twice. Validates that the SDK returns consistent values and that exposure dedup produces only one exposure event. Web SDK's FlagEvaluationAggregator aggregates duplicate evaluations into one event with evaluation_count=2; the web override and expectations.events matcher verify this. Non-aggregating platforms emit two separate events.", + "context": { + "targetingKey": "dedup-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "491646f7138ee482b2ace2d2834ff8ff34b20c8ba8cd329156857ab1be6154a6": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + }, + { + "flag": "fixture.dedup.flag", + "variationType": "string", + "defaultValue": "default", + "expectedResult": { + "value": "alpha", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1, + "evaluation_count": 2 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json new file mode 100644 index 0000000000..4c038b0492 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json @@ -0,0 +1,142 @@ +{ + "name": "obfuscation-serial-id-on-exposure", + "description": "Generated obfuscated companion: Validates that serialId from the precomputed assignment is forwarded as serial_id on the exposure event. Covers: present serialId, absent serialId, serialId of 0 (must not be treated as absent), and null serialId.", + "context": { + "targetingKey": "serial-id-test-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "flags": { + "8c7c04076517d2c3c0633fda9a6664d4b114cb359cc512dd38c4ef69373032fd": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "36d69e5a88de3b456cd6bfca1070ee06c667c9c94d7de9022616661c71928e50": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c7eac87f0bcefab56284d70561f954ffccd2f88fcd895067ffc94832023d3be": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "6597def4cb332745af4ade37ee996a8958091ccadc58082c2c49c4db66c24e98": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "no.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "zero.serial.id.flag", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "null.serial.id.flag", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "serial_id": 340132 + }, + { + "_count": 1, + "serial_id": null + }, + { + "_count": 1, + "serial_id": 0 + }, + { + "_count": 1, + "serial_id": null + } + ], + "evaluationEvents": [ + { + "_count": 4 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json new file mode 100644 index 0000000000..8b20e3f036 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json @@ -0,0 +1,161 @@ +{ + "name": "obfuscation-all-types-success", + "description": "Generated obfuscated companion: Successful precomputed assignments for all supported Flutter value types.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "pro", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "657bbcc45263793d67e4f7967fdd257d6e1896cc095d758c520ca3f52beca003": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "abf42b7b192cf3a719bb8ea77c9ff652c82bd4c7aba54561e374efeb2fbbb9b7": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "820dab9a17046e7bf33d080bcb6f44547e1b92c31b36e7d9dcfde96c46bb5d56": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f4d559502705ccaba0a55b0defc37636fe7ed397a477a07389771c07183f139b": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "41057f3677d1a6c229d214c630b71e9f499abbb68116d50594561663f6ec09d9": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "enabled" + } + }, + { + "flag": "flutter.fixture.title", + "variationType": "string", + "defaultValue": "Fallback title", + "expectedResult": { + "value": "Datadog Flags", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "copy-a" + } + }, + { + "flag": "flutter.fixture.limit", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "limit-five" + } + }, + { + "flag": "flutter.fixture.ratio", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "half" + } + }, + { + "flag": "flutter.fixture.config", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "object-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json new file mode 100644 index 0000000000..b9a7541d60 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json @@ -0,0 +1,130 @@ +{ + "name": "obfuscation-defaults-and-emission-gates", + "description": "Generated obfuscated companion: Precomputed assignments that validate exposure gates, unknown variation isolation, and default/error flagevaluation payloads. Unsupported variation types are omitted; lookup returns FLAG_NOT_FOUND.", + "context": { + "targetingKey": "precomputed-user", + "attributes": { + "plan": "free", + "platform": "flutter" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9176965ed24adf9f9e56f8918f3e4f7d5178ab57ca0fdb3e32266a4203930e49": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "f83c743fa9fc2faf137298f504966877bbe7d23e7cb9380c4b73188b764b0a0c": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1345780c7d221b7c1798057d61113f43466cb92b31ecb05fa74ee4b511d7c482": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "flutter.fixture.silent", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "silent-on" + } + }, + { + "flag": "flutter.fixture.string", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.unknown", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "flutter.fixture.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 4, + "_skip": [ + { + "platform": "web" + } + ] + }, + { + "_count": 1, + "_include": [ + { + "platform": "web" + } + ] + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json new file mode 100644 index 0000000000..c685167982 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json @@ -0,0 +1,83 @@ +{ + "name": "obfuscation-empty-flags-response", + "description": "Generated obfuscated companion: Server returns an empty flags object. All evaluations must return their default values with a flagNotFound error.", + "context": { + "targetingKey": "empty-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.bool.missing", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.string.missing", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + }, + { + "flag": "fixture.int.missing", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 0, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "evaluationEvents": [ + { + "_count": 3, + "_skip": [ + { + "platform": "web" + } + ] + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json new file mode 100644 index 0000000000..e6bd5b9e43 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json @@ -0,0 +1,148 @@ +{ + "name": "obfuscation-falsy-values", + "description": "Generated obfuscated companion: Flags whose variation values are falsy (false, 0, 0.0, empty string, empty object). SDK must return these as real values, not fall back to defaults.", + "context": { + "targetingKey": "falsy-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fc0d169622e17ab8efff1da7fe0d0ff124de893d2e723fb9a8c632a87f5617e2": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7a856abf0d6b1d857471bdfe3f81e7e0c08a1c7da13315ffd303a60e40032c8d": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8e902056a9d96a536e0300af89c407afe670d48fac2ae3ed9c4f0805579011f0": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7aa973f2d2a8f5ff69c54faad155bbad70b0c2733f20d10c0ed170749ce60f3c": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5273f6912bea9ecac5cd7d11674e39b208fa0bc796a3379e5b46138a4d2771a3": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.falsy.bool", + "variationType": "boolean", + "defaultValue": true, + "expectedResult": { + "value": false, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "off" + } + }, + { + "flag": "fixture.falsy.int", + "variationType": "integer", + "defaultValue": 99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero" + } + }, + { + "flag": "fixture.falsy.float", + "variationType": "float", + "defaultValue": 1.0, + "expectedResult": { + "value": 0.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "zero-f" + } + }, + { + "flag": "fixture.falsy.string", + "variationType": "string", + "defaultValue": "non-empty", + "expectedResult": { + "value": "", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty" + } + }, + { + "flag": "fixture.falsy.object", + "variationType": "object", + "defaultValue": { + "fallback": true + }, + "expectedResult": { + "value": {}, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "empty-obj" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 5 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json new file mode 100644 index 0000000000..8ab272bb1e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json @@ -0,0 +1,66 @@ +{ + "name": "obfuscation-integer-precision-native-only", + "description": "Generated obfuscated companion: Validates that SDKs with 64-bit integer flag evaluation preserve values outside JavaScript's safe integer range (Number.MAX_SAFE_INTEGER = 2^53-1).", + "context": { + "targetingKey": "precomputed-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "60c239d3d5039428009ffc2a7918ccbd9c6485ee75e7d3130b148103d3e784ae": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large-int", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "large" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json new file mode 100644 index 0000000000..0883414b59 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json @@ -0,0 +1,490 @@ +{ + "name": "obfuscation-multi-flag-set", + "description": "Generated obfuscated companion: Precomputed response with 50 flags of mixed types. Validates that SDK parsers handle moderate flag sets without truncation or memory errors. Only a sample of flags are evaluated; the remainder are parsed but not asserted.", + "context": { + "targetingKey": "large-set-user", + "attributes": { + "plan": "enterprise" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bf9be622ca7517e32cd02c784169beb8e5102892504fdad8384e348d48e36879": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "75dcd247147ab21d09a4fc73051324b2000bd0bf97d82d35ad6d80eaa6d07973": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32681eee3e6ebf4ebaa0b03b91638cbae0920b26239bdea4fa2b7d0bb3a46df3": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "792f76037d724266f9c00e971967bbfe62cba43274b31f652b9768d6381e7ace": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "59bf45e79a60fa4a33f2d4c639ef910982d3d3e29ca2fb9c147fb4977569f35a": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fce91fe1f119ec27d34a9cb77ab5014d49153c8afa8162b282c9c595591ff130": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "086529987ece3ae0319c12332405bbec6e6eb913266ade8c8fb33b2b7b89ab5f": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1d89f79a25e4eae7f080cb86d35af66fd2147336f6c450dcd40d23333f83d888": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0fe82d095e9c1dbefdca57b310ab4c19c80439c87a41b0eef548685914df239d": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "1ac48586eced16b0aff76e17312da68f2c43fd3cf9ca33d76f3e427f93797c4b": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cc6234a3430646b1df0c9e2bcca80dfe16b05abcca88d58f5f97c19a14a9a320": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "215f50d3c5e5908c31d2bed80414ce26ba68b264ea5baa4ab5b743e1833c1945": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "88c23388c4cec50c57429698b96c975be9f8915f88308fba6bda54acc6d01998": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "b66bc4f899fa5ad230fa050a3ba35ac5c5cda22bebecbb44e6c63afddbaf7b5b": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "83ab766d8522b069512a37e2a5bd8bd8f941fbeef306eab09f6247e8db3b725b": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "db1c7d8f6ac65f0e493bb4c3a87f153ec6cac18bd0c984b20ff09619ff79cce9": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "4d23aa03aec589f62d46d15642b79f6e3c87e3aefa5b49cd319304f4a013ac95": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "023b47247033e8bea815ff0cc16255bebdff082b3cfb1eeb4a9b55b3004276e1": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "0b2b70d8c98c0731692191e78b527512e548190d97d710d7f0fc8f19cd05913d": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c1796f3cb195313519a837ead5d54e083195b6206e189bbdd0220561b4ca766": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f73dce8e960f6d67023c407ac490ce3605b279288b56f78031bfed36f0014935": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "c078d7577eff8d7a6fc8963d0bd16fbb172c3a4a3aeb9e714c75a2574d7967e1": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6326ec0ed3c2465e7cc807f46c743037e7095d3f8e9d1d56dbaea76b2b00452b": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "3d3d982d7910fda9264321566138165779600cddbf42d215d689080b617b3fd0": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "5a80d2d736721d87db810627614f4f6b127ee0a332f5fdea8550a78e3ec4e699": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "86edc8f5b58892c55f5fde77e66cdc7b6d0a9b071e2677d8fa16dbb8f409f380": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "cc6b19cce7d683983134195f55a0c6bf3a5f844944e283cda1c58485b5a2dd2a": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "4c2f7e12bea6b91c5d5c9862de0b24418f6fde074eb5e017c93293cfabf60060": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5a48e1a5c7c8e185b62cd259351f4abe443771f07292eefbd49f0599881e35ce": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "cf6321f08df13c06652debb11c4cb9ea67e215e5a364d845f30d865554876b4d": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "1660da21238865ae03e7c5c4959d02a34bc210101d7608f7221f97f254b801d0": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "34529807d59d7bf904c56e17e112ed555687ee05a332a9ef957d7f029870ed57": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "f8fcf4b3a145194a19daabe0521e6cfea4bac6b20a6fd729f4c8c3ad6ac3e6f6": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "210a549229e78c66c588dffc37d99918e7968a04876fa87cd1b719a5dfe1edf2": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ffd5bbbf90dd7c225ddc95b0c1134cee255c5d1ee7f7889bf2c239b99dd43db4": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "929200b0fd1248b310ac0f0785a5a3ee3b1aa40cbd4cc7d7ee7c141bd57c839b": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "4aefe68d39be982605014ac90b746de9be85b4bea79cacc26459cdc06b96f48f": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ba21a1d31e1f9074d322885263107157affa2265582bf6a05f84441b8c118eb": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "77581b9776c4d5afa1919a1e0c758067d07c3968afa2201870cea6dae6579aa3": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "03461ca38265adbdcede804d0ba7a544dce9c4de6296eb32d8a6683096a9b1ce": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "92325ac72dff844afbdc8a8a8ca939b673fbb452cd67d7b970990c58596baf57": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "934025c0b96e851d348e09166362375a53e2776f595da60d4e2fde30f1503baf": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "f6d270502e6f9fc1f4cfa7fbcc78e2b3377d9fb480f23e864cd4be0c96ec3467": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8bb8e9b84c7ab5397d4012c8d58cb797a8fac7562c86ede747d91f5971bfef10": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "81be2644335d094121dc49dafd671c0c212980253962dd2f24986736cdaf9583": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "b4fa3c55cb8c05baba5ca43eac146e6036b0028e2fb07eafd6819d3935c9e4bc": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a56d4714d20368ac54afa5225b1dec452d9bc102f604fd54bd338e4b693bda38": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "e5b919481d265c3573d94171004324cff98c4e223efd7b568b028ffcd4314a6e": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "370b0fcd16be8003ef1d1e1f39e7e19b3698f17a24957ed654420be0f81eddc5": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0721adc36960473f00919092dc06adc2988d409a72f5244bc47a4b11de285660": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.large.flag-01", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v1" + } + }, + { + "flag": "fixture.large.flag-10", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v10" + } + }, + { + "flag": "fixture.large.flag-20", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "v20" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json new file mode 100644 index 0000000000..4002b72470 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json @@ -0,0 +1,110 @@ +{ + "name": "obfuscation-multiple-reasons", + "description": "Generated obfuscated companion: Flags with different evaluation reasons in a single response: TARGETING_MATCH, DEFAULT, and SPLIT.", + "context": { + "targetingKey": "reason-user", + "attributes": { + "plan": "pro" + } + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "19f9605f249b328efe6542c0a39acdaa7a0b39c61daccb3883f2cf3c5705e9a3": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e433cf607656e33013abf122d4f7576389f0acf3ea88f085fd069bd839390a4f": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "212fded283b3498c3742391c9db4843ff32aad02ded7646c68dd14bc008f3f03": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.reason.targeted", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "targeted-on" + } + }, + { + "flag": "fixture.reason.default", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "baseline", + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "default-val" + } + }, + { + "flag": "fixture.reason.split", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 42, + "reason": "SPLIT", + "errorCode": null, + "variantKey": "bucket-a" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 3 + } + ], + "evaluationEvents": [ + { + "_count": 3 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json new file mode 100644 index 0000000000..90483ddba5 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json @@ -0,0 +1,1094 @@ +{ + "name": "obfuscation-numeric-boundaries-float", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "35dd7544a6b9a875c5074a893aa3d5c95f015b6cff7f07f30390bc90b939839e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "54b3e4e77e354d4e049991350fe68ce1a90ba4e874933a9e269d5b99cb3ad115": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6cec42afe677ad01472e4754a611106037ddb8e786570bf9a730630b8d2f7f30": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8de4314e7ce11a9f64a3490e59968a1130f56b15c39276bdff9ebc344402101d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4182dc078ece15bc1c45cabf6e2ab2ad0b7acaf7f42eaa3ad63fe3956beb1db7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ec7fd610e215fd5d177ffa68bec26d50a871bc1cf0114636a734e1660bd6411e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "53d34567f5337f1104d86a9e821df4511fa74a9cc03516f426c9ab4c7af0ac52": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3e69e701cc02087f7897b6f4832e8be0be9ca155876f8cb95597e6ad32b1e2ab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd1a3e85e06626dbb1c56432a8dea7a0128284f1d781013835a2564e70f87d98": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a212f6d288564e940e009835514eaad55a3d62caada53dd1fdda1b2475efed0a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f8dc9d43242ec89a4db2212c3a70ee9c938b0cb36afac45b2000cd7c77bf7609": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1e20092aa2b67b3e8ed9d5533a2260ff177207d9b86fac2151864b2d7af492e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "45f0d04656e98eb32b8221714cf169d57479d2be4af6b96c5ffe0f9fd589ad0a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "680bb5138700a197f7896709b20d6a451854dfaf7fd2016374154dc214101f96": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5f0e609737393c76014f125cfe1d8b685065b3f8bb2833e393f4de4bb3f271fd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6de38d27b71396e8169bdf012bf53a9f6112b4f9533e492d1d0c372aa5b2b89a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e68a8c78bf6d545e6c949e88209611b6615ddeea9817440de9fd3fb7a289c335": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6d6386fa18188d57ad46d118b937d77a1a8d814e3ff723871452b9d5f6abd03": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9d28b9ca14aed8cd8edab7144dc09ff50ff608f1676df0c92be1a225fc4c95b4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e026139b2c1ef067fcd194745a316b948ca17d4224a2c6ef0f54e42739a6f9e5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9ad722f4aa0e6cbe77da0e6406a3f40ac0dbe3f556a524d1d504115896235fed": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "56cd8c449d33e0a19ca9b0d300b40bd994dfd1f4f3b16d093e453d8a97a3645f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-float-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-float-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-float-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-float-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-float-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json new file mode 100644 index 0000000000..4acc8be2aa --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json @@ -0,0 +1,1094 @@ +{ + "name": "obfuscation-numeric-boundaries-integer", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "dba4f4d05c42d94f2f0fe7fb4c5622432cbace9983c4f2bcd81dba39a6327250": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c053d531267e94db3094b514ac132333832cf45cc102899e23a317cfd1b86c7c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cc6b41d8abf103fc2c0903beb89ea4916203e65e34f56f229bd4bfd30fca2b44": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2cd758e8f6b3d6069426eaf4fe83cad350062d6f3cf9fdff78af6814151ff35f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "be1e08347c503a3116b8872a98f762ce80f8b2d5d9712855f553e9a8e41a0e24": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "439b8c652a5b3d6bb6a8b5601f86c09f38f1ca344dfa0cd10904c41d3a0089ba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "52c0be60636eb11b987e1729b894a63f02463a408660d632004cd68b66c17966": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32ac57434b06ca630219caa81b6d6c685138920a8a9d1e6205055cff023f3179": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "21bae751d95202679767297ac5a668ff2588c260e0c1d93e0ddb494a3c6c44d9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ceb204983395689386573ade9d3496ac4c07fde14fd7dbc965b51654af834df1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c2b69fdede340468e31ce79ee511016e1f3f15fceaf2228fb6bde3c034cb14d9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0a1f9c3aedb3a5721f4215c26cd403a7bca6d5fcba088d6ff8c5eb58a4168903": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cf564971ee029d99592fad8d056f2d1cb13ff91e3e283dc2c16b3a644db90734": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "482ab354d08783942a1cc1613d261773481b27647304272c9a895b3f82aef81b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8ae623d95ec069f4263033a5c3c63cc41c8369a4ada5c2df1e72feb3472b3298": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3ee628175449c6db9aeeb2b77f155f2af7f566ae1209ed32f564b4e74d37ffbe": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1aaaeb79098a7feede64e211e53f192295e0d97db557223f5945bd869a9ec82c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1a9a9de8e21c848b2bfec22d14c770d8a3a176dd1ce39955a5b165bf1f851590": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c2dca5f665b034d94acdc589d0b6646e583561b3f05aeba0a0b6399297c8ccf1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a014f0066d631022d1c7628bd1b990291f772710900955e0aab48543db7c3b2a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "05ab2bff8b65224aff72672e380d66e022ff5f0c1b76e3231aaf172f6c82d467": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a10340fc40240dde6ba540065e6526b8c66b6fd78d116451b42201a9ba4c665d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-integer-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-integer-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-integer-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-integer-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-integer-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json new file mode 100644 index 0000000000..27bfa4511b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json @@ -0,0 +1,1094 @@ +{ + "name": "obfuscation-numeric-boundaries-number", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "4151b6612247033495b8370b980c847c1efe5dc3ddb008a57aaf9e8afb97694d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c3c464e47d3e87d433d2a0b6a5ea9e9cea9ffb65c31a7022fc4be4c51a3e3663": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "d9adf2748e7c2bb330f6b321204a4ab3d61aa555dbeaddcb3d79fb20383e3840": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a3e931eb60a9b70284b948912691bbf4e1f757d1f52c90d1623413a4ed05eaf7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "56a5f99336ac7daaa327d074e3c1cf1389f3b0e198d8b6b0dc4a0773f37466b0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "996e91f621b0091b21aaceea06330f3bcc8a4639939a0b3cafd38cdd2d732bde": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5b95fc3592f62934e978d8f18e998120d1012da96a4ae3c7a3bc832442914bce": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fb8ab09c443a1fef7f78aa00069193131bdddd5f754c69837da77dde8bd53ab4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "13bc44aee0e07075a5f61e06bea48f9c7d7cfe240bbcf45d9922127c5fcbf57a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2b8ee396fbeba43a943135d2ea6962f4eee41f3af704609313b6241bf4911f20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9dec15ccf9649edff360ec3a223801aa6f804087a939b1d14b686a7fe34a0cbc": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1aedbf12de210a49f962e5e8db09a065c2b328a7d2c48ce4c6e919325b9c0c5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4a989c369519e210c4857cd50fd525e571cc89b7da322276ba05487c15b4849f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "313de806bdcd55e96b13d12c28260bc5238057d4ed2a8ecc0294f8c7e234fa01": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1f5409b64eb99b424819230ddfb333b5f399a911840ab6b36f0a0938a438af25": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "61f7deca99e29b35ab18cdf82001dcf64ea3b04caaa9fc751d835f1c70d25fd2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "06503c386b2c84e0299e7a692baecdecf2b19698b90f13b89df7d4e309d43870": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6d9de4417c04ad9908dc846b1a9ae5c6c2fe214e7aca7e2dd3480d82abd46a5f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b553f209415af2ef3dd59e7f7383e270e5090b9811e319c39645a51d58ec26c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4ccf6d4d18107b748ae8ec301e66adb190f15bd77e3bf23246d8994787c8a40c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9de54af153484d9db86d593c9a0c9443f7c5a88881964452b69b7f2d8a878c01": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "52b56cf56113aee71339a1256252ae52741aa5e666d2a5ca3ae3ad194cbb83f5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-boundaries-number-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-1", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-2", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-3", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483647, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-7", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-8", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -2147483649, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-9", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 2147483648, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-12", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-13", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740992, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-boundaries-number-14", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-15", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-16", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9007199254740993, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-17", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-18", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 9223372036854775807, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-19", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "numeric-boundaries-number-20", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-boundaries-number-21", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -9223372036854775808, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-0" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-1" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-3" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-4" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-7" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-8" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-9" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-11" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-12" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-13" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-14" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-15" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-16" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-17" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-18" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-19" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-20" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "numeric-boundaries-number-21" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json new file mode 100644 index 0000000000..fa184c7665 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json @@ -0,0 +1,506 @@ +{ + "name": "obfuscation-numeric-decimal-float", + "description": "Generated obfuscated companion: Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json new file mode 100644 index 0000000000..ae7e9126bf --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json @@ -0,0 +1,506 @@ +{ + "name": "obfuscation-numeric-decimal-integer", + "description": "Generated obfuscated companion: Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json new file mode 100644 index 0000000000..a7e078942a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json @@ -0,0 +1,506 @@ +{ + "name": "obfuscation-numeric-decimal-number", + "description": "Generated obfuscated companion: Exact original-value Integer reads, rounded fractional Double reads, and range rejection survive caching.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "decimal-rounding", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "near-one-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "near-one-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "large-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "large-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 4503599627370498.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "larger-fraction-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "larger-fraction-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 9007199254740994.0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "inexact-whole-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "underflow-integer", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "flag": "underflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "overflow-double", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -99.5, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + } + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + } + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + } + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "decimal-rounding" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "near-one-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "near-one-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "large-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "larger-fraction-double" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "inexact-whole-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "underflow-integer" + }, + "evaluation_count": 1, + "runtime_default_used": true, + "_include": [ + { + "platform": "ios", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "android", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "flutter", + "reason": "Requires a separate Integer getter." + }, + { + "platform": "unity", + "reason": "Requires a separate Integer getter." + } + ] + }, + { + "_count": 1, + "flag": { + "key": "underflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "overflow-double" + }, + "evaluation_count": 1, + "runtime_default_used": true + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json new file mode 100644 index 0000000000..bf65bb4b90 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json @@ -0,0 +1,522 @@ +{ + "name": "obfuscation-numeric-float", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "6711bb1e8b093b846ea439322c667bce9ca0568b0f0d4320e05c236c9ead11ce": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "dd28ccc7dc267c0927246a422010adbf6e81bb5b0e8fa6a3f9c5c999dcfa8cac": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9662b5e2c026477337a6a1b952ca1da98116ea3393c14ad81bf3b0e6f7705eae": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6a126c45f099dc1465fb20627b24f64632aae36028757b78b361873c454d566e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7a331b89b73d723922e64da680ad3048d381db62fd6f9139eb9e57ea43569156": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e152b1e1e80ca9edabd8cfd196b0084ee4728b8eb6b85c1de743705b8ec3f1c5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "968e09a8af812ed8d7fc18b21f59d5b7623d5e1ae6fc8f5a141443f026b11aa7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b4b714689b3211e62d2fbde91099071f193e00c955f5a96a6ffa087b2e6bfa98": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a978ec7a2487b240242a05119affc348f41d0d9fbdce334665c4090c2f4ab1fd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3860419b6dc9dc69573f100ccbf51c008e45a8a9b6a62243ff02b869f4e86b47": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a303e6fa7efedf997c87edfc2d79872c95d3e1a6c5310ccfe65d0826f700ab55": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "41a0cf77c6270e0e6f3941ab2fa80f712b1c916d13793ddd36a99363c07ad885": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-float-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-3", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-float-7", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-float-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-float-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-float-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-float-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-11" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-float-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-float-11" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json new file mode 100644 index 0000000000..e61afa1d8a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json @@ -0,0 +1,522 @@ +{ + "name": "obfuscation-numeric-integer", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "6f6faa00700939aa0c395e059d8dea1cc48b2c5a20eb2ef5bc7abfeb551e2d6a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6418f8b5f4df9814bfd25f30a8ef6865929075706f260ca4f48f5334f5541c89": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0f2ba6c096112e6b701607e345fc3a06a2a0d93710715cea905546b69b536ec0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ddd11d6e8fd3dfc9734f307630745484bb411a3278c36ad0dd7c681f91edcde5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1108b8a690b2ca3dd55e74294ed8bfdf1496b88ed4b3cb87abf27199c6b819e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "01ede755b925df7f56ae33a0bcc4d822a0495b602eea740c5b8570c025527afa": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fc310ab0e0bd16d08c9b769db925a177fa89b3f539d75b01ef8dd319c427fccf": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ad9ca46d7714846bd2a813e74d2af97e1cd25df3b0242f1575db2083f6f9d8b1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "35eb2ca6e144c429db174c281e9e55d2b9b4e9d819a159203bbdf577354e4033": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "27bc83275f0f9a5e871d2e4c5af47829d9f7b72c3e5f1f224fbafe3cef986c81": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "19415991350c00c660f9d74c206db9ec7d9fbde7c8a8cbdcfd6775911e40be6b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a556037a239ae8b7c3789f59013e015ebf29686de47bce8aa1bb17f99569a4e8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-integer-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-3", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-integer-7", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-integer-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-integer-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-integer-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-integer-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-11" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-integer-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-integer-11" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json new file mode 100644 index 0000000000..6ea6bdf921 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json @@ -0,0 +1,522 @@ +{ + "name": "obfuscation-numeric-number", + "description": "Generated obfuscated companion: Lossless numeric reads are independent of the numeric wire label.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "5a1d7afff3fd522b50e8891f7af14267bdd253a1455ed12bc92188f78e12033d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cbc15a556f7e611fa26f06f6a6334ee91ad5d6a344496452557db41ed992bc25": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f1c431305b153a3030fadebc0057e634b6162cd6acda9744a763082b1197c740": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7b3cbab857c42d32035705aa891dd7a2fbadcbe1fc0f1e28f3611076a6aa7945": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "acdfe2ec44b3eb617b86da9a49166e3abecbc85c00951e548516cfd748056403": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3af8ba8e93c58c248ed0d943b8bfbc630236bbbeca96be121efc46ed8da4b9b0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "efb49302883b084e49d51cb2f41fd795224a571d182536e203f6ab28e6648b77": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c38a503ec9a1c032f0a073e23d6d0ede2eccf245aac33f9db02a5374728c810e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "499aef4e9d9d7df120a79816e9406b5925664e851fd638a129f525588e8a9385": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b004cb3fb7dbb1846ba17f461b1a0931cd52d9ffde2c5419d51c0b47e42baf99": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "d858ab64de6a97983f95792030011b2dd635938aaefd90fee12b37ef67e9dac3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3ece2688265c5c214d2904619cf0294042830dc10d72dae32e50050c7cded8c3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "numeric-number-0", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-1", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 0, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-2", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-3", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-4", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-5", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-6", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-number-7", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.25, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-8", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-number-9", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": -0.5, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "numeric-number-10", + "variationType": "integer", + "defaultValue": -99, + "expectedResult": { + "value": -99, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + }, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ] + }, + { + "flag": "numeric-number-11", + "variationType": "float", + "defaultValue": -99.5, + "expectedResult": { + "value": 1.2, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "numeric-number-0" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-1" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-2" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-3" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-4" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-5" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-7" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-9" + } + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-11" + } + } + ], + "evaluationEvents": [ + { + "_count": 1, + "flag": { + "key": "numeric-number-0" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-1" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-2" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-3" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-4" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-5" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-6" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-7" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-8" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-9" + }, + "evaluation_count": 1 + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-10" + }, + "evaluation_count": 1, + "_include": [ + { + "platform": "ios", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "android", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "flutter", + "reason": "This case requires the stated native numeric accessor range." + }, + { + "platform": "unity", + "reason": "This case requires the stated native numeric accessor range." + } + ], + "runtime_default_used": true + }, + { + "_count": 1, + "flag": { + "key": "numeric-number-11" + }, + "evaluation_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json new file mode 100644 index 0000000000..eb4655e1ea --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json @@ -0,0 +1,106 @@ +{ + "name": "obfuscation-object-nested-deep", + "description": "Generated obfuscated companion: Object flag with deeply nested structure. SDK must preserve full object hierarchy without flattening or truncating.", + "context": { + "targetingKey": "nested-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "a75e90efc57ed0e56bd11c3a9b17a2623dcc2a412fa52f3ffa742139fa6b93af": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.config.deep", + "variationType": "object", + "defaultValue": {}, + "expectedResult": { + "value": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "deep-config" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 1 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json new file mode 100644 index 0000000000..7862719d2d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json @@ -0,0 +1,127 @@ +{ + "name": "obfuscation-special-characters-in-keys", + "description": "Generated obfuscated companion: Flag keys contain dots, hyphens, and underscores. SDK must handle these without mangling the key during lookup or emission.", + "context": { + "targetingKey": "special-key-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "5ed289266d5a488851241b3122aab121698b75a75da9fb687c671d606a099d99": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a5bcd95f28f8cf915795582c1edd5b263d2f42cd8a513eb6d69aeef26c39ead0": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e4fb9f7c538eac04c76c225b60d43a48b5f514a112a148d95d25754ad14cc8a8": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "dfd73ab3f42a47bd441de225f613432120e3eb38a6f00a21cb19a45a9de23c70": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "com.example.feature.enabled", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": true, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "on" + } + }, + { + "flag": "my-team--feature-toggle", + "variationType": "string", + "defaultValue": "no", + "expectedResult": { + "value": "yes", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "active" + } + }, + { + "flag": "legacy_flag_name_v2", + "variationType": "integer", + "defaultValue": 0, + "expectedResult": { + "value": 100, + "reason": "DEFAULT", + "errorCode": null, + "variantKey": "v2-val" + } + }, + { + "flag": "mix.ed-key_style.v1", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 2.718, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "mixed-val" + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 4 + } + ], + "evaluationEvents": [ + { + "_count": 4 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json new file mode 100644 index 0000000000..d7950c8e68 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json @@ -0,0 +1,148 @@ +{ + "name": "obfuscation-type-mismatch-all-types", + "description": "Generated obfuscated companion: Wrong nonnumeric getters return TYPE_MISMATCH; an exact numeric conversion succeeds.", + "context": { + "targetingKey": "mismatch-user", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "937d54653333211a63876dd0e042edb2ef108105fb918420af28ff03f620a982": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ed1838c062426321f2125dd154000611b46339c11bbbab4228df3fad88007f6c": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7ba0090b5e66bc7319950479a1e905c26fedd82e605530cdb4eb0adb4c7d5515": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1fa87b21c5821755d962ce404e18a06c5ae2656dd33d4e915e80e425fc77c190": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9a172110c6d75715ead4e47f3063d7ab4d92141de6f83681141130da17f9b743": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "fixture.mismatch.bool-as-string", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.string-as-int", + "variationType": "integer", + "defaultValue": -1, + "expectedResult": { + "value": -1, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.int-as-float", + "variationType": "float", + "defaultValue": 0.0, + "expectedResult": { + "value": 7, + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "var-int" + } + }, + { + "flag": "fixture.mismatch.float-as-bool", + "variationType": "boolean", + "defaultValue": false, + "expectedResult": { + "value": false, + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + }, + { + "flag": "fixture.mismatch.object-as-string", + "variationType": "string", + "defaultValue": "none", + "expectedResult": { + "value": "none", + "reason": "ERROR", + "errorCode": "TYPE_MISMATCH", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1 + } + ], + "evaluationEvents": [ + { + "_count": 5 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "android", + "reason": "Supports obfuscation and the source fixture." + }, + { + "platform": "flutter", + "reason": "Supports obfuscation and the source fixture." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-keys.json new file mode 100644 index 0000000000..9c1be0a323 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-keys.json @@ -0,0 +1,259 @@ +{ + "name": "obfuscation-keys", + "description": "obfuscation keys.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "adca75d2141c51b0c0f084c1058edfb8e6e763f91aaf54ca06326d9847bd9586": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1a5f851cc82a3fdf03a461d72a2241584dcf455df1d384e02a937901b3bc80b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3bfa8c3c17c1b61035b98ecf14007cdf5cba5ce61a48e8cfb65f8106541892d3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1e8b7ec5e8028a1ec96b38dd37f6ccea041c0a90358904af40ac5810c23c8765": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "94b611e0d3b26b52f6ad66013d1c72f8a92ea109390049759e75d3c8d3aa4dab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bac134d201be5e7f28fc7019248f0809c2a013b137e866446ee71add2bc344c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "072d985b427f536ad0a11b2d4c5e0f7e6f0ff6d23e779e082f75599ce3fe3eba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [ + { + "flag": "flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-0", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "Flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-1", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": " flag ", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-2", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "caf\u00e9", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-3", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "cafe\u0301", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-4", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "\ud83d\udeb2/\u65d7", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-5", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "a\u0000b", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-6", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-7", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "flag" + } + }, + { + "_count": 1, + "flag": { + "key": "Flag" + } + }, + { + "_count": 1, + "flag": { + "key": " flag " + } + }, + { + "_count": 1, + "flag": { + "key": "caf\u00e9" + } + }, + { + "_count": 1, + "flag": { + "key": "cafe\u0301" + } + }, + { + "_count": 1, + "flag": { + "key": "\ud83d\udeb2/\u65d7" + } + }, + { + "_count": 1, + "flag": { + "key": "a\u0000b" + } + }, + { + "_count": 1, + "flag": { + "key": "" + } + } + ], + "evaluationEvents": [ + { + "_count": 9 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-false-with-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-false-with-descriptor.json new file mode 100644 index 0000000000..40a036689d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-false-with-descriptor.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-false-with-descriptor", + "description": "obfuscation reject false with descriptor.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": false, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-descriptor.json new file mode 100644 index 0000000000..0c0d8633f1 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-descriptor.json @@ -0,0 +1,46 @@ +{ + "name": "obfuscation-reject-missing-descriptor", + "description": "obfuscation reject missing descriptor.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-marker.json new file mode 100644 index 0000000000..18741990fb --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-marker.json @@ -0,0 +1,49 @@ +{ + "name": "obfuscation-reject-missing-marker", + "description": "obfuscation reject missing marker.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-salt.json new file mode 100644 index 0000000000..ce837948c3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-missing-salt.json @@ -0,0 +1,49 @@ +{ + "name": "obfuscation-reject-missing-salt", + "description": "obfuscation reject missing salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-digest.json new file mode 100644 index 0000000000..1cbb1a4e6b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-digest.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-nonhex-digest", + "description": "obfuscation reject nonhex digest.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-salt.json new file mode 100644 index 0000000000..1c2c2ffbb1 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-nonhex-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-nonhex-salt", + "description": "obfuscation reject nonhex salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-descriptor.json new file mode 100644 index 0000000000..e1d273588c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-descriptor.json @@ -0,0 +1,47 @@ +{ + "name": "obfuscation-reject-null-descriptor", + "description": "obfuscation reject null descriptor.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": null + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-marker.json new file mode 100644 index 0000000000..67a35537ed --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-marker.json @@ -0,0 +1,46 @@ +{ + "name": "obfuscation-reject-null-marker", + "description": "obfuscation reject null marker.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": null + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-salt.json new file mode 100644 index 0000000000..84350ae9e2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-null-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-null-salt", + "description": "obfuscation reject null salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": null + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-plaintext.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-plaintext.json new file mode 100644 index 0000000000..94059a2655 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-plaintext.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-plaintext", + "description": "obfuscation reject plaintext.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flag": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-digest.json new file mode 100644 index 0000000000..0e2f913c12 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-digest.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-short-digest", + "description": "obfuscation reject short digest.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-salt.json new file mode 100644 index 0000000000..2b54a3938c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-short-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-short-salt", + "description": "obfuscation reject short salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "00" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-string-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-string-marker.json new file mode 100644 index 0000000000..1c1a8bf92d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-string-marker.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-string-marker", + "description": "obfuscation reject string marker.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": "true", + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-unknown-scheme.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-unknown-scheme.json new file mode 100644 index 0000000000..7e98d9e894 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-unknown-scheme.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-unknown-scheme", + "description": "obfuscation reject unknown scheme.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "unknown", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-digest.json new file mode 100644 index 0000000000..4d53d3e8c2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-digest.json @@ -0,0 +1,59 @@ +{ + "name": "obfuscation-reject-uppercase-digest", + "description": "obfuscation reject uppercase digest.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-salt.json new file mode 100644 index 0000000000..2d89321ff3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-reject-uppercase-salt.json @@ -0,0 +1,50 @@ +{ + "name": "obfuscation-reject-uppercase-salt", + "description": "obfuscation reject uppercase salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090A0B0C0D0E0F" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "react-native", + "reason": "Reject invalid or unsupported encoding before installation." + }, + { + "platform": "unity", + "reason": "Reject invalid or unsupported encoding before installation." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-rotated-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-rotated-salt.json new file mode 100644 index 0000000000..ba16c9a0ce --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-rotated-salt.json @@ -0,0 +1,259 @@ +{ + "name": "obfuscation-rotated-salt", + "description": "obfuscation rotated salt.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "e9da87d1d051b4e9ae47e00e97c87a11d939a8746280eed92f475a9fb495ae18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb0951b2113f60f04611546a5ec4c7e7e12cabc968eff59362aaeecc6d683fa9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd79ba8d71066ff40da24e6ed335f7669113d17fef540cde96044c03411af1c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a2d89601f57c605c641d06d9bbe736d7e7873744ada2beecff36c051d67c1114": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "65675bf54578b33d06926dc6dbb43abe39d12458b7f7202d72d22d3cf294e63d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2d84852642a347b30811707354eed8d3ea564967f46d549be94b4c58e53c78c8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd982551b4c9c73d5a4d857288e0889e2641a71ea2be43767284d29348399139": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f43ee46b2cdf25f71c542e857876b885b68295fe1845d7f3ea83f32f334c276c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "ffffffffffffffffffffffffffffffff" + } + } + } + }, + "evaluations": [ + { + "flag": "flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-0", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "Flag", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-1", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": " flag ", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-2", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "caf\u00e9", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-3", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "cafe\u0301", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-4", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "\ud83d\udeb2/\u65d7", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-5", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "a\u0000b", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-6", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "value-7", + "reason": "TARGETING_MATCH", + "errorCode": null, + "variantKey": "variant" + } + }, + { + "flag": "e9da87d1d051b4e9ae47e00e97c87a11d939a8746280eed92f475a9fb495ae18", + "variationType": "string", + "defaultValue": "fallback", + "expectedResult": { + "value": "fallback", + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND", + "variantKey": null + } + } + ], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [ + { + "_count": 1, + "flag": { + "key": "flag" + } + }, + { + "_count": 1, + "flag": { + "key": "Flag" + } + }, + { + "_count": 1, + "flag": { + "key": " flag " + } + }, + { + "_count": 1, + "flag": { + "key": "caf\u00e9" + } + }, + { + "_count": 1, + "flag": { + "key": "cafe\u0301" + } + }, + { + "_count": 1, + "flag": { + "key": "\ud83d\udeb2/\u65d7" + } + }, + { + "_count": 1, + "flag": { + "key": "a\u0000b" + } + }, + { + "_count": 1, + "flag": { + "key": "" + } + } + ], + "evaluationEvents": [ + { + "_count": 9 + } + ] + }, + "_include": [ + { + "platform": "ios", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "android", + "reason": "Implements assignment-encoding-flag-key-256-v1." + }, + { + "platform": "flutter", + "reason": "Implements assignment-encoding-flag-key-256-v1." + } + ] +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-unsupported-consumer.json b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-unsupported-consumer.json new file mode 100644 index 0000000000..94894f1bc6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/cases/obfuscation/obfuscation-unsupported-consumer.json @@ -0,0 +1,47 @@ +{ + "name": "obfuscation-unsupported-consumer", + "description": "obfuscation unsupported consumer.", + "context": { + "targetingKey": "fixture-subject", + "attributes": {} + }, + "response": { + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + }, + "evaluations": [], + "expectations": { + "noUnmatchedEvents": true, + "exposures": [], + "evaluationEvents": [] + }, + "_include": [ + { + "platform": "react-native", + "reason": "Encoded assignments are unsupported; reject rather than install digest keys." + }, + { + "platform": "unity", + "reason": "Encoded assignments are unsupported; reject rather than install digest keys." + } + ], + "expectedResponseError": "INVALID_RESPONSE" +} diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/index.json b/reliability/single-fit/flags/src/test/resources/precomputed/index.json new file mode 100644 index 0000000000..03d2dbe3b9 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/index.json @@ -0,0 +1,65 @@ +[ + "cases/emissions/all-doLog-false.json", + "cases/emissions/duplicate-evaluation.json", + "cases/emissions/serial-id-on-exposure.json", + "cases/evaluator/all-types-success.json", + "cases/evaluator/defaults-and-emission-gates.json", + "cases/evaluator/empty-flags-response.json", + "cases/evaluator/falsy-values.json", + "cases/evaluator/integer-precision-native-only.json", + "cases/evaluator/multi-flag-set.json", + "cases/evaluator/multiple-reasons.json", + "cases/evaluator/numeric-boundaries-float.json", + "cases/evaluator/numeric-boundaries-integer.json", + "cases/evaluator/numeric-boundaries-number.json", + "cases/evaluator/numeric-decimal-float.json", + "cases/evaluator/numeric-decimal-integer.json", + "cases/evaluator/numeric-decimal-number.json", + "cases/evaluator/numeric-float.json", + "cases/evaluator/numeric-integer.json", + "cases/evaluator/numeric-number.json", + "cases/evaluator/object-nested-deep.json", + "cases/evaluator/special-characters-in-keys.json", + "cases/evaluator/type-mismatch-all-types.json", + "cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json", + "cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json", + "cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json", + "cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json", + "cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json", + "cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json", + "cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json", + "cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json", + "cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json", + "cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json", + "cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json", + "cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json", + "cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json", + "cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json", + "cases/obfuscation/obfuscation-keys.json", + "cases/obfuscation/obfuscation-reject-false-with-descriptor.json", + "cases/obfuscation/obfuscation-reject-missing-descriptor.json", + "cases/obfuscation/obfuscation-reject-missing-marker.json", + "cases/obfuscation/obfuscation-reject-missing-salt.json", + "cases/obfuscation/obfuscation-reject-nonhex-digest.json", + "cases/obfuscation/obfuscation-reject-nonhex-salt.json", + "cases/obfuscation/obfuscation-reject-null-descriptor.json", + "cases/obfuscation/obfuscation-reject-null-marker.json", + "cases/obfuscation/obfuscation-reject-null-salt.json", + "cases/obfuscation/obfuscation-reject-plaintext.json", + "cases/obfuscation/obfuscation-reject-short-digest.json", + "cases/obfuscation/obfuscation-reject-short-salt.json", + "cases/obfuscation/obfuscation-reject-string-marker.json", + "cases/obfuscation/obfuscation-reject-unknown-scheme.json", + "cases/obfuscation/obfuscation-reject-uppercase-digest.json", + "cases/obfuscation/obfuscation-reject-uppercase-salt.json", + "cases/obfuscation/obfuscation-rotated-salt.json", + "cases/obfuscation/obfuscation-unsupported-consumer.json" +] diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/all-doLog-false.json new file mode 100644 index 0000000000..11f45b7b77 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/all-doLog-false.json @@ -0,0 +1,33 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.silent.bool": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.string": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.silent.int": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/duplicate-evaluation.json new file mode 100644 index 0000000000..fe351c47c7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/duplicate-evaluation.json @@ -0,0 +1,17 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.dedup.flag": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/serial-id-on-exposure.json new file mode 100644 index 0000000000..fec74dab07 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/emissions/serial-id-on-exposure.json @@ -0,0 +1,43 @@ +{ + "data": { + "attributes": { + "flags": { + "serial.id.flag": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "no.serial.id.flag": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "zero.serial.id.flag": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "null.serial.id.flag": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/all-types-success.json new file mode 100644 index 0000000000..1151852b05 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/all-types-success.json @@ -0,0 +1,55 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.enabled": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.title": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.limit": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.ratio": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.config": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/defaults-and-emission-gates.json new file mode 100644 index 0000000000..5a3b68e179 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/defaults-and-emission-gates.json @@ -0,0 +1,33 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flutter.fixture.silent": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "flutter.fixture.string": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flutter.fixture.unknown": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/empty-flags-response.json new file mode 100644 index 0000000000..001a3f4efa --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/empty-flags-response.json @@ -0,0 +1,8 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {} + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/falsy-values.json new file mode 100644 index 0000000000..88766371b0 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/falsy-values.json @@ -0,0 +1,49 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.falsy.bool": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.int": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.float": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.string": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.falsy.object": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/integer-precision-native-only.json new file mode 100644 index 0000000000..6e173e5408 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/integer-precision-native-only.json @@ -0,0 +1,17 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large-int": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multi-flag-set.json new file mode 100644 index 0000000000..1d718813c5 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multi-flag-set.json @@ -0,0 +1,411 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.large.flag-01": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-02": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-03": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-04": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-05": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-06": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "fixture.large.flag-07": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-08": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-09": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-10": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-11": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-12": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-13": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "fixture.large.flag-14": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-15": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-16": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "fixture.large.flag-17": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-18": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "fixture.large.flag-19": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.large.flag-20": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.021": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.022": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.023": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.024": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.025": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.026": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.027": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.028": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.029": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.030": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.031": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.032": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.033": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.034": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.035": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.036": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.037": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.038": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "flag.set.039": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.040": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.041": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.042": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.043": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.044": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.045": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.046": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.047": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "flag.set.048": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "flag.set.049": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "flag.set.050": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multiple-reasons.json new file mode 100644 index 0000000000..5d144285a4 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/multiple-reasons.json @@ -0,0 +1,33 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.reason.targeted": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.reason.default": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "fixture.reason.split": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-float.json new file mode 100644 index 0000000000..393544ac6b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-float.json @@ -0,0 +1,185 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-float-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-float-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-integer.json new file mode 100644 index 0000000000..f44c18a9f4 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-integer.json @@ -0,0 +1,185 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-integer-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-integer-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-number.json new file mode 100644 index 0000000000..52081f2990 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-boundaries-number.json @@ -0,0 +1,185 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-boundaries-number-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-12": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-13": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-14": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-16": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-17": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-19": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-boundaries-number-21": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-float.json new file mode 100644 index 0000000000..fbbedcdf95 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-float.json @@ -0,0 +1,97 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-integer.json new file mode 100644 index 0000000000..466c9658df --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-integer.json @@ -0,0 +1,97 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-number.json new file mode 100644 index 0000000000..c38c806da6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-decimal-number.json @@ -0,0 +1,97 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "decimal-rounding": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "near-one-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "large-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "larger-fraction-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "inexact-whole-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-integer": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "underflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "overflow-double": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-float.json new file mode 100644 index 0000000000..f7c0609836 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-float.json @@ -0,0 +1,105 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-float-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-float-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-integer.json new file mode 100644 index 0000000000..caf983bdb3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-integer.json @@ -0,0 +1,105 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-integer-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-integer-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-number.json new file mode 100644 index 0000000000..e984d6e70e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/numeric-number.json @@ -0,0 +1,105 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "numeric-number-0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-6": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-10": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "numeric-number-11": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/object-nested-deep.json new file mode 100644 index 0000000000..3cc336b08e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/object-nested-deep.json @@ -0,0 +1,35 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.config.deep": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/special-characters-in-keys.json new file mode 100644 index 0000000000..288615a148 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/special-characters-in-keys.json @@ -0,0 +1,41 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "com.example.feature.enabled": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "my-team--feature-toggle": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "legacy_flag_name_v2": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "mix.ed-key_style.v1": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/type-mismatch-all-types.json new file mode 100644 index 0000000000..e0a504bedf --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/evaluator/type-mismatch-all-types.json @@ -0,0 +1,51 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fixture.mismatch.bool-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.string-as-int": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.int-as-float": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.float-as-bool": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fixture.mismatch.object-as-string": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json new file mode 100644 index 0000000000..3d88efdbe7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-all-doLog-false.json @@ -0,0 +1,38 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "84a2ac6cb08b06570e965cb98d55a0a83b415e1810ef3f4e103cd8fcde14ad7d": { + "allocationKey": "alloc-silent", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "2fa983b9b25a656f30c3c1ca3547327d0c91abb8dd5a968cbf737fe8b4def524": { + "allocationKey": "alloc-silent", + "variationKey": "greeting", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": false + }, + "94501ab4c2a58e69a11081f73e5f61a1f37c9c88fa95e61555a9d14ee1dd4024": { + "allocationKey": "alloc-silent", + "variationKey": "limit-ten", + "variationType": "integer", + "variationValue": 10, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json new file mode 100644 index 0000000000..fed8a7520b --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-duplicate-evaluation.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "491646f7138ee482b2ace2d2834ff8ff34b20c8ba8cd329156857ab1be6154a6": { + "allocationKey": "alloc-dedup", + "variationKey": "variant-a", + "variationType": "string", + "variationValue": "alpha", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json new file mode 100644 index 0000000000..d77ab75ac2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/emissions/obfuscation-serial-id-on-exposure.json @@ -0,0 +1,48 @@ +{ + "data": { + "attributes": { + "flags": { + "8c7c04076517d2c3c0633fda9a6664d4b114cb359cc512dd38c4ef69373032fd": { + "allocationKey": "holdout-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 340132 + }, + "36d69e5a88de3b456cd6bfca1070ee06c667c9c94d7de9022616661c71928e50": { + "allocationKey": "regular-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c7eac87f0bcefab56284d70561f954ffccd2f88fcd895067ffc94832023d3be": { + "allocationKey": "zero-allocation", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": 0 + }, + "6597def4cb332745af4ade37ee996a8958091ccadc58082c2c49c4db66c24e98": { + "allocationKey": "null-allocation", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true, + "serialId": null + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json new file mode 100644 index 0000000000..cba53f8d01 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-all-types-success.json @@ -0,0 +1,60 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "657bbcc45263793d67e4f7967fdd257d6e1896cc095d758c520ca3f52beca003": { + "allocationKey": "allocation-success", + "variationKey": "enabled", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "abf42b7b192cf3a719bb8ea77c9ff652c82bd4c7aba54561e374efeb2fbbb9b7": { + "allocationKey": "allocation-success", + "variationKey": "copy-a", + "variationType": "string", + "variationValue": "Datadog Flags", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "820dab9a17046e7bf33d080bcb6f44547e1b92c31b36e7d9dcfde96c46bb5d56": { + "allocationKey": "allocation-success", + "variationKey": "limit-five", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f4d559502705ccaba0a55b0defc37636fe7ed397a477a07389771c07183f139b": { + "allocationKey": "allocation-success", + "variationKey": "half", + "variationType": "float", + "variationValue": 0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "41057f3677d1a6c229d214c630b71e9f499abbb68116d50594561663f6ec09d9": { + "allocationKey": "allocation-success", + "variationKey": "object-a", + "variationType": "object", + "variationValue": { + "showBanner": true, + "colors": [ + "blue", + "green" + ] + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json new file mode 100644 index 0000000000..f2421d04e3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-defaults-and-emission-gates.json @@ -0,0 +1,38 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9176965ed24adf9f9e56f8918f3e4f7d5178ab57ca0fdb3e32266a4203930e49": { + "allocationKey": "allocation-silent", + "variationKey": "silent-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "f83c743fa9fc2faf137298f504966877bbe7d23e7cb9380c4b73188b764b0a0c": { + "allocationKey": "allocation-string", + "variationKey": "copy-b", + "variationType": "string", + "variationValue": "Actual string", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1345780c7d221b7c1798057d61113f43466cb92b31ecb05fa74ee4b511d7c482": { + "allocationKey": "allocation-unknown", + "variationKey": "unknown-a", + "variationType": "unsupported", + "variationValue": "Ignored", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json new file mode 100644 index 0000000000..9d945d60c2 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-empty-flags-response.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json new file mode 100644 index 0000000000..dd29ce980e --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-falsy-values.json @@ -0,0 +1,54 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "fc0d169622e17ab8efff1da7fe0d0ff124de893d2e723fb9a8c632a87f5617e2": { + "allocationKey": "alloc-falsy", + "variationKey": "off", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7a856abf0d6b1d857471bdfe3f81e7e0c08a1c7da13315ffd303a60e40032c8d": { + "allocationKey": "alloc-falsy", + "variationKey": "zero", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8e902056a9d96a536e0300af89c407afe670d48fac2ae3ed9c4f0805579011f0": { + "allocationKey": "alloc-falsy", + "variationKey": "zero-f", + "variationType": "float", + "variationValue": 0.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7aa973f2d2a8f5ff69c54faad155bbad70b0c2733f20d10c0ed170749ce60f3c": { + "allocationKey": "alloc-falsy", + "variationKey": "empty", + "variationType": "string", + "variationValue": "", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5273f6912bea9ecac5cd7d11674e39b208fa0bc796a3379e5b46138a4d2771a3": { + "allocationKey": "alloc-falsy", + "variationKey": "empty-obj", + "variationType": "object", + "variationValue": {}, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json new file mode 100644 index 0000000000..b5faca838c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-integer-precision-native-only.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "60c239d3d5039428009ffc2a7918ccbd9c6485ee75e7d3130b148103d3e784ae": { + "allocationKey": "allocation-a", + "variationKey": "large", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json new file mode 100644 index 0000000000..cadee14bfd --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multi-flag-set.json @@ -0,0 +1,416 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bf9be622ca7517e32cd02c784169beb8e5102892504fdad8384e348d48e36879": { + "allocationKey": "alloc-l", + "variationKey": "v1", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "75dcd247147ab21d09a4fc73051324b2000bd0bf97d82d35ad6d80eaa6d07973": { + "allocationKey": "alloc-l", + "variationKey": "v2", + "variationType": "string", + "variationValue": "two", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32681eee3e6ebf4ebaa0b03b91638cbae0920b26239bdea4fa2b7d0bb3a46df3": { + "allocationKey": "alloc-l", + "variationKey": "v3", + "variationType": "integer", + "variationValue": 3, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "792f76037d724266f9c00e971967bbfe62cba43274b31f652b9768d6381e7ace": { + "allocationKey": "alloc-l", + "variationKey": "v4", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": true + }, + "59bf45e79a60fa4a33f2d4c639ef910982d3d3e29ca2fb9c147fb4977569f35a": { + "allocationKey": "alloc-l", + "variationKey": "v5", + "variationType": "string", + "variationValue": "five", + "reason": "SPLIT", + "doLog": true + }, + "fce91fe1f119ec27d34a9cb77ab5014d49153c8afa8162b282c9c595591ff130": { + "allocationKey": "alloc-l", + "variationKey": "v6", + "variationType": "float", + "variationValue": 6.6, + "reason": "TARGETING_MATCH", + "doLog": false + }, + "086529987ece3ae0319c12332405bbec6e6eb913266ade8c8fb33b2b7b89ab5f": { + "allocationKey": "alloc-l", + "variationKey": "v7", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1d89f79a25e4eae7f080cb86d35af66fd2147336f6c450dcd40d23333f83d888": { + "allocationKey": "alloc-l", + "variationKey": "v8", + "variationType": "string", + "variationValue": "eight", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0fe82d095e9c1dbefdca57b310ab4c19c80439c87a41b0eef548685914df239d": { + "allocationKey": "alloc-l", + "variationKey": "v9", + "variationType": "integer", + "variationValue": 9, + "reason": "DEFAULT", + "doLog": true + }, + "1ac48586eced16b0aff76e17312da68f2c43fd3cf9ca33d76f3e427f93797c4b": { + "allocationKey": "alloc-l", + "variationKey": "v10", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cc6234a3430646b1df0c9e2bcca80dfe16b05abcca88d58f5f97c19a14a9a320": { + "allocationKey": "alloc-l", + "variationKey": "v11", + "variationType": "string", + "variationValue": "eleven", + "reason": "SPLIT", + "doLog": true + }, + "215f50d3c5e5908c31d2bed80414ce26ba68b264ea5baa4ab5b743e1833c1945": { + "allocationKey": "alloc-l", + "variationKey": "v12", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "88c23388c4cec50c57429698b96c975be9f8915f88308fba6bda54acc6d01998": { + "allocationKey": "alloc-l", + "variationKey": "v13", + "variationType": "boolean", + "variationValue": false, + "reason": "DEFAULT", + "doLog": false + }, + "b66bc4f899fa5ad230fa050a3ba35ac5c5cda22bebecbb44e6c63afddbaf7b5b": { + "allocationKey": "alloc-l", + "variationKey": "v14", + "variationType": "string", + "variationValue": "fourteen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "83ab766d8522b069512a37e2a5bd8bd8f941fbeef306eab09f6247e8db3b725b": { + "allocationKey": "alloc-l", + "variationKey": "v15", + "variationType": "integer", + "variationValue": 15, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "db1c7d8f6ac65f0e493bb4c3a87f153ec6cac18bd0c984b20ff09619ff79cce9": { + "allocationKey": "alloc-l", + "variationKey": "v16", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "4d23aa03aec589f62d46d15642b79f6e3c87e3aefa5b49cd319304f4a013ac95": { + "allocationKey": "alloc-l", + "variationKey": "v17", + "variationType": "string", + "variationValue": "seventeen", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "023b47247033e8bea815ff0cc16255bebdff082b3cfb1eeb4a9b55b3004276e1": { + "allocationKey": "alloc-l", + "variationKey": "v18", + "variationType": "float", + "variationValue": 18.0, + "reason": "DEFAULT", + "doLog": true + }, + "0b2b70d8c98c0731692191e78b527512e548190d97d710d7f0fc8f19cd05913d": { + "allocationKey": "alloc-l", + "variationKey": "v19", + "variationType": "boolean", + "variationValue": false, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9c1796f3cb195313519a837ead5d54e083195b6206e189bbdd0220561b4ca766": { + "allocationKey": "alloc-l", + "variationKey": "v20", + "variationType": "object", + "variationValue": { + "count": 20 + }, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f73dce8e960f6d67023c407ac490ce3605b279288b56f78031bfed36f0014935": { + "allocationKey": "alloc-21", + "variationKey": "var-21", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "c078d7577eff8d7a6fc8963d0bd16fbb172c3a4a3aeb9e714c75a2574d7967e1": { + "allocationKey": "alloc-22", + "variationKey": "var-22", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6326ec0ed3c2465e7cc807f46c743037e7095d3f8e9d1d56dbaea76b2b00452b": { + "allocationKey": "alloc-23", + "variationKey": "var-23", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "3d3d982d7910fda9264321566138165779600cddbf42d215d689080b617b3fd0": { + "allocationKey": "alloc-24", + "variationKey": "var-24", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "5a80d2d736721d87db810627614f4f6b127ee0a332f5fdea8550a78e3ec4e699": { + "allocationKey": "alloc-25", + "variationKey": "var-25", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "86edc8f5b58892c55f5fde77e66cdc7b6d0a9b071e2677d8fa16dbb8f409f380": { + "allocationKey": "alloc-26", + "variationKey": "var-26", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "cc6b19cce7d683983134195f55a0c6bf3a5f844944e283cda1c58485b5a2dd2a": { + "allocationKey": "alloc-27", + "variationKey": "var-27", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "4c2f7e12bea6b91c5d5c9862de0b24418f6fde074eb5e017c93293cfabf60060": { + "allocationKey": "alloc-28", + "variationKey": "var-28", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5a48e1a5c7c8e185b62cd259351f4abe443771f07292eefbd49f0599881e35ce": { + "allocationKey": "alloc-29", + "variationKey": "var-29", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "cf6321f08df13c06652debb11c4cb9ea67e215e5a364d845f30d865554876b4d": { + "allocationKey": "alloc-30", + "variationKey": "var-30", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "1660da21238865ae03e7c5c4959d02a34bc210101d7608f7221f97f254b801d0": { + "allocationKey": "alloc-31", + "variationKey": "var-31", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "34529807d59d7bf904c56e17e112ed555687ee05a332a9ef957d7f029870ed57": { + "allocationKey": "alloc-32", + "variationKey": "var-32", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "f8fcf4b3a145194a19daabe0521e6cfea4bac6b20a6fd729f4c8c3ad6ac3e6f6": { + "allocationKey": "alloc-33", + "variationKey": "var-33", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "210a549229e78c66c588dffc37d99918e7968a04876fa87cd1b719a5dfe1edf2": { + "allocationKey": "alloc-34", + "variationKey": "var-34", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ffd5bbbf90dd7c225ddc95b0c1134cee255c5d1ee7f7889bf2c239b99dd43db4": { + "allocationKey": "alloc-35", + "variationKey": "var-35", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "929200b0fd1248b310ac0f0785a5a3ee3b1aa40cbd4cc7d7ee7c141bd57c839b": { + "allocationKey": "alloc-36", + "variationKey": "var-36", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "4aefe68d39be982605014ac90b746de9be85b4bea79cacc26459cdc06b96f48f": { + "allocationKey": "alloc-37", + "variationKey": "var-37", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ba21a1d31e1f9074d322885263107157affa2265582bf6a05f84441b8c118eb": { + "allocationKey": "alloc-38", + "variationKey": "var-38", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + }, + "77581b9776c4d5afa1919a1e0c758067d07c3968afa2201870cea6dae6579aa3": { + "allocationKey": "alloc-39", + "variationKey": "var-39", + "variationType": "integer", + "variationValue": 42, + "reason": "DEFAULT", + "doLog": false + }, + "03461ca38265adbdcede804d0ba7a544dce9c4de6296eb32d8a6683096a9b1ce": { + "allocationKey": "alloc-40", + "variationKey": "var-40", + "variationType": "float", + "variationValue": 1.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "92325ac72dff844afbdc8a8a8ca939b673fbb452cd67d7b970990c58596baf57": { + "allocationKey": "alloc-41", + "variationKey": "var-41", + "variationType": "boolean", + "variationValue": true, + "reason": "SPLIT", + "doLog": true + }, + "934025c0b96e851d348e09166362375a53e2776f595da60d4e2fde30f1503baf": { + "allocationKey": "alloc-42", + "variationKey": "var-42", + "variationType": "string", + "variationValue": "value", + "reason": "DEFAULT", + "doLog": false + }, + "f6d270502e6f9fc1f4cfa7fbcc78e2b3377d9fb480f23e864cd4be0c96ec3467": { + "allocationKey": "alloc-43", + "variationKey": "var-43", + "variationType": "integer", + "variationValue": 42, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8bb8e9b84c7ab5397d4012c8d58cb797a8fac7562c86ede747d91f5971bfef10": { + "allocationKey": "alloc-44", + "variationKey": "var-44", + "variationType": "float", + "variationValue": 1.5, + "reason": "SPLIT", + "doLog": true + }, + "81be2644335d094121dc49dafd671c0c212980253962dd2f24986736cdaf9583": { + "allocationKey": "alloc-45", + "variationKey": "var-45", + "variationType": "boolean", + "variationValue": true, + "reason": "DEFAULT", + "doLog": false + }, + "b4fa3c55cb8c05baba5ca43eac146e6036b0028e2fb07eafd6819d3935c9e4bc": { + "allocationKey": "alloc-46", + "variationKey": "var-46", + "variationType": "string", + "variationValue": "value", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a56d4714d20368ac54afa5225b1dec452d9bc102f604fd54bd338e4b693bda38": { + "allocationKey": "alloc-47", + "variationKey": "var-47", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + }, + "e5b919481d265c3573d94171004324cff98c4e223efd7b568b028ffcd4314a6e": { + "allocationKey": "alloc-48", + "variationKey": "var-48", + "variationType": "float", + "variationValue": 1.5, + "reason": "DEFAULT", + "doLog": false + }, + "370b0fcd16be8003ef1d1e1f39e7e19b3698f17a24957ed654420be0f81eddc5": { + "allocationKey": "alloc-49", + "variationKey": "var-49", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0721adc36960473f00919092dc06adc2988d409a72f5244bc47a4b11de285660": { + "allocationKey": "alloc-50", + "variationKey": "var-50", + "variationType": "string", + "variationValue": "value", + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json new file mode 100644 index 0000000000..83d8cda9af --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-multiple-reasons.json @@ -0,0 +1,38 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "19f9605f249b328efe6542c0a39acdaa7a0b39c61daccb3883f2cf3c5705e9a3": { + "allocationKey": "alloc-target", + "variationKey": "targeted-on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e433cf607656e33013abf122d4f7576389f0acf3ea88f085fd069bd839390a4f": { + "allocationKey": "alloc-default", + "variationKey": "default-val", + "variationType": "string", + "variationValue": "baseline", + "reason": "DEFAULT", + "doLog": true + }, + "212fded283b3498c3742391c9db4843ff32aad02ded7646c68dd14bc008f3f03": { + "allocationKey": "alloc-split", + "variationKey": "bucket-a", + "variationType": "integer", + "variationValue": 42, + "reason": "SPLIT", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json new file mode 100644 index 0000000000..fae798790f --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-float.json @@ -0,0 +1,190 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "35dd7544a6b9a875c5074a893aa3d5c95f015b6cff7f07f30390bc90b939839e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "54b3e4e77e354d4e049991350fe68ce1a90ba4e874933a9e269d5b99cb3ad115": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6cec42afe677ad01472e4754a611106037ddb8e786570bf9a730630b8d2f7f30": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8de4314e7ce11a9f64a3490e59968a1130f56b15c39276bdff9ebc344402101d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4182dc078ece15bc1c45cabf6e2ab2ad0b7acaf7f42eaa3ad63fe3956beb1db7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ec7fd610e215fd5d177ffa68bec26d50a871bc1cf0114636a734e1660bd6411e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "53d34567f5337f1104d86a9e821df4511fa74a9cc03516f426c9ab4c7af0ac52": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3e69e701cc02087f7897b6f4832e8be0be9ca155876f8cb95597e6ad32b1e2ab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd1a3e85e06626dbb1c56432a8dea7a0128284f1d781013835a2564e70f87d98": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a212f6d288564e940e009835514eaad55a3d62caada53dd1fdda1b2475efed0a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f8dc9d43242ec89a4db2212c3a70ee9c938b0cb36afac45b2000cd7c77bf7609": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1e20092aa2b67b3e8ed9d5533a2260ff177207d9b86fac2151864b2d7af492e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "45f0d04656e98eb32b8221714cf169d57479d2be4af6b96c5ffe0f9fd589ad0a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "680bb5138700a197f7896709b20d6a451854dfaf7fd2016374154dc214101f96": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5f0e609737393c76014f125cfe1d8b685065b3f8bb2833e393f4de4bb3f271fd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6de38d27b71396e8169bdf012bf53a9f6112b4f9533e492d1d0c372aa5b2b89a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e68a8c78bf6d545e6c949e88209611b6615ddeea9817440de9fd3fb7a289c335": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6d6386fa18188d57ad46d118b937d77a1a8d814e3ff723871452b9d5f6abd03": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9d28b9ca14aed8cd8edab7144dc09ff50ff608f1676df0c92be1a225fc4c95b4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e026139b2c1ef067fcd194745a316b948ca17d4224a2c6ef0f54e42739a6f9e5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9ad722f4aa0e6cbe77da0e6406a3f40ac0dbe3f556a524d1d504115896235fed": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "56cd8c449d33e0a19ca9b0d300b40bd994dfd1f4f3b16d093e453d8a97a3645f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json new file mode 100644 index 0000000000..8ec4120f92 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-integer.json @@ -0,0 +1,190 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "dba4f4d05c42d94f2f0fe7fb4c5622432cbace9983c4f2bcd81dba39a6327250": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c053d531267e94db3094b514ac132333832cf45cc102899e23a317cfd1b86c7c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cc6b41d8abf103fc2c0903beb89ea4916203e65e34f56f229bd4bfd30fca2b44": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2cd758e8f6b3d6069426eaf4fe83cad350062d6f3cf9fdff78af6814151ff35f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "be1e08347c503a3116b8872a98f762ce80f8b2d5d9712855f553e9a8e41a0e24": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "439b8c652a5b3d6bb6a8b5601f86c09f38f1ca344dfa0cd10904c41d3a0089ba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "52c0be60636eb11b987e1729b894a63f02463a408660d632004cd68b66c17966": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32ac57434b06ca630219caa81b6d6c685138920a8a9d1e6205055cff023f3179": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "21bae751d95202679767297ac5a668ff2588c260e0c1d93e0ddb494a3c6c44d9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ceb204983395689386573ade9d3496ac4c07fde14fd7dbc965b51654af834df1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c2b69fdede340468e31ce79ee511016e1f3f15fceaf2228fb6bde3c034cb14d9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0a1f9c3aedb3a5721f4215c26cd403a7bca6d5fcba088d6ff8c5eb58a4168903": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cf564971ee029d99592fad8d056f2d1cb13ff91e3e283dc2c16b3a644db90734": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "482ab354d08783942a1cc1613d261773481b27647304272c9a895b3f82aef81b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "8ae623d95ec069f4263033a5c3c63cc41c8369a4ada5c2df1e72feb3472b3298": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3ee628175449c6db9aeeb2b77f155f2af7f566ae1209ed32f564b4e74d37ffbe": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1aaaeb79098a7feede64e211e53f192295e0d97db557223f5945bd869a9ec82c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1a9a9de8e21c848b2bfec22d14c770d8a3a176dd1ce39955a5b165bf1f851590": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c2dca5f665b034d94acdc589d0b6646e583561b3f05aeba0a0b6399297c8ccf1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a014f0066d631022d1c7628bd1b990291f772710900955e0aab48543db7c3b2a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "05ab2bff8b65224aff72672e380d66e022ff5f0c1b76e3231aaf172f6c82d467": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a10340fc40240dde6ba540065e6526b8c66b6fd78d116451b42201a9ba4c665d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json new file mode 100644 index 0000000000..b84498290c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-boundaries-number.json @@ -0,0 +1,190 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "4151b6612247033495b8370b980c847c1efe5dc3ddb008a57aaf9e8afb97694d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c3c464e47d3e87d433d2a0b6a5ea9e9cea9ffb65c31a7022fc4be4c51a3e3663": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "d9adf2748e7c2bb330f6b321204a4ab3d61aa555dbeaddcb3d79fb20383e3840": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a3e931eb60a9b70284b948912691bbf4e1f757d1f52c90d1623413a4ed05eaf7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "56a5f99336ac7daaa327d074e3c1cf1389f3b0e198d8b6b0dc4a0773f37466b0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "996e91f621b0091b21aaceea06330f3bcc8a4639939a0b3cafd38cdd2d732bde": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483647, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5b95fc3592f62934e978d8f18e998120d1012da96a4ae3c7a3bc832442914bce": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fb8ab09c443a1fef7f78aa00069193131bdddd5f754c69837da77dde8bd53ab4": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "13bc44aee0e07075a5f61e06bea48f9c7d7cfe240bbcf45d9922127c5fcbf57a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -2147483649, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2b8ee396fbeba43a943135d2ea6962f4eee41f3af704609313b6241bf4911f20": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9dec15ccf9649edff360ec3a223801aa6f804087a939b1d14b686a7fe34a0cbc": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1aedbf12de210a49f962e5e8db09a065c2b328a7d2c48ce4c6e919325b9c0c5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 2147483648, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4a989c369519e210c4857cd50fd525e571cc89b7da322276ba05487c15b4849f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "313de806bdcd55e96b13d12c28260bc5238057d4ed2a8ecc0294f8c7e234fa01": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740992, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1f5409b64eb99b424819230ddfb333b5f399a911840ab6b36f0a0938a438af25": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "61f7deca99e29b35ab18cdf82001dcf64ea3b04caaa9fc751d835f1c70d25fd2": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "06503c386b2c84e0299e7a692baecdecf2b19698b90f13b89df7d4e309d43870": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6d9de4417c04ad9908dc846b1a9ae5c6c2fe214e7aca7e2dd3480d82abd46a5f": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b553f209415af2ef3dd59e7f7383e270e5090b9811e319c39645a51d58ec26c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "4ccf6d4d18107b748ae8ec301e66adb190f15bd77e3bf23246d8994787c8a40c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9223372036854775807, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9de54af153484d9db86d593c9a0c9443f7c5a88881964452b69b7f2d8a878c01": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "52b56cf56113aee71339a1256252ae52741aa5e666d2a5ca3ae3ad194cbb83f5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -9223372036854775808, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json new file mode 100644 index 0000000000..c919307c75 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-float.json @@ -0,0 +1,102 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json new file mode 100644 index 0000000000..17a5312cc3 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-integer.json @@ -0,0 +1,102 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json new file mode 100644 index 0000000000..a94212a030 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-decimal-number.json @@ -0,0 +1,102 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "bddd309e37dc139a619f8462b00bf8c06ee0d0374480577ee730dd326273a854": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.20000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6302058db808f41842729e01d1bf4a93e154bed094c7f40bddfec81cfc9dac3d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6ebdf7e0d68f12baa53f5aa781c3208af6220453a1102c7e1b067462631a5b15": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.00000000000000001, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9db8c418e203825e3e669c6f4ccf253012f88883a92dba2e74f5f66f6d905ee9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "5402822670a559407a74e519d884ab68e9bf3c8333ca48c9796c32f1b9985419": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 4503599627370497.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb9e0426d2cb9cd6bffbeb73f64356ccb5474e8406b492ac97a60d495c7aeb26": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1b60a6e0a0fdcce4aaf1bccf57d86c0df1148a97a029e043d76f8947eb38762b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ce4b4417d9c1a8af068640325064a37de221e83b9bd70a6e7ed3909781e2b764": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 9007199254740993, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a6fb31b5d33413363a857b23ab6d441123b2dcb20f3844039473b0a8a0326acd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "32e894504a432076acee0526876de94b12022f76f08656b047bd637acf212083": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e-400, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a1e6b551217a818f8362d88e0f47cb4087dd85b1af41bed8f49b88823b0f167c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1e400, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json new file mode 100644 index 0000000000..556d44392d --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-float.json @@ -0,0 +1,110 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "6711bb1e8b093b846ea439322c667bce9ca0568b0f0d4320e05c236c9ead11ce": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "dd28ccc7dc267c0927246a422010adbf6e81bb5b0e8fa6a3f9c5c999dcfa8cac": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9662b5e2c026477337a6a1b952ca1da98116ea3393c14ad81bf3b0e6f7705eae": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6a126c45f099dc1465fb20627b24f64632aae36028757b78b361873c454d566e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7a331b89b73d723922e64da680ad3048d381db62fd6f9139eb9e57ea43569156": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e152b1e1e80ca9edabd8cfd196b0084ee4728b8eb6b85c1de743705b8ec3f1c5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "968e09a8af812ed8d7fc18b21f59d5b7623d5e1ae6fc8f5a141443f026b11aa7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b4b714689b3211e62d2fbde91099071f193e00c955f5a96a6ffa087b2e6bfa98": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a978ec7a2487b240242a05119affc348f41d0d9fbdce334665c4090c2f4ab1fd": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3860419b6dc9dc69573f100ccbf51c008e45a8a9b6a62243ff02b869f4e86b47": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a303e6fa7efedf997c87edfc2d79872c95d3e1a6c5310ccfe65d0826f700ab55": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "41a0cf77c6270e0e6f3941ab2fa80f712b1c916d13793ddd36a99363c07ad885": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "float", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json new file mode 100644 index 0000000000..31926b49cc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-integer.json @@ -0,0 +1,110 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "6f6faa00700939aa0c395e059d8dea1cc48b2c5a20eb2ef5bc7abfeb551e2d6a": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "6418f8b5f4df9814bfd25f30a8ef6865929075706f260ca4f48f5334f5541c89": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "0f2ba6c096112e6b701607e345fc3a06a2a0d93710715cea905546b69b536ec0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ddd11d6e8fd3dfc9734f307630745484bb411a3278c36ad0dd7c681f91edcde5": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1108b8a690b2ca3dd55e74294ed8bfdf1496b88ed4b3cb87abf27199c6b819e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "01ede755b925df7f56ae33a0bcc4d822a0495b602eea740c5b8570c025527afa": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "fc310ab0e0bd16d08c9b769db925a177fa89b3f539d75b01ef8dd319c427fccf": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ad9ca46d7714846bd2a813e74d2af97e1cd25df3b0242f1575db2083f6f9d8b1": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "35eb2ca6e144c429db174c281e9e55d2b9b4e9d819a159203bbdf577354e4033": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "27bc83275f0f9a5e871d2e4c5af47829d9f7b72c3e5f1f224fbafe3cef986c81": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "19415991350c00c660f9d74c206db9ec7d9fbde7c8a8cbdcfd6775911e40be6b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a556037a239ae8b7c3789f59013e015ebf29686de47bce8aa1bb17f99569a4e8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "integer", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json new file mode 100644 index 0000000000..743c0c21bd --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-numeric-number.json @@ -0,0 +1,110 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "5a1d7afff3fd522b50e8891f7af14267bdd253a1455ed12bc92188f78e12033d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "cbc15a556f7e611fa26f06f6a6334ee91ad5d6a344496452557db41ed992bc25": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f1c431305b153a3030fadebc0057e634b6162cd6acda9744a763082b1197c740": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5.0, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7b3cbab857c42d32035705aa891dd7a2fbadcbe1fc0f1e28f3611076a6aa7945": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "acdfe2ec44b3eb617b86da9a49166e3abecbc85c00951e548516cfd748056403": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3af8ba8e93c58c248ed0d943b8bfbc630236bbbeca96be121efc46ed8da4b9b0": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "efb49302883b084e49d51cb2f41fd795224a571d182536e203f6ab28e6648b77": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "c38a503ec9a1c032f0a073e23d6d0ede2eccf245aac33f9db02a5374728c810e": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.25, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "499aef4e9d9d7df120a79816e9406b5925664e851fd638a129f525588e8a9385": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b004cb3fb7dbb1846ba17f461b1a0931cd52d9ffde2c5419d51c0b47e42baf99": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": -0.5, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "d858ab64de6a97983f95792030011b2dd635938aaefd90fee12b37ef67e9dac3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3ece2688265c5c214d2904619cf0294042830dc10d72dae32e50050c7cded8c3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "number", + "variationValue": 1.2, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json new file mode 100644 index 0000000000..09caa7005a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-object-nested-deep.json @@ -0,0 +1,40 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "a75e90efc57ed0e56bd11c3a9b17a2623dcc2a412fa52f3ffa742139fa6b93af": { + "allocationKey": "alloc-deep", + "variationKey": "deep-config", + "variationType": "object", + "variationValue": { + "level1": { + "level2": { + "level3": { + "enabled": true, + "tags": [ + "alpha", + "beta" + ], + "threshold": 0.95 + } + }, + "name": "nested-test" + }, + "metadata": { + "version": 3, + "empty": {} + } + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json new file mode 100644 index 0000000000..5d48cc3da0 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-special-characters-in-keys.json @@ -0,0 +1,46 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "5ed289266d5a488851241b3122aab121698b75a75da9fb687c671d606a099d99": { + "allocationKey": "alloc-dot", + "variationKey": "on", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a5bcd95f28f8cf915795582c1edd5b263d2f42cd8a513eb6d69aeef26c39ead0": { + "allocationKey": "alloc-hyphen", + "variationKey": "active", + "variationType": "string", + "variationValue": "yes", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "e4fb9f7c538eac04c76c225b60d43a48b5f514a112a148d95d25754ad14cc8a8": { + "allocationKey": "alloc-underscore", + "variationKey": "v2-val", + "variationType": "integer", + "variationValue": 100, + "reason": "DEFAULT", + "doLog": true + }, + "dfd73ab3f42a47bd441de225f613432120e3eb38a6f00a21cb19a45a9de23c70": { + "allocationKey": "alloc-mixed", + "variationKey": "mixed-val", + "variationType": "float", + "variationValue": 2.718, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json new file mode 100644 index 0000000000..0180dd326a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/generated/evaluator/obfuscation-type-mismatch-all-types.json @@ -0,0 +1,56 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "937d54653333211a63876dd0e042edb2ef108105fb918420af28ff03f620a982": { + "allocationKey": "alloc-mm", + "variationKey": "var-bool", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "ed1838c062426321f2125dd154000611b46339c11bbbab4228df3fad88007f6c": { + "allocationKey": "alloc-mm", + "variationKey": "var-str", + "variationType": "string", + "variationValue": "hello", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "7ba0090b5e66bc7319950479a1e905c26fedd82e605530cdb4eb0adb4c7d5515": { + "allocationKey": "alloc-mm", + "variationKey": "var-int", + "variationType": "integer", + "variationValue": 7, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1fa87b21c5821755d962ce404e18a06c5ae2656dd33d4e915e80e425fc77c190": { + "allocationKey": "alloc-mm", + "variationKey": "var-float", + "variationType": "float", + "variationValue": 3.14, + "reason": "TARGETING_MATCH", + "doLog": true + }, + "9a172110c6d75715ead4e47f3063d7ab4d92141de6f83681141130da17f9b743": { + "allocationKey": "alloc-mm", + "variationKey": "var-obj", + "variationType": "object", + "variationValue": { + "key": "value" + }, + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-keys.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-keys.json new file mode 100644 index 0000000000..de793f1519 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-keys.json @@ -0,0 +1,78 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "adca75d2141c51b0c0f084c1058edfb8e6e763f91aaf54ca06326d9847bd9586": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "b1a5f851cc82a3fdf03a461d72a2241584dcf455df1d384e02a937901b3bc80b": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "3bfa8c3c17c1b61035b98ecf14007cdf5cba5ce61a48e8cfb65f8106541892d3": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "1e8b7ec5e8028a1ec96b38dd37f6ccea041c0a90358904af40ac5810c23c8765": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "94b611e0d3b26b52f6ad66013d1c72f8a92ea109390049759e75d3c8d3aa4dab": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bac134d201be5e7f28fc7019248f0809c2a013b137e866446ee71add2bc344c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "072d985b427f536ad0a11b2d4c5e0f7e6f0ff6d23e779e082f75599ce3fe3eba": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json new file mode 100644 index 0000000000..5ce3250c47 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-false-with-descriptor.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": false, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json new file mode 100644 index 0000000000..0f57503574 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-descriptor.json @@ -0,0 +1,9 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-marker.json new file mode 100644 index 0000000000..eac48a73a5 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-marker.json @@ -0,0 +1,12 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-salt.json new file mode 100644 index 0000000000..0acbfbe83c --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-missing-salt.json @@ -0,0 +1,12 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json new file mode 100644 index 0000000000..37c42110ad --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-digest.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json new file mode 100644 index 0000000000..524af52021 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-nonhex-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-descriptor.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-descriptor.json new file mode 100644 index 0000000000..aef8d0fe1a --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-descriptor.json @@ -0,0 +1,10 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": null + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-marker.json new file mode 100644 index 0000000000..cbe2dacb61 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-marker.json @@ -0,0 +1,9 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": null + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-salt.json new file mode 100644 index 0000000000..effbc83c55 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-null-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": null + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-plaintext.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-plaintext.json new file mode 100644 index 0000000000..0b52c3be42 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-plaintext.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "flag": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-digest.json new file mode 100644 index 0000000000..e5d562c805 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-digest.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-salt.json new file mode 100644 index 0000000000..a3c39c5dc6 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-short-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "00" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-string-marker.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-string-marker.json new file mode 100644 index 0000000000..e04be21bef --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-string-marker.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": "true", + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json new file mode 100644 index 0000000000..f1bd297feb --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-unknown-scheme.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "unknown", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json new file mode 100644 index 0000000000..9109f07782 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-digest.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json new file mode 100644 index 0000000000..0704f7c6eb --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-reject-uppercase-salt.json @@ -0,0 +1,13 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": {}, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090A0B0C0D0E0F" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-rotated-salt.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-rotated-salt.json new file mode 100644 index 0000000000..53f58a2cdc --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-rotated-salt.json @@ -0,0 +1,78 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "e9da87d1d051b4e9ae47e00e97c87a11d939a8746280eed92f475a9fb495ae18": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-0", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bb0951b2113f60f04611546a5ec4c7e7e12cabc968eff59362aaeecc6d683fa9": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-1", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd79ba8d71066ff40da24e6ed335f7669113d17fef540cde96044c03411af1c7": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-2", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "a2d89601f57c605c641d06d9bbe736d7e7873744ada2beecff36c051d67c1114": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-3", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "65675bf54578b33d06926dc6dbb43abe39d12458b7f7202d72d22d3cf294e63d": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-4", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "2d84852642a347b30811707354eed8d3ea564967f46d549be94b4c58e53c78c8": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-5", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "bd982551b4c9c73d5a4d857288e0889e2641a71ea2be43767284d29348399139": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-6", + "reason": "TARGETING_MATCH", + "doLog": true + }, + "f43ee46b2cdf25f71c542e857876b885b68295fe1845d7f3ea83f32f334c276c": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "string", + "variationValue": "value-7", + "reason": "TARGETING_MATCH", + "doLog": true + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "ffffffffffffffffffffffffffffffff" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-unsupported-consumer.json b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-unsupported-consumer.json new file mode 100644 index 0000000000..0c9c4991f7 --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/responses/cases/obfuscation/obfuscation-unsupported-consumer.json @@ -0,0 +1,22 @@ +{ + "data": { + "attributes": { + "createdAt": "2026-01-01T00:00:00Z", + "flags": { + "9817872c144b018abd77e3915bd77e2c27f4f534dccff8ffaca27361e3a5e1ee": { + "allocationKey": "allocation", + "variationKey": "variant", + "variationType": "boolean", + "variationValue": true, + "reason": "TARGETING_MATCH", + "doLog": false + } + }, + "obfuscated": true, + "obfuscation": { + "scheme": "flag-key-sha256-v1", + "salt": "000102030405060708090a0b0c0d0e0f" + } + } + } + } diff --git a/reliability/single-fit/flags/src/test/resources/precomputed/schema.json b/reliability/single-fit/flags/src/test/resources/precomputed/schema.json new file mode 100644 index 0000000000..98d67165ef --- /dev/null +++ b/reliability/single-fit/flags/src/test/resources/precomputed/schema.json @@ -0,0 +1,416 @@ +{ + "$id": "precomputed-assignment", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "FLEX Precomputed Assignment Fixture", + "description": "Schema for precomputed assignment test fixtures consumed by client SDKs.", + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "description", + "context", + "response", + "evaluations", + "expectations" + ], + "properties": { + "name": { + "type": "string", + "description": "Fixture identifier; should match the filename stem." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the fixture tests." + }, + "context": { + "$ref": "#/$defs/EvaluationContext" + }, + "response": { + "type": "object", + "description": "Mocked precomputed-assignments response. Open-ended; downstream code validates the payload shape." + }, + "evaluations": { + "type": "array", + "items": { + "$ref": "#/$defs/Evaluation" + } + }, + "expectations": { + "$ref": "#/$defs/Expectations" + }, + "_skip": { + "$ref": "#/$defs/PlatformFilter", + "description": "Skip this entire fixture for listed platforms." + }, + "_include": { + "$ref": "#/$defs/PlatformFilter", + "description": "Run this fixture only for listed platforms." + }, + "expectedResponseError": { + "const": "INVALID_RESPONSE", + "description": "Reject the complete response before installation; run no evaluations and emit no events." + } + }, + "not": { + "required": [ + "_skip", + "_include" + ] + }, + "$defs": { + "PlatformFilterEntry": { + "type": "object", + "required": [ + "platform" + ], + "additionalProperties": false, + "properties": { + "platform": { + "type": "string", + "description": "Platform identifier (e.g., web, ios, android, flutter, react-native, maui, kotlin-multiplatform, electron).", + "enum": [ + "web", + "ios", + "android", + "react-native", + "flutter", + "unity", + "maui", + "kotlin-multiplatform", + "electron" + ] + }, + "reason": { + "type": "string", + "description": "Optional explanation for why this platform is skipped or included." + } + } + }, + "PlatformFilter": { + "type": "array", + "items": { + "$ref": "#/$defs/PlatformFilterEntry" + }, + "minItems": 1 + }, + "EvaluationContext": { + "type": "object", + "required": [ + "targetingKey", + "attributes" + ], + "additionalProperties": false, + "properties": { + "targetingKey": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": "object" + } + } + }, + "ExpectedResult": { + "type": "object", + "required": [ + "value" + ], + "additionalProperties": false, + "properties": { + "value": { + "description": "Expected flag value; must match the evaluation variationType." + }, + "reason": { + "type": [ + "string", + "null" + ], + "description": "Expected OpenFeature reason code." + }, + "errorCode": { + "type": [ + "string", + "null" + ], + "description": "Expected OpenFeature error code." + }, + "variantKey": { + "type": [ + "string", + "null" + ], + "description": "Expected variation key." + } + } + }, + "Evaluation": { + "type": "object", + "required": [ + "flag", + "variationType", + "defaultValue", + "expectedResult" + ], + "additionalProperties": false, + "properties": { + "flag": { + "type": "string", + "description": "Flag key to evaluate." + }, + "variationType": { + "type": "string", + "enum": [ + "boolean", + "string", + "integer", + "float", + "object" + ], + "description": "Type to request from the SDK getter." + }, + "defaultValue": { + "description": "Default value passed to the getter; must match variationType." + }, + "expectedResult": { + "$ref": "#/$defs/ExpectedResult" + }, + "_skip": { + "$ref": "#/$defs/PlatformFilter" + }, + "_include": { + "$ref": "#/$defs/PlatformFilter" + } + }, + "allOf": [ + { + "if": { + "properties": { + "variationType": { + "const": "boolean" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "boolean" + }, + "expectedResult": { + "properties": { + "value": { + "type": "boolean" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "string" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "string" + }, + "expectedResult": { + "properties": { + "value": { + "type": "string" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "integer" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "integer" + }, + "expectedResult": { + "properties": { + "value": { + "type": "integer" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "float" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "number" + }, + "expectedResult": { + "properties": { + "value": { + "type": "number" + } + } + } + } + } + }, + { + "if": { + "properties": { + "variationType": { + "const": "object" + } + }, + "required": [ + "variationType" + ] + }, + "then": { + "properties": { + "defaultValue": { + "type": "object" + }, + "expectedResult": { + "properties": { + "value": { + "type": "object" + } + } + } + } + } + } + ], + "not": { + "required": [ + "_skip", + "_include" + ] + } + }, + "EventMatcher": { + "type": "object", + "required": [ + "_count" + ], + "description": "Matches received events by subset. All keys not prefixed with _ are match fields compared against event data. Control fields: _count (required), _skip, _include.", + "properties": { + "_count": { + "type": "integer", + "minimum": 1, + "description": "Number of events this matcher should consume." + }, + "_skip": { + "$ref": "#/$defs/PlatformFilter" + }, + "_include": { + "$ref": "#/$defs/PlatformFilter" + } + }, + "not": { + "required": [ + "_skip", + "_include" + ] + } + }, + "Expectations": { + "type": "object", + "additionalProperties": false, + "properties": { + "noUnmatchedEvents": { + "type": "boolean", + "default": false, + "description": "When true, assert that all received events (exposures and evaluations) are consumed by matchers. No extra events allowed." + }, + "exposures": { + "type": "array", + "items": { + "$ref": "#/$defs/EventMatcher" + }, + "description": "Ordered, exclusive matchers for exposure events." + }, + "evaluationEvents": { + "type": "array", + "items": { + "$ref": "#/$defs/EventMatcher" + }, + "description": "Ordered, exclusive matchers for flagevaluation events." + } + } + } + }, + "allOf": [ + { + "if": { + "required": [ + "expectedResponseError" + ] + }, + "then": { + "properties": { + "evaluations": { + "maxItems": 0 + }, + "expectations": { + "required": [ + "noUnmatchedEvents" + ], + "properties": { + "noUnmatchedEvents": { + "const": true + }, + "exposures": { + "maxItems": 0 + }, + "evaluationEvents": { + "maxItems": 0 + } + } + } + } + }, + "else": { + "properties": { + "evaluations": { + "minItems": 1 + } + } + } + } + ] +}