diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7c4ca5..0757780 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: build-ios: name: Build iOS - runs-on: [ macos-latest ] + runs-on: [ macos-14 ] steps: - uses: actions/checkout@v4 @@ -55,7 +55,7 @@ jobs: test-ios: name: Test iOS - runs-on: [ macos-latest ] + runs-on: [ macos-14 ] steps: - uses: actions/checkout@v4 diff --git a/splitio/CHANGELOG.md b/splitio/CHANGELOG.md index 3ed9258..057ee4c 100644 --- a/splitio/CHANGELOG.md +++ b/splitio/CHANGELOG.md @@ -1,3 +1,13 @@ +# 1.0.0 (Aug 14, 2025) +- Updated Android SDK to `5.3.1` & iOS SDK to `3.3.2` +- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. +- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules. +- Added two new configuration options to control the behavior of the persisted rollout plan cache. Use `rolloutCacheConfiguration` in the config. +- Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. +- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on SplitView type objects. Read more in our docs. + +# 1.0.0-rc.1 (Aug 14, 2025) + # 0.2.0 (Nov 6, 2024) * Added support for targeting rules based on large segments. * BREAKING CHANGE (for Split Proxy users): diff --git a/splitio/LICENSE b/splitio/LICENSE index 68e2d99..af74bff 100644 --- a/splitio/LICENSE +++ b/splitio/LICENSE @@ -176,7 +176,7 @@ END OF TERMS AND CONDITIONS - Copyright © 2024 Split Software, Inc. + Copyright © 2025 Split Software, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/splitio/example/android/app/build.gradle b/splitio/example/android/app/build.gradle index e231f55..25dc71f 100644 --- a/splitio/example/android/app/build.gradle +++ b/splitio/example/android/app/build.gradle @@ -1,29 +1,8 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' +plugins { + id 'com.android.application' + id 'dev.flutter.flutter-gradle-plugin' } -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdk flutter.compileSdkVersion @@ -37,8 +16,8 @@ android { applicationId "io.split.splitio_example" minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + versionCode 1 + versionName "1.0" } buildTypes { diff --git a/splitio/example/android/build.gradle b/splitio/example/android/build.gradle index 45f577c..bc157bd 100644 --- a/splitio/example/android/build.gradle +++ b/splitio/example/android/build.gradle @@ -1,14 +1,3 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.4.0' - } -} - allprojects { repositories { google() diff --git a/splitio/example/android/settings.gradle b/splitio/example/android/settings.gradle index 44e62bc..ea55ff6 100644 --- a/splitio/example/android/settings.gradle +++ b/splitio/example/android/settings.gradle @@ -1,11 +1,24 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" // apply true + id "com.android.application" version "8.4.0" apply false +} + +include ":app" \ No newline at end of file diff --git a/splitio/example/ios/Podfile.lock b/splitio/example/ios/Podfile.lock index ddaefcf..e004592 100644 --- a/splitio/example/ios/Podfile.lock +++ b/splitio/example/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - Flutter (1.0.0) - - Split (3.0.0) - - splitio_ios (0.7.0): + - Split (3.3.2) + - splitio_ios (0.8.0): - Flutter - - Split (~> 3.0.0) + - Split (~> 3.3.2) DEPENDENCIES: - Flutter (from `Flutter`) @@ -21,9 +21,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Split: 66424040ad573d052f58269f841e71b34578a916 - splitio_ios: e4e3becbe89cae0a2fa9ca03a575c21f23af0d90 + Split: 0d4962a6c15180e1857c1a3753e1ae9c91a6150b + splitio_ios: 438ad21d0dfe467670f8b9508773b77b16a71d6b PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 -COCOAPODS: 1.15.0 +COCOAPODS: 1.16.2 diff --git a/splitio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/splitio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e0c9630..4300fbd 100644 --- a/splitio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/splitio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/splitio/example/ios/Runner/AppDelegate.swift b/splitio/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/splitio/example/ios/Runner/AppDelegate.swift +++ b/splitio/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/splitio/example/pubspec.lock b/splitio/example/pubspec.lock index 8da00b5..624c7f2 100644 --- a/splitio/example/pubspec.lock +++ b/splitio/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -79,18 +79,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -111,34 +111,34 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -151,86 +151,83 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" splitio: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.2.0" + version: "1.0.0-rc.1" splitio_android: dependency: transitive description: - name: splitio_android - sha256: "44b0e1dddd374fc73fc1b5ef89598b96ea405d533a8211c06a45665f5d6187b5" - url: "https://pub.dev" - source: hosted - version: "0.2.0" + path: "../../splitio_android" + relative: true + source: path + version: "1.0.0-rc.1" splitio_ios: dependency: transitive description: - name: splitio_ios - sha256: "7c7a2a60711b8e6267cde7e2754d30931dafc76b20b28e1356624963628cb166" - url: "https://pub.dev" - source: hosted - version: "0.2.0" + path: "../../splitio_ios" + relative: true + source: path + version: "1.0.0-rc.1" splitio_platform_interface: dependency: transitive description: - name: splitio_platform_interface - sha256: "2f0457991d18d654486264a66dacf54c7cf23cd88bbb73ed299d69dbbc2fd49b" - url: "https://pub.dev" - source: hosted - version: "1.5.0" + path: "../../splitio_platform_interface" + relative: true + source: path + version: "2.0.0-rc.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" vector_math: dependency: transitive description: @@ -243,10 +240,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "15.0.0" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/splitio/lib/split_client.dart b/splitio/lib/split_client.dart index cb9b27f..883a9f4 100644 --- a/splitio/lib/split_client.dart +++ b/splitio/lib/split_client.dart @@ -18,7 +18,8 @@ abstract class SplitClient { /// /// Returns the evaluated treatment, the default treatment of this feature flag, or 'control'. Future getTreatment(String featureFlagName, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Performs and evaluation and returns a [SplitResult] object for the /// [featureFlagName] feature flag. This object contains the treatment alongside the @@ -37,7 +38,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future getTreatmentWithConfig(String featureFlagName, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations. Returns a [Map] in /// which the keys are feature flag names and the values are treatments. @@ -47,7 +49,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future> getTreatments(List featureFlagNames, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag set. Returns a [Map] in /// which the keys are feature flag names and the values are treatments. @@ -57,7 +60,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future> getTreatmentsByFlagSet(String flagSet, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag sets. Returns a [Map] in /// which the keys are feature flag names and the values are treatments. @@ -67,7 +71,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future> getTreatmentsByFlagSets(List flagSets, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag set. Returns a [Map] in /// which the keys are feature flag names and the values are [SplitResult] objects. @@ -76,8 +81,10 @@ abstract class SplitClient { /// /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. - Future> getTreatmentsWithConfigByFlagSet(String flagSet, - [Map attributes = const {}]); + Future> getTreatmentsWithConfigByFlagSet( + String flagSet, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag sets. Returns a [Map] in /// which the keys are feature flag names and the values are [SplitResult] objects. @@ -86,8 +93,10 @@ abstract class SplitClient { /// /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. - Future> getTreatmentsWithConfigByFlagSets(List flagSets, - [Map attributes = const {}]); + Future> getTreatmentsWithConfigByFlagSets( + List flagSets, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations. Returns a [Map] in /// which the keys are feature flag names and the values are [SplitResult] objects. @@ -98,7 +107,8 @@ abstract class SplitClient { /// take into account when evaluating. Future> getTreatmentsWithConfig( List featureFlagNames, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Enqueue a new event to be sent to Split data collection services. /// @@ -186,79 +196,105 @@ class DefaultSplitClient implements SplitClient { @override Future getTreatment(String featureFlagName, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatment( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitName: featureFlagName, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override Future getTreatmentWithConfig(String featureFlagName, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatmentWithConfig( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitName: featureFlagName, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override Future> getTreatments(List featureFlagNames, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatments( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitNames: featureFlagNames, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override Future> getTreatmentsWithConfig( List featureFlagNames, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatmentsWithConfig( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitNames: featureFlagNames, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsByFlagSet(String flagSet, [Map attributes = const {}]) { + Future> getTreatmentsByFlagSet(String flagSet, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsByFlagSet( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSet: flagSet, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsByFlagSets(List flagSets, [Map attributes = const {}]) { + Future> getTreatmentsByFlagSets(List flagSets, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsByFlagSets( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSets: flagSets, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsWithConfigByFlagSet(String flagSet, [Map attributes = const {}]) { + Future> getTreatmentsWithConfigByFlagSet( + String flagSet, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsWithConfigByFlagSet( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSet: flagSet, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsWithConfigByFlagSets(List flagSets, [Map attributes = const {}]) { + Future> getTreatmentsWithConfigByFlagSets( + List flagSets, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsWithConfigByFlagSets( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSets: flagSets, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override diff --git a/splitio/lib/splitio.dart b/splitio/lib/splitio.dart index 029d523..948c0b7 100644 --- a/splitio/lib/splitio.dart +++ b/splitio/lib/splitio.dart @@ -10,6 +10,7 @@ export 'package:splitio_platform_interface/split_result.dart'; export 'package:splitio_platform_interface/split_sync_config.dart'; export 'package:splitio_platform_interface/split_view.dart'; export 'package:splitio_platform_interface/split_certificate_pinning_configuration.dart'; +export 'package:splitio_platform_interface/split_evaluation_options.dart'; typedef ClientReadinessCallback = void Function(SplitClient splitClient); diff --git a/splitio/pubspec.yaml b/splitio/pubspec.yaml index 3e6d086..05216c1 100644 --- a/splitio/pubspec.yaml +++ b/splitio/pubspec.yaml @@ -1,6 +1,6 @@ name: splitio description: Official plugin for split.io, the platform for controlled rollouts, which serves features to your users via feature flags to manage your complete customer experience. -version: 0.2.0 +version: 1.0.0 homepage: https://split.io/ repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio/ @@ -19,10 +19,9 @@ flutter: dependencies: flutter: sdk: flutter - splitio_android: ^0.2.0 - splitio_ios: ^0.2.0 - splitio_platform_interface: ^1.5.0 - + splitio_android: ^1.0.0 + splitio_ios: ^1.0.0 + splitio_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/splitio/test/splitio_client_test.dart b/splitio/test/splitio_client_test.dart index de984a8..cac009a 100644 --- a/splitio/test/splitio_client_test.dart +++ b/splitio/test/splitio_client_test.dart @@ -21,6 +21,161 @@ void main() { _platform = SplitioPlatformStub(); }); + group('evaluationOptions tests', () { + test('getTreatment includes evaluationOptions when non-empty', () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatment('split', {}, eo); + + expect(_platform.methodName, 'getTreatment'); + expect(_platform.methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentWithConfig includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentWithConfig('split1', {}, eo); + + expect(_platform.methodName, 'getTreatmentWithConfig'); + expect(_platform.methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatments includes evaluationOptions when non-empty', () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatments(['split1', 'split2'], {}, eo); + + expect(_platform.methodName, 'getTreatments'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentsWithConfig includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsWithConfig(['split1', 'split2'], {}, eo); + + expect(_platform.methodName, 'getTreatmentsWithConfig'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentsByFlagSet includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsByFlagSet('set_1', {}, eo); + + expect(_platform.methodName, 'getTreatmentsByFlagSet'); + expect(_platform.methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentsByFlagSets includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsByFlagSets(['set_1', 'set_2'], {}, eo); + + expect(_platform.methodName, 'getTreatmentsByFlagSets'); + expect(_platform.methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSet includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsWithConfigByFlagSet('set_1', {}, eo); + + expect(_platform.methodName, 'getTreatmentsWithConfigByFlagSet'); + expect(_platform.methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSets includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client + .getTreatmentsWithConfigByFlagSets(['set_1', 'set_2'], {}, eo); + + expect(_platform.methodName, 'getTreatmentsWithConfigByFlagSets'); + expect(_platform.methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + }); + group('evaluation', () { test('getTreatment without attributes', () async { SplitClient client = _getClient(); @@ -235,7 +390,8 @@ void main() { test('getTreatmentsWithConfigByFlagSets with attributes', () async { SplitClient client = _getClient(); - client.getTreatmentsWithConfigByFlagSets(['set_1', 'set_2'], {'attr1': true}); + client.getTreatmentsWithConfigByFlagSets( + ['set_1', 'set_2'], {'attr1': true}); expect(_platform.methodName, 'getTreatmentsWithConfigByFlagSets'); expect(_platform.methodArguments, { diff --git a/splitio/test/splitio_platform_stub.dart b/splitio/test/splitio_platform_stub.dart index a996c97..001d7b3 100644 --- a/splitio/test/splitio_platform_stub.dart +++ b/splitio/test/splitio_platform_stub.dart @@ -1,5 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:splitio_platform_interface/splitio_platform_interface.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; class SplitioPlatformStub with MockPlatformInterfaceMixin @@ -89,7 +90,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatment'; methodArguments = { @@ -99,6 +101,10 @@ class SplitioPlatformStub 'attributes': attributes }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value(''); } @@ -107,7 +113,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentWithConfig'; methodArguments = { @@ -117,6 +124,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value(const SplitResult('on', null)); } @@ -125,7 +136,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatments'; methodArguments = { @@ -135,6 +147,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -143,7 +159,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsWithConfig'; methodArguments = { @@ -153,6 +170,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -161,7 +182,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsByFlagSet'; methodArguments = { @@ -171,6 +193,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -179,7 +205,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsByFlagSets'; methodArguments = { @@ -189,6 +216,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -197,7 +228,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsWithConfigByFlagSet'; methodArguments = { @@ -207,6 +239,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -215,7 +251,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsWithConfigByFlagSets'; methodArguments = { @@ -225,6 +262,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } diff --git a/splitio_android/CHANGELOG.md b/splitio_android/CHANGELOG.md index 4093fab..6f47087 100644 --- a/splitio_android/CHANGELOG.md +++ b/splitio_android/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.0.0 (Aug 14, 2025) +- Updated Android SDK to `5.3.1`. + +# 1.0.0-rc.1 (Aug 14, 2025) + # 0.2.0 (Nov 6, 2024) * Updated Android SDK to `5.0.0` diff --git a/splitio_android/LICENSE b/splitio_android/LICENSE index 68e2d99..af74bff 100644 --- a/splitio_android/LICENSE +++ b/splitio_android/LICENSE @@ -176,7 +176,7 @@ END OF TERMS AND CONDITIONS - Copyright © 2024 Split Software, Inc. + Copyright © 2025 Split Software, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/splitio_android/android/build.gradle b/splitio_android/android/build.gradle index f943883..0cd5200 100644 --- a/splitio_android/android/build.gradle +++ b/splitio_android/android/build.gradle @@ -1,6 +1,3 @@ -group 'io.split.splitio' -version '0.0.1' - buildscript { repositories { google() @@ -12,6 +9,10 @@ buildscript { } } +plugins { + id 'com.android.library' +} + rootProject.allprojects { repositories { google() @@ -19,7 +20,8 @@ rootProject.allprojects { } } -apply plugin: 'com.android.library' +group 'io.split.splitio' +version '0.0.1' android { compileSdk 31 @@ -36,7 +38,7 @@ android { } dependencies { - implementation 'io.split.client:android-client:5.0.0' + implementation 'io.split.client:android-client:5.3.1' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:3.12.4' diff --git a/splitio_android/android/src/main/java/io/split/splitio/Constants.java b/splitio_android/android/src/main/java/io/split/splitio/Constants.java index 4537843..eae5404 100644 --- a/splitio_android/android/src/main/java/io/split/splitio/Constants.java +++ b/splitio_android/android/src/main/java/io/split/splitio/Constants.java @@ -41,6 +41,7 @@ static class Argument { static final String SDK_CONFIGURATION = "sdkConfiguration"; static final String SPLIT_NAME = "splitName"; static final String ATTRIBUTES = "attributes"; + static final String EVALUATION_OPTIONS = "evaluationOptions"; static final String EVENT_TYPE = "eventType"; static final String TRAFFIC_TYPE = "trafficType"; static final String VALUE = "value"; diff --git a/splitio_android/android/src/main/java/io/split/splitio/EvaluationWrapper.java b/splitio_android/android/src/main/java/io/split/splitio/EvaluationWrapper.java index 62ed6d0..8f50160 100644 --- a/splitio_android/android/src/main/java/io/split/splitio/EvaluationWrapper.java +++ b/splitio_android/android/src/main/java/io/split/splitio/EvaluationWrapper.java @@ -3,22 +3,23 @@ import java.util.List; import java.util.Map; +import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitResult; interface EvaluationWrapper { - String getTreatment(String matchingKey, String bucketingKey, String splitName, Map attributes); + String getTreatment(String matchingKey, String bucketingKey, String splitName, Map attributes, EvaluationOptions evaluationOptions); - Map getTreatments(String matchingKey, String bucketingKey, List splitNames, Map attributes); + Map getTreatments(String matchingKey, String bucketingKey, List splitNames, Map attributes, EvaluationOptions evaluationOptions); - SplitResult getTreatmentWithConfig(String matchingKey, String bucketingKey, String splitName, Map attributes); + SplitResult getTreatmentWithConfig(String matchingKey, String bucketingKey, String splitName, Map attributes, EvaluationOptions evaluationOptions); - Map getTreatmentsWithConfig(String matchingKey, String bucketingKey, List splitNames, Map attributes); + Map getTreatmentsWithConfig(String matchingKey, String bucketingKey, List splitNames, Map attributes, EvaluationOptions evaluationOptions); - Map getTreatmentsByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes); + Map getTreatmentsByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes, EvaluationOptions evaluationOptions); - Map getTreatmentsByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes); + Map getTreatmentsByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes, EvaluationOptions evaluationOptions); - Map getTreatmentsWithConfigByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes); + Map getTreatmentsWithConfigByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes, EvaluationOptions evaluationOptions); - Map getTreatmentsWithConfigByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes); + Map getTreatmentsWithConfigByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes, EvaluationOptions evaluationOptions); } diff --git a/splitio_android/android/src/main/java/io/split/splitio/ImpressionListenerImp.java b/splitio_android/android/src/main/java/io/split/splitio/ImpressionListenerImp.java index 838d76b..38d9c96 100644 --- a/splitio_android/android/src/main/java/io/split/splitio/ImpressionListenerImp.java +++ b/splitio_android/android/src/main/java/io/split/splitio/ImpressionListenerImp.java @@ -33,7 +33,6 @@ public void log(Impression impression) { public void close() { } - private static Map impressionToMap(final Impression impression) { final Map impressionMap = new HashMap<>(); @@ -45,6 +44,7 @@ private static Map impressionToMap(final Impression impression) impressionMap.put("appliedRule", impression.appliedRule()); impressionMap.put("changeNumber", impression.changeNumber()); impressionMap.put("attributes", impression.attributes()); + impressionMap.put("properties", impression.properties()); return impressionMap; } diff --git a/splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java b/splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java index ce83e03..2df8e0d 100644 --- a/splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java @@ -10,6 +10,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import io.split.android.client.RolloutCacheConfiguration; import io.split.android.client.ServiceEndpoints; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFilter; @@ -51,6 +52,9 @@ class SplitClientConfigHelper { private static final String READY_TIMEOUT = "readyTimeout"; private static final String CERTIFICATE_PINNING_CONFIGURATION = "certificatePinningConfiguration"; private static final String CERTIFICATE_PINNING_CONFIGURATION_PINS = "pins"; + private static final String ROLLOUT_CACHE_CONFIGURATION = "rolloutCacheConfiguration"; + private static final String ROLLOUT_CACHE_CONFIGURATION_EXPIRATION = "expirationDays"; + private static final String ROLLOUT_CACHE_CONFIGURATION_CLEAR_ON_INIT = "clearOnInit"; /** * Creates a {@link SplitClientConfig} object from a map. @@ -242,6 +246,22 @@ static SplitClientConfig fromMap(@NonNull Map configurationMap, } } + Map rolloutCacheConfiguration = getObjectMap(configurationMap, ROLLOUT_CACHE_CONFIGURATION); + if (rolloutCacheConfiguration != null) { + Integer expirationDays = getInteger(rolloutCacheConfiguration, ROLLOUT_CACHE_CONFIGURATION_EXPIRATION); + Boolean clearOnInit = getBoolean(rolloutCacheConfiguration, ROLLOUT_CACHE_CONFIGURATION_CLEAR_ON_INIT); + if (expirationDays != null || clearOnInit != null) { + RolloutCacheConfiguration.Builder cacheConfigBuilder = RolloutCacheConfiguration.builder(); + if (expirationDays != null) { + cacheConfigBuilder.expirationDays(expirationDays); + } + if (clearOnInit != null) { + cacheConfigBuilder.clearOnInit(clearOnInit); + } + builder.rolloutCacheConfiguration(cacheConfigBuilder.build()); + } + } + return builder.serviceEndpoints(serviceEndpointsBuilder.build()).build(); } diff --git a/splitio_android/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java b/splitio_android/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java index 812143c..1f4d248 100644 --- a/splitio_android/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java @@ -4,6 +4,7 @@ import static io.split.splitio.Constants.Argument.ATTRIBUTES; import static io.split.splitio.Constants.Argument.ATTRIBUTE_NAME; import static io.split.splitio.Constants.Argument.BUCKETING_KEY; +import static io.split.splitio.Constants.Argument.EVALUATION_OPTIONS; import static io.split.splitio.Constants.Argument.EVENT_TYPE; import static io.split.splitio.Constants.Argument.FLAG_SET; import static io.split.splitio.Constants.Argument.FLAG_SETS; @@ -57,9 +58,11 @@ import java.util.Map; import io.flutter.plugin.common.MethodChannel; +import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitClient; import io.split.android.client.SplitResult; import io.split.android.client.api.SplitView; +import io.split.android.client.dtos.Prerequisite; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; @@ -115,56 +118,64 @@ public void onMethodCall(String methodName, Object arguments, @NonNull MethodCha mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringArgument(SPLIT_NAME, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case GET_TREATMENTS: result.success(getTreatments( mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringListArgument(SPLIT_NAME, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case GET_TREATMENT_WITH_CONFIG: result.success(getTreatmentWithConfig( mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringArgument(SPLIT_NAME, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case GET_TREATMENTS_WITH_CONFIG: result.success(getTreatmentsWithConfig( mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringListArgument(SPLIT_NAME, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case GET_TREATMENTS_BY_FLAG_SET: result.success(getTreatmentsByFlagSet( mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringArgument(FLAG_SET, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case GET_TREATMENTS_BY_FLAG_SETS: result.success(getTreatmentsByFlagSets( mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringListArgument(FLAG_SETS, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET: result.success(getTreatmentsWithConfigByFlagSet( mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringArgument(FLAG_SET, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS: result.success(getTreatmentsWithConfigByFlagSets( mArgumentParser.getStringArgument(MATCHING_KEY, arguments), mArgumentParser.getStringArgument(BUCKETING_KEY, arguments), mArgumentParser.getStringListArgument(FLAG_SETS, arguments), - mArgumentParser.getMapArgument(ATTRIBUTES, arguments))); + mArgumentParser.getMapArgument(ATTRIBUTES, arguments), + buildEvaluationOptions(mArgumentParser.getMapArgument(EVALUATION_OPTIONS, arguments)))); break; case TRACK: result.success(track( @@ -277,23 +288,26 @@ private SplitClient getClient(String matchingKey, String bucketingKey) { private String getTreatment(String matchingKey, String bucketingKey, String splitName, - Map attributes) { - return mSplitWrapper.getTreatment(matchingKey, bucketingKey, splitName, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + return mSplitWrapper.getTreatment(matchingKey, bucketingKey, splitName, attributes, evaluationOptions); } private Map getTreatments(String matchingKey, String bucketingKey, List splitNames, - Map attributes) { - return mSplitWrapper.getTreatments(matchingKey, bucketingKey, splitNames, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + return mSplitWrapper.getTreatments(matchingKey, bucketingKey, splitNames, attributes, evaluationOptions); } private Map> getTreatmentWithConfig( String matchingKey, String bucketingKey, String splitName, - Map attributes) { - SplitResult treatment = mSplitWrapper.getTreatmentWithConfig(matchingKey, bucketingKey, splitName, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + SplitResult treatment = mSplitWrapper.getTreatmentWithConfig(matchingKey, bucketingKey, splitName, attributes, evaluationOptions); return Collections.singletonMap(splitName, getSplitResultMap(treatment)); } @@ -302,8 +316,9 @@ private Map> getTreatmentsWithConfig( String matchingKey, String bucketingKey, List splitNames, - Map attributes) { - Map treatmentsWithConfig = mSplitWrapper.getTreatmentsWithConfig(matchingKey, bucketingKey, splitNames, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + Map treatmentsWithConfig = mSplitWrapper.getTreatmentsWithConfig(matchingKey, bucketingKey, splitNames, attributes, evaluationOptions); return mapToSplitResults(treatmentsWithConfig); } @@ -311,24 +326,27 @@ private Map getTreatmentsByFlagSet( String matchingKey, String bucketingKey, String flagSet, - Map attributes) { - return mSplitWrapper.getTreatmentsByFlagSet(matchingKey, bucketingKey, flagSet, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + return mSplitWrapper.getTreatmentsByFlagSet(matchingKey, bucketingKey, flagSet, attributes, evaluationOptions); } private Map getTreatmentsByFlagSets( String matchingKey, String bucketingKey, List flagSets, - Map attributes) { - return mSplitWrapper.getTreatmentsByFlagSets(matchingKey, bucketingKey, flagSets, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + return mSplitWrapper.getTreatmentsByFlagSets(matchingKey, bucketingKey, flagSets, attributes, evaluationOptions); } private Map> getTreatmentsWithConfigByFlagSet( String matchingKey, String bucketingKey, String flagSet, - Map attributes) { - Map treatmentsWithConfig = mSplitWrapper.getTreatmentsWithConfigByFlagSet(matchingKey, bucketingKey, flagSet, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + Map treatmentsWithConfig = mSplitWrapper.getTreatmentsWithConfigByFlagSet(matchingKey, bucketingKey, flagSet, attributes, evaluationOptions); return mapToSplitResults(treatmentsWithConfig); } @@ -336,8 +354,9 @@ private Map> getTreatmentsWithConfigByFlagSets( String matchingKey, String bucketingKey, List flagSets, - Map attributes) { - Map treatmentsWithConfig = mSplitWrapper.getTreatmentsWithConfigByFlagSets(matchingKey, bucketingKey, flagSets, attributes); + Map attributes, + EvaluationOptions evaluationOptions) { + Map treatmentsWithConfig = mSplitWrapper.getTreatmentsWithConfigByFlagSets(matchingKey, bucketingKey, flagSets, attributes, evaluationOptions); return mapToSplitResults(treatmentsWithConfig); } @@ -474,7 +493,31 @@ private static Map getSplitViewAsMap(@Nullable SplitView splitVi splitViewMap.put("configs", splitView.configs); splitViewMap.put("defaultTreatment", splitView.defaultTreatment); splitViewMap.put("sets", splitView.sets); + splitViewMap.put("prerequisites", getPrerequisiteMap(splitView.prerequisites)); + splitViewMap.put("impressionsDisabled", splitView.impressionsDisabled); return splitViewMap; } + + private static List> getPrerequisiteMap(List prerequisites) { + List> prerequisiteList = new ArrayList<>(); + + for (Prerequisite prerequisite : prerequisites) { + Map prerequisiteMap = new HashMap<>(); + prerequisiteMap.put("n", prerequisite.getFlagName()); + prerequisiteMap.put("t", prerequisite.getTreatments()); + prerequisiteList.add(prerequisiteMap); + } + + return prerequisiteList; + } + + @Nullable + private EvaluationOptions buildEvaluationOptions(Map properties) { + if (properties == null || properties.isEmpty()) { + return null; + } + + return new EvaluationOptions(properties); + } } diff --git a/splitio_android/android/src/main/java/io/split/splitio/SplitWrapperImpl.java b/splitio_android/android/src/main/java/io/split/splitio/SplitWrapperImpl.java index 1adef4e..83be8bb 100644 --- a/splitio_android/android/src/main/java/io/split/splitio/SplitWrapperImpl.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitWrapperImpl.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitClient; import io.split.android.client.SplitFactory; import io.split.android.client.SplitResult; @@ -78,17 +79,17 @@ public boolean track(String matchingKey, @Nullable String bucketingKey, String e } @Override - public String getTreatment(String matchingKey, String bucketingKey, String splitName, Map attributes) { + public String getTreatment(String matchingKey, String bucketingKey, String splitName, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { return Treatments.CONTROL; } - return client.getTreatment(splitName, attributes); + return client.getTreatment(splitName, attributes, evaluationOptions); } @Override - public Map getTreatments(String matchingKey, String bucketingKey, List splitNames, Map attributes) { + public Map getTreatments(String matchingKey, String bucketingKey, List splitNames, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { Map defaultResult = new HashMap<>(); @@ -99,21 +100,21 @@ public Map getTreatments(String matchingKey, String bucketingKey return defaultResult; } - return client.getTreatments(splitNames, attributes); + return client.getTreatments(splitNames, attributes, evaluationOptions); } @Override - public SplitResult getTreatmentWithConfig(String matchingKey, String bucketingKey, String splitName, Map attributes) { + public SplitResult getTreatmentWithConfig(String matchingKey, String bucketingKey, String splitName, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { return new SplitResult(Treatments.CONTROL); } - return client.getTreatmentWithConfig(splitName, attributes); + return client.getTreatmentWithConfig(splitName, attributes, evaluationOptions); } @Override - public Map getTreatmentsWithConfig(String matchingKey, String bucketingKey, List splitNames, Map attributes) { + public Map getTreatmentsWithConfig(String matchingKey, String bucketingKey, List splitNames, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { Map defaultResult = new HashMap<>(); @@ -124,47 +125,47 @@ public Map getTreatmentsWithConfig(String matchingKey, Stri return defaultResult; } - return client.getTreatmentsWithConfig(splitNames, attributes); + return client.getTreatmentsWithConfig(splitNames, attributes, evaluationOptions); } @Override - public Map getTreatmentsByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes) { + public Map getTreatmentsByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { return new HashMap<>(); } - return client.getTreatmentsByFlagSet(flagSet, attributes); + return client.getTreatmentsByFlagSet(flagSet, attributes, evaluationOptions); } @Override - public Map getTreatmentsByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes) { + public Map getTreatmentsByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { return new HashMap<>(); } - return client.getTreatmentsByFlagSets(flagSets, attributes); + return client.getTreatmentsByFlagSets(flagSets, attributes, evaluationOptions); } @Override - public Map getTreatmentsWithConfigByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes) { + public Map getTreatmentsWithConfigByFlagSet(String matchingKey, String bucketingKey, String flagSet, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { return new HashMap<>(); } - return client.getTreatmentsWithConfigByFlagSet(flagSet, attributes); + return client.getTreatmentsWithConfigByFlagSet(flagSet, attributes, evaluationOptions); } @Override - public Map getTreatmentsWithConfigByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes) { + public Map getTreatmentsWithConfigByFlagSets(String matchingKey, String bucketingKey, List flagSets, Map attributes, EvaluationOptions evaluationOptions) { SplitClient client = getInitializedClient(matchingKey, bucketingKey); if (client == null) { return new HashMap<>(); } - return client.getTreatmentsWithConfigByFlagSets(flagSets, attributes); + return client.getTreatmentsWithConfigByFlagSets(flagSets, attributes, evaluationOptions); } @Override diff --git a/splitio_android/android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java b/splitio_android/android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java index 69be7b9..91b624c 100644 --- a/splitio_android/android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java +++ b/splitio_android/android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java @@ -31,7 +31,7 @@ public void setUp() { @Test public void loggingInvokesMethodOnMethodChannel() { - Impression impression = new Impression("key", null, "my_split", "on", 20021002, "on treatment", 1002L, Collections.emptyMap()); + Impression impression = new Impression("key", null, "my_split", "on", 20021002, "on treatment", 1002L, Collections.emptyMap(), "[{\"prop1\", \"value1\"}, {\"prop2\", \"value2\"}]"); mImpressionListener.log(impression); verify(mMethodChannel).invokeMethod(eq("impressionLog"), any()); @@ -39,7 +39,7 @@ public void loggingInvokesMethodOnMethodChannel() { @Test public void loggingInvokesMethodOnMethodChannelWithCorrectArgument() { - Impression impression = new Impression("key", null, "my_split", "on", 20021002, "on treatment", 1002L, Collections.singletonMap("age", 25)); + Impression impression = new Impression("key", null, "my_split", "on", 20021002, "on treatment", 1002L, Collections.singletonMap("age", 25), "[{\"prop1\", \"value1\"}, {\"prop2\", \"value2\"}]"); Map expectedImpressionMap = new HashMap<>(); expectedImpressionMap.put("key", "key"); expectedImpressionMap.put("bucketingKey", null); @@ -49,6 +49,7 @@ public void loggingInvokesMethodOnMethodChannelWithCorrectArgument() { expectedImpressionMap.put("appliedRule", "on treatment"); expectedImpressionMap.put("changeNumber", 1002L); expectedImpressionMap.put("attributes", Collections.singletonMap("age", 25)); + expectedImpressionMap.put("properties", "[{\"prop1\", \"value1\"}, {\"prop2\", \"value2\"}]"); mImpressionListener.log(impression); verify(mMethodChannel).invokeMethod("impressionLog", expectedImpressionMap); diff --git a/splitio_android/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java b/splitio_android/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java index 18691f0..23a5215 100644 --- a/splitio_android/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java +++ b/splitio_android/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java @@ -219,4 +219,20 @@ public void certificatePinningConfigurationValuesAreMappedCorrectly() { Set host2Pins = actualConfig.getPins().get("host2"); assertEquals("sha256", host2Pins.iterator().next().getAlgorithm()); } + + @Test + public void rolloutCacheConfigurationValuesAreMappedCorrectly() { + Map configValues = new HashMap<>(); + Map rolloutCacheConfigValues = new HashMap<>(); + + rolloutCacheConfigValues.put("expirationDays", 5); + rolloutCacheConfigValues.put("clearOnInit", true); + configValues.put("rolloutCacheConfiguration", rolloutCacheConfigValues); + + SplitClientConfig splitClientConfig = SplitClientConfigHelper + .fromMap(configValues, mock(ImpressionListener.class)); + + assertEquals(5, splitClientConfig.rolloutCacheConfiguration().getExpirationDays()); + assertTrue(splitClientConfig.rolloutCacheConfiguration().isClearOnInit()); + } } diff --git a/splitio_android/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java b/splitio_android/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java index 7f7a225..d4e804a 100644 --- a/splitio_android/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java +++ b/splitio_android/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java @@ -1,5 +1,7 @@ package io.split.splitio; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -8,19 +10,29 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; + import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import io.flutter.plugin.common.MethodChannel; +import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitClient; import io.split.android.client.SplitResult; +import io.split.android.client.api.SplitView; +import io.split.android.client.dtos.Prerequisite; public class SplitMethodParserImplTest { @@ -55,7 +67,7 @@ public void successfulGetClient() { mMethodParser.onMethodCall("getClient", map, mResult); verify(mResult).success(null); - verify(mSplitWrapper).getClient("user-key", "bucketing-key"); + verify(mSplitWrapper).getClient(eq("user-key"), eq("bucketing-key")); } @Test @@ -127,11 +139,13 @@ public void getTreatmentWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn(null); when(mArgumentParser.getStringArgument("splitName", map)).thenReturn("split-name"); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatment(any(), any(), any(), any())).thenReturn("on"); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatment(any(), any(), any(), any(), any())).thenReturn("on"); mMethodParser.onMethodCall("getTreatment", map, mResult); - verify(mSplitWrapper).getTreatment("user-key", null, "split-name", Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatment(eq("user-key"), eq((String) null), eq("split-name"), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); verify(mResult).success("on"); } @@ -150,11 +164,13 @@ public void getTreatmentsWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn(null); when(mArgumentParser.getStringListArgument("splitName", map)).thenReturn(Arrays.asList("split1", "split2")); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatments(any(), any(), any(), any())).thenReturn(expectedResponse); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatments(any(), any(), any(), any(), any())).thenReturn(expectedResponse); mMethodParser.onMethodCall("getTreatments", map, mResult); - verify(mSplitWrapper).getTreatments("user-key", null, Arrays.asList("split1", "split2"), Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatments(eq("user-key"), eq((String) null), eq(Arrays.asList("split1", "split2")), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); verify(mResult).success(expectedResponse); } @@ -184,11 +200,13 @@ public void getTreatmentsWithConfigWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn("bucketing-key"); when(mArgumentParser.getStringListArgument("splitName", map)).thenReturn(Arrays.asList("split1", "split2")); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatmentsWithConfig(any(), any(), any(), any())).thenReturn(mockResult); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatmentsWithConfig(any(), any(), any(), any(), any())).thenReturn(mockResult); mMethodParser.onMethodCall("getTreatmentsWithConfig", map, mResult); - verify(mSplitWrapper).getTreatmentsWithConfig("user-key", "bucketing-key", Arrays.asList("split1", "split2"), Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatmentsWithConfig(eq("user-key"), eq("bucketing-key"), eq(Arrays.asList("split1", "split2")), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); verify(mResult).success(finalResultMap); } @@ -204,11 +222,13 @@ public void getTreatmentWithConfigWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn("bucketing-key"); when(mArgumentParser.getStringArgument("splitName", map)).thenReturn("split-name"); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatmentWithConfig(any(), any(), any(), any())).thenReturn(new SplitResult("on", "{config}")); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatmentWithConfig(any(), any(), any(), any(), any())).thenReturn(new SplitResult("on", "{config}")); mMethodParser.onMethodCall("getTreatmentWithConfig", map, mResult); - verify(mSplitWrapper).getTreatmentWithConfig("user-key", "bucketing-key", "split-name", Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatmentWithConfig(eq("user-key"), eq("bucketing-key"), eq("split-name"), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); Map resultMap = new HashMap<>(); resultMap.put("treatment", "on"); resultMap.put("config", "{config}"); @@ -231,7 +251,7 @@ public void trackWithValue() { mMethodParser.onMethodCall("track", map, mResult); - verify(mSplitWrapper).track("user-key", "bucketing-key", "my-event", null, 25.20, Collections.emptyMap()); + verify(mSplitWrapper).track(eq("user-key"), eq("bucketing-key"), eq("my-event"), eq((String) null), eq(25.20), eq(Collections.emptyMap())); } @Test @@ -249,7 +269,7 @@ public void trackWithInvalidValue() { mMethodParser.onMethodCall("track", map, mResult); - verify(mSplitWrapper).track("user-key", "bucketing-key", "my-event", null, null, Collections.emptyMap()); + verify(mSplitWrapper).track(eq("user-key"), eq("bucketing-key"), eq("my-event"), eq((String) null), eq((Double) null), eq(Collections.emptyMap())); } @Test @@ -269,7 +289,7 @@ public void trackWithValueAndProperties() { mMethodParser.onMethodCall("track", map, mResult); - verify(mSplitWrapper).track("user-key", "bucketing-key", "my-event", null, 25.20, Collections.singletonMap("age", 50)); + verify(mSplitWrapper).track(eq("user-key"), eq("bucketing-key"), eq("my-event"), eq((String) null), eq(25.20), eq(Collections.singletonMap("age", 50))); } @Test @@ -291,7 +311,7 @@ public void trackWithEverything() { mMethodParser.onMethodCall("track", map, mResult); - verify(mSplitWrapper).track("user-key", "bucketing-key", "my-event", "account", 25.20, Collections.singletonMap("age", 50)); + verify(mSplitWrapper).track(eq("user-key"), eq("bucketing-key"), eq("my-event"), eq("account"), eq(25.20), eq(Collections.singletonMap("age", 50))); } @Test @@ -307,7 +327,7 @@ public void getSingleAttribute() { mMethodParser.onMethodCall("getAttribute", map, mResult); - verify(mSplitWrapper).getAttribute("user-key", "bucketing-key", "my_attribute"); + verify(mSplitWrapper).getAttribute(eq("user-key"), eq("bucketing-key"), eq("my_attribute")); } @Test @@ -321,7 +341,7 @@ public void getAllAttributes() { mMethodParser.onMethodCall("getAllAttributes", map, mResult); - verify(mSplitWrapper).getAllAttributes("user-key", "bucketing-key"); + verify(mSplitWrapper).getAllAttributes(eq("user-key"), eq("bucketing-key")); } @Test @@ -339,7 +359,7 @@ public void setSingleAttribute() { mMethodParser.onMethodCall("setAttribute", map, mResult); - verify(mSplitWrapper).setAttribute("user-key", "bucketing-key", "my_attr", "attr_value"); + verify(mSplitWrapper).setAttribute(eq("user-key"), eq("bucketing-key"), eq("my_attr"), eq("attr_value")); } @Test @@ -361,7 +381,7 @@ public void setMultipleAttributes() { mMethodParser.onMethodCall("setAttributes", map, mResult); - verify(mSplitWrapper).setAttributes("user-key", "bucketing-key", attributesMap); + verify(mSplitWrapper).setAttributes(eq("user-key"), eq("bucketing-key"), eq(attributesMap)); } @Test @@ -377,7 +397,7 @@ public void removeAttribute() { mMethodParser.onMethodCall("removeAttribute", map, mResult); - verify(mSplitWrapper).removeAttribute("user-key", "bucketing-key", "my_attr"); + verify(mSplitWrapper).removeAttribute(eq("user-key"), eq("bucketing-key"), eq("my_attr")); } @Test @@ -391,7 +411,7 @@ public void clearAttributes() { mMethodParser.onMethodCall("clearAttributes", map, mResult); - verify(mSplitWrapper).clearAttributes("user-key", "bucketing-key"); + verify(mSplitWrapper).clearAttributes(eq("user-key"), eq("bucketing-key")); } @Test @@ -405,7 +425,7 @@ public void flush() { mMethodParser.onMethodCall("flush", map, mResult); - verify(mSplitWrapper).flush("user-key", "bucketing-key"); + verify(mSplitWrapper).flush(eq("user-key"), eq("bucketing-key")); verify(mResult).success(null); } @@ -420,22 +440,89 @@ public void destroy() { mMethodParser.onMethodCall("destroy", map, mResult); - verify(mSplitWrapper).destroy("user-key", "bucketing-key"); + verify(mSplitWrapper).destroy(eq("user-key"), eq("bucketing-key")); verify(mResult).success(null); } @Test - public void splitNames() { - mMethodParser.onMethodCall("splitNames", Collections.emptyMap(), mResult); - - verify(mSplitWrapper).splitNames(); + public void splitViews() { + Prerequisite prerequisite1 = mock(Prerequisite.class); + when(prerequisite1.getFlagName()).thenReturn("flag1"); + when(prerequisite1.getTreatments()).thenReturn(new HashSet<>(Arrays.asList("on", "true"))); + + Prerequisite prerequisite2 = mock(Prerequisite.class); + when(prerequisite2.getFlagName()).thenReturn("flag2"); + when(prerequisite2.getTreatments()).thenReturn(new HashSet<>(Arrays.asList("on", "true"))); + + List prerequisites = Arrays.asList(prerequisite1, prerequisite2); + + SplitView splitView = mock(SplitView.class); + splitView.name = "test_split"; + splitView.trafficType = "user"; + splitView.killed = false; + splitView.treatments = Arrays.asList("on", "off"); + splitView.changeNumber = 123L; + splitView.configs = Collections.singletonMap("on", "{\"color\": \"blue\"}"); + splitView.defaultTreatment = "off"; + splitView.sets = Arrays.asList("set1", "set2"); + splitView.prerequisites = prerequisites; + splitView.impressionsDisabled = true; + + when(mSplitWrapper.splits()).thenReturn(Arrays.asList(splitView)); + + mMethodParser.onMethodCall("splits", Collections.emptyMap(), mResult); + + verify(mSplitWrapper).splits(); + + // Verify that the result includes the correctly formatted prerequisites + ArgumentCaptor>> captor = ArgumentCaptor.forClass(List.class); + verify(mResult).success(captor.capture()); + + List> result = captor.getValue(); + assertEquals(1, result.size()); + + Map splitViewMap = result.get(0); + + // Verify all SplitView fields are correctly mapped + assertEquals("test_split", splitViewMap.get("name")); + assertEquals("user", splitViewMap.get("trafficType")); + assertEquals(false, splitViewMap.get("killed")); + assertEquals(Arrays.asList("on", "off"), splitViewMap.get("treatments")); + assertEquals(123L, splitViewMap.get("changeNumber")); + assertEquals(Collections.singletonMap("on", "{\"color\": \"blue\"}"), splitViewMap.get("configs")); + assertEquals("off", splitViewMap.get("defaultTreatment")); + assertEquals(Arrays.asList("set1", "set2"), splitViewMap.get("sets")); + assertEquals(true, splitViewMap.get("impressionsDisabled")); + + // Verify prerequisites + assertTrue(splitViewMap.containsKey("prerequisites")); + + @SuppressWarnings("unchecked") + List> prerequisitesResult = (List>) splitViewMap.get("prerequisites"); + assertEquals(2, prerequisitesResult.size()); + + // Verify first prerequisite + Map prereq1 = prerequisitesResult.get(0); + assertEquals("flag1", prereq1.get("n")); + Set treatments = (Set) prereq1.get("t"); + assertEquals(2, treatments.size()); + assertTrue(treatments.contains("on")); + assertTrue(treatments.contains("true")); + + // Verify second prerequisite + Map prereq2 = prerequisitesResult.get(1); + assertEquals("flag2", prereq2.get("n")); + Set treatments1 = (Set) prereq2.get("t"); + assertEquals(2, treatments1.size()); + assertTrue(treatments1.contains("on")); + assertTrue(treatments1.contains("true")); } @Test - public void splits() { - mMethodParser.onMethodCall("splits", Collections.emptyMap(), mResult); + public void splitNames() { + mMethodParser.onMethodCall("splitNames", Collections.emptyMap(), mResult); - verify(mSplitWrapper).splits(); + verify(mSplitWrapper).splitNames(); } @Test @@ -444,7 +531,7 @@ public void split() { mMethodParser.onMethodCall("split", Collections.singletonMap("splitName", "my_split"), mResult); - verify(mSplitWrapper).split("my_split"); + verify(mSplitWrapper).split(eq("my_split")); } @Test @@ -492,7 +579,7 @@ public void setUserConsentEnabled() { mMethodParser.onMethodCall("setUserConsent", Collections.singletonMap("value", true), mResult); verify(mResult).success(null); - verify(mSplitWrapper).setUserConsent(true); + verify(mSplitWrapper).setUserConsent(eq(true)); } @Test @@ -502,7 +589,7 @@ public void setUserConsentDisabled() { mMethodParser.onMethodCall("setUserConsent", Collections.singletonMap("value", false), mResult); verify(mResult).success(null); - verify(mSplitWrapper).setUserConsent(false); + verify(mSplitWrapper).setUserConsent(eq(false)); } @Test @@ -517,11 +604,13 @@ public void getTreatmentsByFlagSetWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn("bucketing-key"); when(mArgumentParser.getStringArgument("flagSet", map)).thenReturn("set_1"); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatmentsByFlagSet(any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", "on")); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatmentsByFlagSet(any(), any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", "on")); mMethodParser.onMethodCall("getTreatmentsByFlagSet", map, mResult); - verify(mSplitWrapper).getTreatmentsByFlagSet("user-key", "bucketing-key", "set_1", Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatmentsByFlagSet(eq("user-key"), eq("bucketing-key"), eq("set_1"), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); verify(mResult).success(Collections.singletonMap("flag_1", "on")); } @@ -537,11 +626,13 @@ public void getTreatmentsByFlagSetsWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn("bucketing-key"); when(mArgumentParser.getStringListArgument("flagSets", map)).thenReturn(Arrays.asList("set_1", "set_2")); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatmentsByFlagSets(any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", "on")); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatmentsByFlagSets(any(), any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", "on")); mMethodParser.onMethodCall("getTreatmentsByFlagSets", map, mResult); - verify(mSplitWrapper).getTreatmentsByFlagSets("user-key", "bucketing-key", Arrays.asList("set_1", "set_2"), Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatmentsByFlagSets(eq("user-key"), eq("bucketing-key"), eq(Arrays.asList("set_1", "set_2")), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); verify(mResult).success(Collections.singletonMap("flag_1", "on")); } @@ -561,11 +652,13 @@ public void getTreatmentsWithConfigByFlagSetWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn("bucketing-key"); when(mArgumentParser.getStringArgument("flagSet", map)).thenReturn("set_1"); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatmentsWithConfigByFlagSet(any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", new SplitResult("on", "{config}"))); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatmentsWithConfigByFlagSet(any(), any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", new SplitResult("on", "{config}"))); mMethodParser.onMethodCall("getTreatmentsWithConfigByFlagSet", map, mResult); - verify(mSplitWrapper).getTreatmentsWithConfigByFlagSet("user-key", "bucketing-key", "set_1", Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatmentsWithConfigByFlagSet(eq("user-key"), eq("bucketing-key"), eq("set_1"), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); verify(mResult).success(Collections.singletonMap("flag_1", resultMap1)); } @@ -585,11 +678,18 @@ public void getTreatmentsWithConfigByFlagSetsWorksCorrectly() { when(mArgumentParser.getStringArgument("bucketingKey", map)).thenReturn("bucketing-key"); when(mArgumentParser.getStringListArgument("flagSets", map)).thenReturn(Arrays.asList("set_1", "set_2")); when(mArgumentParser.getMapArgument("attributes", map)).thenReturn(Collections.singletonMap("age", 10)); - when(mSplitWrapper.getTreatmentsWithConfigByFlagSets(any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", new SplitResult("on", "{config}"))); + when(mArgumentParser.getMapArgument("evaluationOptions", map)).thenReturn(Collections.singletonMap("boolean", true)); + when(mSplitWrapper.getTreatmentsWithConfigByFlagSets(any(), any(), any(), any(), any())).thenReturn(Collections.singletonMap("flag_1", new SplitResult("on", "{config}"))); mMethodParser.onMethodCall("getTreatmentsWithConfigByFlagSets", map, mResult); - verify(mSplitWrapper).getTreatmentsWithConfigByFlagSets("user-key", "bucketing-key", Arrays.asList("set_1", "set_2"), Collections.singletonMap("age", 10)); + verify(mSplitWrapper).getTreatmentsWithConfigByFlagSets(eq("user-key"), eq("bucketing-key"), eq(Arrays.asList("set_1", "set_2")), eq(Collections.singletonMap("age", 10)), + argThat(evaluationOptionsPropertiesMatcher())); verify(mResult).success(Collections.singletonMap("flag_1", resultMap1)); } + + @NonNull + private static ArgumentMatcher evaluationOptionsPropertiesMatcher() { + return options -> options != null && options.getProperties().size() == 1 && options.getProperties().containsKey("boolean") && (Boolean) options.getProperties().get("boolean"); + } } diff --git a/splitio_android/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java b/splitio_android/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java index 733fb31..42ae794 100644 --- a/splitio_android/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java +++ b/splitio_android/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Set; +import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitClient; import io.split.android.client.SplitFactory; import io.split.android.client.SplitManager; @@ -60,9 +61,10 @@ public void testGetTreatment() { when(mSplitFactory.client("key", null)).thenReturn(clientMock); when(mUsedKeys.contains(new Key("key", null))).thenReturn(true); - mSplitWrapper.getTreatment("key", null, "split-name", Collections.singletonMap("age", 50)); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatment("key", null, "split-name", Collections.singletonMap("age", 50), evaluationOptions); - verify(clientMock).getTreatment("split-name", Collections.singletonMap("age", 50)); + verify(clientMock).getTreatment("split-name", Collections.singletonMap("age", 50), evaluationOptions); } @Test @@ -72,9 +74,10 @@ public void testGetTreatments() { when(mSplitFactory.client("key", null)).thenReturn(clientMock); when(mUsedKeys.contains(new Key("key", null))).thenReturn(true); - mSplitWrapper.getTreatments("key", null, Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50)); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatments("key", null, Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50), evaluationOptions); - verify(clientMock).getTreatments(Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50)); + verify(clientMock).getTreatments(Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50), evaluationOptions); } @Test @@ -84,9 +87,10 @@ public void testGetTreatmentWithConfig() { when(mSplitFactory.client("key", null)).thenReturn(clientMock); when(mUsedKeys.contains(new Key("key", null))).thenReturn(true); - mSplitWrapper.getTreatmentWithConfig("key", null, "split-name", Collections.singletonMap("age", 50)); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatmentWithConfig("key", null, "split-name", Collections.singletonMap("age", 50), evaluationOptions); - verify(clientMock).getTreatmentWithConfig("split-name", Collections.singletonMap("age", 50)); + verify(clientMock).getTreatmentWithConfig("split-name", Collections.singletonMap("age", 50), evaluationOptions); } @Test @@ -96,9 +100,10 @@ public void testGetTreatmentsWithConfig() { when(mSplitFactory.client("key", null)).thenReturn(clientMock); when(mUsedKeys.contains(new Key("key", null))).thenReturn(true); - mSplitWrapper.getTreatmentsWithConfig("key", null, Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50)); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatmentsWithConfig("key", null, Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50), evaluationOptions); - verify(clientMock).getTreatmentsWithConfig(Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50)); + verify(clientMock).getTreatmentsWithConfig(Arrays.asList("split1", "split2"), Collections.singletonMap("age", 50), evaluationOptions); } @Test @@ -108,9 +113,10 @@ public void testGetTreatmentsByFlagSet() { when(mUsedKeys.contains(new Key("key", null))).thenReturn(true); Map attrs = Collections.singletonMap("age", 50); - mSplitWrapper.getTreatmentsByFlagSet("key", null, "flag-set", attrs); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatmentsByFlagSet("key", null, "flag-set", attrs, evaluationOptions); - verify(clientMock).getTreatmentsByFlagSet("flag-set", attrs); + verify(clientMock).getTreatmentsByFlagSet("flag-set", attrs, evaluationOptions); } @Test @@ -121,9 +127,10 @@ public void testGetTreatmentsByFlagSets() { Map attrs = Collections.singletonMap("age", 50); List sets = Arrays.asList("set_1", "set_2"); - mSplitWrapper.getTreatmentsByFlagSets("key", null, sets, attrs); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatmentsByFlagSets("key", null, sets, attrs, evaluationOptions); - verify(clientMock).getTreatmentsByFlagSets(sets, attrs); + verify(clientMock).getTreatmentsByFlagSets(sets, attrs, evaluationOptions); } @Test @@ -133,9 +140,10 @@ public void testGetTreatmentsWithConfigByFlagSet() { when(mUsedKeys.contains(new Key("key", null))).thenReturn(true); Map attrs = Collections.singletonMap("age", 50); - mSplitWrapper.getTreatmentsWithConfigByFlagSet("key", null,"set_1", attrs); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatmentsWithConfigByFlagSet("key", null,"set_1", attrs, evaluationOptions); - verify(clientMock).getTreatmentsWithConfigByFlagSet("set_1", attrs); + verify(clientMock).getTreatmentsWithConfigByFlagSet("set_1", attrs, evaluationOptions); } @Test @@ -146,9 +154,10 @@ public void testGetTreatmentsWithConfigByFlagSets() { Map attrs = Collections.singletonMap("age", 50); List sets = Arrays.asList("set_1", "set_2"); - mSplitWrapper.getTreatmentsWithConfigByFlagSets("key", null, sets, attrs); + EvaluationOptions evaluationOptions = new EvaluationOptions(Collections.singletonMap("boolean", true)); + mSplitWrapper.getTreatmentsWithConfigByFlagSets("key", null, sets, attrs, evaluationOptions); - verify(clientMock).getTreatmentsWithConfigByFlagSets(sets, attrs); + verify(clientMock).getTreatmentsWithConfigByFlagSets(sets, attrs, evaluationOptions); } @Test diff --git a/splitio_android/pubspec.yaml b/splitio_android/pubspec.yaml index cc75d6e..fc2bbbe 100644 --- a/splitio_android/pubspec.yaml +++ b/splitio_android/pubspec.yaml @@ -1,7 +1,7 @@ name: splitio_android description: The official Android implementation of splitio Flutter plugin. repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_android -version: 0.2.0 +version: 1.0.0 environment: sdk: ">=2.16.2 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - splitio_platform_interface: ^1.5.0 + splitio_platform_interface: ^2.0.0 dev_dependencies: flutter_test: diff --git a/splitio_android/test/splitio_android_test.dart b/splitio_android/test/splitio_android_test.dart index 554d4cd..7cecd3d 100644 --- a/splitio_android/test/splitio_android_test.dart +++ b/splitio_android/test/splitio_android_test.dart @@ -502,14 +502,20 @@ void main() { apiKey: 'api-key', matchingKey: 'matching-key', bucketingKey: 'bucketing-key', - sdkConfiguration: - SplitConfiguration(logLevel: SplitLogLevel.error, streamingEnabled: false, readyTimeout: 1)); + sdkConfiguration: SplitConfiguration( + logLevel: SplitLogLevel.error, + streamingEnabled: false, + readyTimeout: 1)); expect(methodName, 'init'); expect(methodArguments, { 'apiKey': 'api-key', 'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key', - 'sdkConfiguration': {'logLevel': 'error', 'streamingEnabled': false, 'readyTimeout': 1}, + 'sdkConfiguration': { + 'logLevel': 'error', + 'streamingEnabled': false, + 'readyTimeout': 1 + }, }); }); }); @@ -631,6 +637,7 @@ void main() { expect(impression.appliedRule, 'appliedRule'); expect(impression.changeNumber, 200); expect(impression.attributes, {}); + expect(impression.properties, {}); }), ); _simulateMethodInvocation('impressionLog', key: 'matching-key', arguments: { @@ -641,7 +648,8 @@ void main() { 'time': 3000, 'appliedRule': 'appliedRule', 'changeNumber': 200, - 'attributes': {} + 'attributes': {}, + 'properties': {}, }); }); diff --git a/splitio_ios/CHANGELOG.md b/splitio_ios/CHANGELOG.md index d8e08de..d0935b9 100644 --- a/splitio_ios/CHANGELOG.md +++ b/splitio_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.0.0 (Aug 14, 2025) +- iOS SDK to `3.3.2` + +# 1.0.0-rc.1 (Aug 14, 2025) + # 0.2.0 (Nov 6, 2024) * Updated iOS SDK to `3.0.0` diff --git a/splitio_ios/LICENSE b/splitio_ios/LICENSE index 68e2d99..af74bff 100644 --- a/splitio_ios/LICENSE +++ b/splitio_ios/LICENSE @@ -176,7 +176,7 @@ END OF TERMS AND CONDITIONS - Copyright © 2024 Split Software, Inc. + Copyright © 2025 Split Software, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/splitio_ios/example/ios/Podfile.lock b/splitio_ios/example/ios/Podfile.lock index f1c80a0..ea8a88c 100644 --- a/splitio_ios/example/ios/Podfile.lock +++ b/splitio_ios/example/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - Flutter (1.0.0) - - Split (3.0.0) - - splitio_ios (0.7.0): + - Split (3.3.2) + - splitio_ios (0.8.0): - Flutter - - Split (~> 3.0.0) + - Split (~> 3.3.2) DEPENDENCIES: - Flutter (from `Flutter`) @@ -21,9 +21,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Split: 66424040ad573d052f58269f841e71b34578a916 - splitio_ios: e4e3becbe89cae0a2fa9ca03a575c21f23af0d90 + Split: 0d4962a6c15180e1857c1a3753e1ae9c91a6150b + splitio_ios: 438ad21d0dfe467670f8b9508773b77b16a71d6b PODFILE CHECKSUM: aed42fc5c94ade572556b7ed357c5c57f1bd83a2 -COCOAPODS: 1.15.0 +COCOAPODS: 1.16.2 diff --git a/splitio_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/splitio_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e0c9630..4300fbd 100644 --- a/splitio_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/splitio_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/splitio_ios/example/ios/Runner/AppDelegate.swift b/splitio_ios/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/splitio_ios/example/ios/Runner/AppDelegate.swift +++ b/splitio_ios/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/splitio_ios/example/ios/SplitTests/ExtensionsTests.swift b/splitio_ios/example/ios/SplitTests/ExtensionsTests.swift index 8cc0e67..ea87d72 100644 --- a/splitio_ios/example/ios/SplitTests/ExtensionsTests.swift +++ b/splitio_ios/example/ios/SplitTests/ExtensionsTests.swift @@ -1,5 +1,6 @@ import XCTest import Split +@testable import splitio_ios class ExtensionsTests: XCTestCase { @@ -13,9 +14,10 @@ class ExtensionsTests: XCTestCase { impression.label = "label" impression.treatment = "on" impression.time = 16161616 + impression.properties = "{\"myProp\": true}" let impressionMap = impression.toMap() - XCTAssert(impressionMap.count == 8) + XCTAssert(impressionMap.count == 9) XCTAssert(NSDictionary(dictionary: impressionMap).isEqual(to: [ "key": "matching-key", "bucketingKey": "bucketing-key", @@ -24,6 +26,45 @@ class ExtensionsTests: XCTestCase { "appliedRule": "label", "treatment": "on", "split": "my-split", - "time": 16161616])) + "time": 16161616, + "properties": "{\"myProp\": true}"])) + } + + func testSplitViewMapping() throws { + var splitView = SplitView() + splitView.name = "my-split" + splitView.trafficType = "account" + splitView.killed = true + splitView.treatments = ["on", "off"] + splitView.changeNumber = 121212 + splitView.configs = ["key": "value"] + splitView.defaultTreatment = "off" + splitView.sets = ["set1", "set2"] + + let prerequisiteJSON = """ + { + "n": "pre1", + "t": ["on", "off"] + } + """.data(using: .utf8)! + + let prerequisite = try JSONDecoder().decode(Prerequisite.self, from: prerequisiteJSON) + splitView.prerequisites = [prerequisite] + splitView.impressionsDisabled = true + + let splitViewMap = SplitView.asMap(splitView: splitView) + + XCTAssert(splitViewMap.count == 10) + XCTAssert(NSDictionary(dictionary: splitViewMap).isEqual(to: [ + "name": "my-split", + "trafficType": "account", + "killed": true, + "treatments": ["on", "off"], + "changeNumber": 121212, + "configs": ["key": "value"], + "defaultTreatment": "off", + "sets": ["set1", "set2"], + "impressionsDisabled": true, + "prerequisites": [prerequisite]])) } } diff --git a/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift b/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift index d07edd7..2c29a06 100644 --- a/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift +++ b/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift @@ -127,12 +127,31 @@ class SplitClientConfigHelperTests: XCTestCase { ] let splitClientConfig: SplitClientConfig = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) - let actualConfig = splitClientConfig.certificatePinningConfig?.pins + let actualConfig = splitClientConfig.certificatePinningConfig!.pins - let containsPins = actualConfig?.contains { pin in - (pin.host == "host1" && pin.algo == KeyHashAlgo.sha256) && - (pin.host == "host1" && pin.algo == KeyHashAlgo.sha1) && + let containsPins = actualConfig.contains { pin in + (pin.host == "host1" && pin.algo == KeyHashAlgo.sha256) } && + actualConfig.contains { pin in + (pin.host == "host1" && pin.algo == KeyHashAlgo.sha1) } && + actualConfig.contains { pin in (pin.host == "host2" && pin.algo == KeyHashAlgo.sha256 ) } + + XCTAssertTrue(containsPins) + } + + func testRolloutCacheConfigurationValuesAreMappedCorrectly() { + let configValues = [ + "rolloutCacheConfiguration": [ + "expirationDays": 5, + "clearOnInit": true + ] + ] + + let splitClientConfig: SplitClientConfig = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) + let actualConfig = splitClientConfig.rolloutCacheConfiguration! + + XCTAssertEqual(5, actualConfig.expirationDays) + XCTAssertTrue(actualConfig.clearOnInit) } } diff --git a/splitio_ios/example/ios/SplitTests/SplitTests.swift b/splitio_ios/example/ios/SplitTests/SplitTests.swift index 7f378bb..a749dde 100644 --- a/splitio_ios/example/ios/SplitTests/SplitTests.swift +++ b/splitio_ios/example/ios/SplitTests/SplitTests.swift @@ -356,6 +356,7 @@ class SplitClientStub: SplitClient { var clearAttributesCalled: Bool = false var sdkReadyEventAction: SplitAction? + // MARK: Evaluation feature func getTreatment(_ split: String, attributes: [String: Any]?) -> String { methodCalls["getTreatment"] = true return SplitConstants.control @@ -365,7 +366,7 @@ class SplitClientStub: SplitClient { methodCalls["getTreatment"] = true return SplitConstants.control } - + func getTreatments(splits: [String], attributes: [String: Any]?) -> [String: String] { methodCalls["getTreatments"] = true return ["feature": SplitConstants.control] @@ -385,45 +386,48 @@ class SplitClientStub: SplitClient { methodCalls["getTreatmentsWithConfig"] = true return ["feature": SplitResult(treatment: SplitConstants.control)] } - - func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : String] { - methodCalls["getTreatmentsByFlagSet"] = true - return [:] + + // MARK: Evaluation with Properties (EvaluationOptions) + func getTreatment(_ split: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> String { + methodCalls["getTreatment"] = true + return SplitConstants.control } - - func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String : String] { - methodCalls["getTreatmentsByFlagSets"] = true - return [:] + + func getTreatments(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] { + methodCalls["getTreatments"] = true + return ["feature": SplitConstants.control] } - - func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : SplitResult] { - methodCalls["getTreatmentsWithConfigByFlagSet"] = true - return [:] + + func getTreatmentWithConfig(_ split: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> SplitResult { + methodCalls["getTreatmentWithConfig"] = true + return SplitResult(treatment: SplitConstants.control) } - - func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String : SplitResult] { - methodCalls["getTreatmentsWithConfigByFlagSets"] = true - return [:] + + func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { + methodCalls["getTreatmentsWithConfig"] = true + return ["feature": SplitResult(treatment: SplitConstants.control)] } - func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { + // MARK: Event handling + func on(event: SplitEvent, execute action: @escaping SplitAction) { if event == .sdkReady { sdkReadyEventAction = action } } - - func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { + + func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { if event == .sdkReady { sdkReadyEventAction = action } } - - func on(event: SplitEvent, execute action: @escaping SplitAction) { + + func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { if event == .sdkReady { sdkReadyEventAction = action } } + // MARK: Track feature func track(trafficType: String, eventType: String) -> Bool { return true } @@ -470,6 +474,7 @@ class SplitClientStub: SplitClient { return true } + // MARK: Persistent attributes feature func setAttribute(name: String, value: Any) -> Bool { attributeNameValue = name attributeValue = value @@ -499,6 +504,7 @@ class SplitClientStub: SplitClient { return true } + // MARK: Client lifecycle func flush() { methodCalls["flush"] = true } @@ -510,6 +516,48 @@ class SplitClientStub: SplitClient { func destroy(completion: (() -> Void)?) { destroyCalled = true } + + // MARK: Evaluation with flagsets + func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] { + methodCalls["getTreatmentsByFlagSet"] = true + return [:] + } + + func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: String] { + methodCalls["getTreatmentsByFlagSets"] = true + return [:] + } + + func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: SplitResult] { + methodCalls["getTreatmentsWithConfigByFlagSet"] = true + return [:] + } + + func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: SplitResult] { + methodCalls["getTreatmentsWithConfigByFlagSets"] = true + return [:] + } + + // MARK: Evaluation with flagsets and properties (EvaluationOptions) + func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] { + methodCalls["getTreatmentsByFlagSet"] = true + return [:] + } + + func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] { + methodCalls["getTreatmentsByFlagSets"] = true + return [:] + } + + func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { + methodCalls["getTreatmentsWithConfigByFlagSet"] = true + return [:] + } + + func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { + methodCalls["getTreatmentsWithConfigByFlagSets"] = true + return [:] + } } class SplitManagerStub: SplitManager, Destroyable { diff --git a/splitio_ios/example/pubspec.lock b/splitio_ios/example/pubspec.lock index 23342f6..5d236bc 100644 --- a/splitio_ios/example/pubspec.lock +++ b/splitio_ios/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -79,18 +79,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -111,34 +111,34 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -151,69 +151,69 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" splitio_ios: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.1.9" + version: "1.0.0-rc.1" splitio_platform_interface: dependency: transitive description: path: "../../splitio_platform_interface" relative: true source: path - version: "1.4.0" + version: "2.0.0-rc.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" vector_math: dependency: transitive description: @@ -226,10 +226,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "15.0.0" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/splitio_ios/ios/Classes/Extensions.swift b/splitio_ios/ios/Classes/Extensions.swift index d17250c..1c8e65e 100644 --- a/splitio_ios/ios/Classes/Extensions.swift +++ b/splitio_ios/ios/Classes/Extensions.swift @@ -1,7 +1,7 @@ import Split extension Impression { - public func toMap() -> [String: Any?] { + func toMap() -> [String: Any?] { ["key": keyName, "bucketingKey": bucketingKey, "split": feature, @@ -9,7 +9,8 @@ extension Impression { "time": time, "appliedRule": label, "changeNumber": changeNumber, - "attributes": attributes] + "attributes": attributes, + "properties": properties] } } @@ -24,7 +25,9 @@ extension SplitView { "changeNumber": splitView.changeNumber, "configs": splitView.configs, "defaultTreatment": splitView.defaultTreatment, - "sets": splitView.sets + "sets": splitView.sets, + "impressionsDisabled": splitView.impressionsDisabled, + "prerequisites": splitView.prerequisites ] } else { return [:] diff --git a/splitio_ios/ios/Classes/SplitClientConfigHelper.swift b/splitio_ios/ios/Classes/SplitClientConfigHelper.swift index 025237b..abe76d0 100644 --- a/splitio_ios/ios/Classes/SplitClientConfigHelper.swift +++ b/splitio_ios/ios/Classes/SplitClientConfigHelper.swift @@ -33,6 +33,9 @@ class SplitClientConfigHelper { static private let READY_TIMEOUT = "readyTimeout" static private let CERTIFICATE_PINNING_CONFIGURATION = "certificatePinningConfiguration" static private let CERTIFICATE_PINNING_CONFIGURATION_PINS = "pins"; + static private let ROLLOUT_CACHE_CONFIGURATION = "rolloutCacheConfiguration" + static private let ROLLOUT_CACHE_CONFIGURATION_EXPIRATION = "expirationDays" + static private let ROLLOUT_CACHE_CONFIGURATION_CLEAR_ON_INIT = "clearOnInit" static func fromMap(configurationMap: [String: Any?], impressionListener: SplitImpressionListener?) -> SplitClientConfig { let config = SplitClientConfig() @@ -117,31 +120,31 @@ class SplitClientConfigHelper { if configurationMap[SDK_ENDPOINT] != nil { if let sdkEndpoint = configurationMap[SDK_ENDPOINT] as? String { - serviceEndpointsBuilder.set(sdkEndpoint: sdkEndpoint) + _ = serviceEndpointsBuilder.set(sdkEndpoint: sdkEndpoint) } } if configurationMap[EVENTS_ENDPOINT] != nil { if let eventsEndpoint = configurationMap[EVENTS_ENDPOINT] as? String { - serviceEndpointsBuilder.set(eventsEndpoint: eventsEndpoint) + _ = serviceEndpointsBuilder.set(eventsEndpoint: eventsEndpoint) } } if configurationMap[SSE_AUTH_SERVICE_ENDPOINT] != nil { if let sseAuthServiceEndpoint = configurationMap[SSE_AUTH_SERVICE_ENDPOINT] as? String { - serviceEndpointsBuilder.set(authServiceEndpoint: sseAuthServiceEndpoint) + _ = serviceEndpointsBuilder.set(authServiceEndpoint: sseAuthServiceEndpoint) } } if configurationMap[STREAMING_SERVICE_ENDPOINT] != nil { if let streamingServiceEndpoint = configurationMap[STREAMING_SERVICE_ENDPOINT] as? String { - serviceEndpointsBuilder.set(streamingServiceEndpoint: streamingServiceEndpoint) + _ = serviceEndpointsBuilder.set(streamingServiceEndpoint: streamingServiceEndpoint) } } if configurationMap[TELEMETRY_SERVICE_ENDPOINT] != nil { if let telemetryServiceEndpoint = configurationMap[TELEMETRY_SERVICE_ENDPOINT] as? String { - serviceEndpointsBuilder.set(telemetryServiceEndpoint: telemetryServiceEndpoint) + _ = serviceEndpointsBuilder.set(telemetryServiceEndpoint: telemetryServiceEndpoint) } } @@ -234,6 +237,22 @@ class SplitClientConfigHelper { } } + if let rolloutCacheConfig = configurationMap[ROLLOUT_CACHE_CONFIGURATION] as? [String: Any?] { + let rolloutCacheConfigurationBuilder = RolloutCacheConfiguration.builder() + if rolloutCacheConfig[ROLLOUT_CACHE_CONFIGURATION_EXPIRATION] != nil { + if let expirationDays = rolloutCacheConfig[ROLLOUT_CACHE_CONFIGURATION_EXPIRATION] as? Int { + rolloutCacheConfigurationBuilder.set(expirationDays: expirationDays) + } + } + + if rolloutCacheConfig[ROLLOUT_CACHE_CONFIGURATION_CLEAR_ON_INIT] != nil { + if let clearOnInit = rolloutCacheConfig[ROLLOUT_CACHE_CONFIGURATION_CLEAR_ON_INIT] as? Bool { + rolloutCacheConfigurationBuilder.set(clearOnInit: clearOnInit) + } + } + config.rolloutCacheConfiguration = rolloutCacheConfigurationBuilder.build() + } + return config } diff --git a/splitio_ios/ios/splitio_ios.podspec b/splitio_ios/ios/splitio_ios.podspec index a593c06..bb24308 100644 --- a/splitio_ios/ios/splitio_ios.podspec +++ b/splitio_ios/ios/splitio_ios.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'splitio_ios' - s.version = '0.7.0' + s.version = '0.8.0' s.summary = 'split.io official Flutter plugin.' s.description = <<-DESC split.io official Flutter plugin. @@ -15,7 +15,7 @@ split.io official Flutter plugin. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'Split', '~> 3.0.0' + s.dependency 'Split', '~> 3.3.2' s.platform = :ios, '9.0' # Flutter.framework does not contain a i386 slice. diff --git a/splitio_ios/pubspec.yaml b/splitio_ios/pubspec.yaml index 5e77494..8c6637b 100644 --- a/splitio_ios/pubspec.yaml +++ b/splitio_ios/pubspec.yaml @@ -1,7 +1,7 @@ name: splitio_ios description: The official iOS implementation of splitio Flutter plugin. repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_ios -version: 0.2.0 +version: 1.0.0 environment: sdk: ">=2.16.2 <4.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - splitio_platform_interface: ^1.5.0 + splitio_platform_interface: ^2.0.0 dev_dependencies: flutter_test: diff --git a/splitio_ios/test/splitio_ios_test.dart b/splitio_ios/test/splitio_ios_test.dart index ab04f62..df312aa 100644 --- a/splitio_ios/test/splitio_ios_test.dart +++ b/splitio_ios/test/splitio_ios_test.dart @@ -190,7 +190,6 @@ void main() { }); }); - test('getTreatmentsByFlagSet without attributes', () async { _platform.getTreatmentsByFlagSet( matchingKey: 'matching-key', @@ -503,14 +502,20 @@ void main() { apiKey: 'api-key', matchingKey: 'matching-key', bucketingKey: 'bucketing-key', - sdkConfiguration: - SplitConfiguration(logLevel: SplitLogLevel.error, streamingEnabled: false, readyTimeout: 1)); + sdkConfiguration: SplitConfiguration( + logLevel: SplitLogLevel.error, + streamingEnabled: false, + readyTimeout: 1)); expect(methodName, 'init'); expect(methodArguments, { 'apiKey': 'api-key', 'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key', - 'sdkConfiguration': {'logLevel': 'error', 'streamingEnabled': false, 'readyTimeout': 1}, + 'sdkConfiguration': { + 'logLevel': 'error', + 'streamingEnabled': false, + 'readyTimeout': 1 + }, }); }); }); @@ -632,6 +637,7 @@ void main() { expect(impression.appliedRule, 'appliedRule'); expect(impression.changeNumber, 200); expect(impression.attributes, {}); + expect(impression.properties, {}); }), ); _simulateMethodInvocation('impressionLog', key: 'matching-key', arguments: { @@ -642,11 +648,11 @@ void main() { 'time': 3000, 'appliedRule': 'appliedRule', 'changeNumber': 200, - 'attributes': {} + 'attributes': {}, + 'properties': {} }); }); - group('userConsent', () { test('get user consent', () async { UserConsent userConsent = await _platform.getUserConsent(); diff --git a/splitio_platform_interface/CHANGELOG.md b/splitio_platform_interface/CHANGELOG.md index 3e5e7e1..cf67b5e 100644 --- a/splitio_platform_interface/CHANGELOG.md +++ b/splitio_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.0.0 (Aug 14, 2025) + +# 2.0.0-rc.1 (Aug 14, 2025) + # 1.5.0 (Oct 18, 2024) * Added certificate pinning functionality. This feature allows you to pin a certificate to the SDK, ensuring that the SDK only communicates with servers that present this certificate. Read more in our documentation. diff --git a/splitio_platform_interface/LICENSE b/splitio_platform_interface/LICENSE index 68e2d99..af74bff 100644 --- a/splitio_platform_interface/LICENSE +++ b/splitio_platform_interface/LICENSE @@ -176,7 +176,7 @@ END OF TERMS AND CONDITIONS - Copyright © 2024 Split Software, Inc. + Copyright © 2025 Split Software, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/splitio_platform_interface/lib/method_channel_platform.dart b/splitio_platform_interface/lib/method_channel_platform.dart index d3fb11d..aaf0b2c 100644 --- a/splitio_platform_interface/lib/method_channel_platform.dart +++ b/splitio_platform_interface/lib/method_channel_platform.dart @@ -22,6 +22,19 @@ class MethodChannelPlatform extends SplitioPlatform { } } + Map _withEvalOptions( + String matchingKey, + String? bucketingKey, { + required Map base, + required EvaluationOptions evaluationOptions, + }) { + final args = Map.from(base); + if (evaluationOptions.properties.isNotEmpty) { + args['evaluationOptions'] = evaluationOptions.toJson(); + } + return _buildParameters(matchingKey, bucketingKey, args); + } + @override Future init( {required String apiKey, @@ -107,11 +120,19 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) async { - return await methodChannel.invokeMethod( - 'getTreatment', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitName, 'attributes': attributes})) ?? + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitName, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + return await methodChannel.invokeMethod('getTreatment', params) ?? _controlTreatment; } @@ -120,14 +141,23 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) async { - Map? treatment = (await methodChannel.invokeMapMethod( - 'getTreatmentWithConfig', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitName, 'attributes': attributes}))) - ?.entries - .first - .value; + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitName, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatment = + (await methodChannel.invokeMapMethod('getTreatmentWithConfig', params)) + ?.entries + .first + .value; if (treatment == null) { return _controlResult; } @@ -140,11 +170,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatments', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitNames, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitNames, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatments', params); return treatments ?.map((key, value) => MapEntry(key, value)) ?? @@ -156,11 +195,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsWithConfig', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitNames, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitNames, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatmentsWithConfig', params); return treatments?.map((key, value) => MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? @@ -172,11 +220,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsByFlagSet', - _buildParameters(matchingKey, bucketingKey, - {'flagSet': flagSet, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSet': flagSet, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatmentsByFlagSet', params); return treatments ?.map((key, value) => MapEntry(key, value)) ?? @@ -188,11 +245,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsByFlagSets', - _buildParameters(matchingKey, bucketingKey, - {'flagSets': flagSets, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSets': flagSets, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatmentsByFlagSets', params); return treatments ?.map((key, value) => MapEntry(key, value)) ?? @@ -204,11 +270,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) async { + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSet': flagSet, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsWithConfigByFlagSet', - _buildParameters(matchingKey, bucketingKey, - {'flagSet': flagSet, 'attributes': attributes})); + 'getTreatmentsWithConfigByFlagSet', params); return treatments?.map((key, value) => MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? @@ -220,14 +295,23 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) async { + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSets': flagSets, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsWithConfigByFlagSets', - _buildParameters(matchingKey, bucketingKey, - {'flagSets': flagSets, 'attributes': attributes})); + 'getTreatmentsWithConfigByFlagSets', params); return treatments?.map((key, value) => - MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? + MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? {}; } diff --git a/splitio_platform_interface/lib/split_configuration.dart b/splitio_platform_interface/lib/split_configuration.dart index e6a12fc..ffe9a9d 100644 --- a/splitio_platform_interface/lib/split_configuration.dart +++ b/splitio_platform_interface/lib/split_configuration.dart @@ -1,5 +1,6 @@ import 'package:splitio_platform_interface/split_certificate_pinning_configuration.dart'; import 'package:splitio_platform_interface/split_sync_config.dart'; +import 'package:splitio_platform_interface/split_rollout_cache_configuration.dart'; class SplitConfiguration { final Map configurationMap = {}; @@ -74,6 +75,7 @@ class SplitConfiguration { SplitLogLevel? logLevel, int? readyTimeout = 10, CertificatePinningConfiguration? certificatePinningConfiguration, + RolloutCacheConfiguration? rolloutCacheConfiguration, }) { if (featuresRefreshRate != null) { configurationMap['featuresRefreshRate'] = featuresRefreshRate; @@ -186,6 +188,13 @@ class SplitConfiguration { 'pins': certificatePinningConfiguration.pins }; } + + if (rolloutCacheConfiguration != null) { + configurationMap['rolloutCacheConfiguration'] = { + 'expirationDays': rolloutCacheConfiguration.expirationDays, + 'clearOnInit': rolloutCacheConfiguration.clearOnInit + }; + } } } diff --git a/splitio_platform_interface/lib/split_evaluation_options.dart b/splitio_platform_interface/lib/split_evaluation_options.dart new file mode 100644 index 0000000..2a603a8 --- /dev/null +++ b/splitio_platform_interface/lib/split_evaluation_options.dart @@ -0,0 +1,20 @@ +import 'dart:collection'; + +class EvaluationOptions { + final Map _properties; + + Map get properties => UnmodifiableMapView(_properties); + + const EvaluationOptions.empty() : _properties = const {}; + + factory EvaluationOptions([Map properties = const {}]) { + return EvaluationOptions._( + Map.unmodifiable(Map.from(properties))); + } + + Map toJson() => { + 'properties': _properties, + }; + + const EvaluationOptions._(this._properties); +} diff --git a/splitio_platform_interface/lib/split_impression.dart b/splitio_platform_interface/lib/split_impression.dart index 6deec0b..3bfdf4e 100644 --- a/splitio_platform_interface/lib/split_impression.dart +++ b/splitio_platform_interface/lib/split_impression.dart @@ -7,11 +7,18 @@ class Impression { final String? appliedRule; final num? changeNumber; final Map attributes; + final Map? properties; Impression(this.key, this.bucketingKey, this.split, this.treatment, this.time, - this.appliedRule, this.changeNumber, this.attributes); + this.appliedRule, this.changeNumber, this.attributes, this.properties); static Impression fromMap(Map map) { + var properties = null; + if (map['properties'] != null) { + properties = Map.from(map['properties'] as Map); + } else { + properties = null; + } return Impression( map['key'] as String?, map['bucketingKey'] as String?, @@ -20,11 +27,12 @@ class Impression { map['time'] as num?, map['appliedRule'] as String?, map['changeNumber'] as num?, - Map.from(map['attributes'] as Map)); + Map.from(map['attributes'] as Map), + properties); } @override String toString() { - return 'Impression = {"key":$key, "bucketingKey":$bucketingKey, "split":$split, "treatment":$treatment, "time":$time, "appliedRule": $appliedRule, "changeNumber":$changeNumber, "attributes":$attributes}'; + return 'Impression = {"key":$key, "bucketingKey":$bucketingKey, "split":$split, "treatment":$treatment, "time":$time, "appliedRule": $appliedRule, "changeNumber":$changeNumber, "attributes":$attributes, "properties":$properties}'; } } diff --git a/splitio_platform_interface/lib/split_prerequisite.dart b/splitio_platform_interface/lib/split_prerequisite.dart new file mode 100644 index 0000000..b8be081 --- /dev/null +++ b/splitio_platform_interface/lib/split_prerequisite.dart @@ -0,0 +1,49 @@ +class Prerequisite { + final String _name; + + final Set _treatments; + + String get name => _name; + + Set get treatments => _treatments; + + Prerequisite(this._name, this._treatments); + + static Prerequisite fromEntry(el) { + final String name = (el['n'] ?? el['n:'] ?? '').toString(); + final List rawTreatments = (el['t'] as List?) ?? []; + final Set treatments = + rawTreatments.map((e) => e.toString()).toSet(); + + return Prerequisite(name, treatments); + } + + @override + String toString() { + return '''Prerequisite = { + name: $name, + treatments: $treatments + }'''; + } + + equals(Prerequisite other) { + return name == other.name && treatments == other.treatments; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Prerequisite && + name == other.name && + other.treatments.containsAll(treatments); + } + + @override + int get hashCode { + int treatmentsHash = 0; + for (final t in _treatments) { + treatmentsHash ^= t.hashCode; + } + return name.hashCode ^ treatmentsHash; + } +} diff --git a/splitio_platform_interface/lib/split_rollout_cache_configuration.dart b/splitio_platform_interface/lib/split_rollout_cache_configuration.dart new file mode 100644 index 0000000..9c2b0f3 --- /dev/null +++ b/splitio_platform_interface/lib/split_rollout_cache_configuration.dart @@ -0,0 +1,16 @@ +class RolloutCacheConfiguration { + + late int _expirationDays; + late bool _clearOnInit; + + int get expirationDays => _expirationDays; + bool get clearOnInit => _clearOnInit; + + RolloutCacheConfiguration({int expirationDays = 10, bool clearOnInit = false}) { + if (expirationDays < 1) { + expirationDays = 10; + } + _expirationDays = expirationDays; + _clearOnInit = clearOnInit; + } +} diff --git a/splitio_platform_interface/lib/split_view.dart b/splitio_platform_interface/lib/split_view.dart index 789daa1..8908cf6 100644 --- a/splitio_platform_interface/lib/split_view.dart +++ b/splitio_platform_interface/lib/split_view.dart @@ -1,18 +1,36 @@ import 'dart:core'; +import 'package:splitio_platform_interface/split_prerequisite.dart'; + class SplitView { + static const String _keyName = 'name'; + static const String _keyTrafficType = 'trafficType'; + static const String _keyKilled = 'killed'; + static const String _keyTreatments = 'treatments'; + static const String _keyChangeNumber = 'changeNumber'; + static const String _keyConfigs = 'configs'; + static const String _keyDefaultTreatment = 'defaultTreatment'; + static const String _keySets = 'sets'; + static const String _keyImpressionsDisabled = 'impressionsDisabled'; + static const String _keyPrerequisites = 'prerequisites'; + String name; String trafficType; bool killed = false; List treatments = []; - int changeNumber; + int? changeNumber; Map configs = {}; String defaultTreatment; List sets = []; + bool impressionsDisabled = false; + Set prerequisites = {}; SplitView(this.name, this.trafficType, this.killed, this.treatments, this.changeNumber, this.configs, - [this.defaultTreatment = '', this.sets = const []]); + [this.defaultTreatment = '', + this.sets = const [], + this.impressionsDisabled = false, + this.prerequisites = const {}]); static SplitView? fromEntry(Map? entry) { if (entry == null || entry.isEmpty) { @@ -20,41 +38,55 @@ class SplitView { } final Map mappedConfig = {}; - entry['configs']?.entries.forEach((MapEntry entry) => { + entry[_keyConfigs]?.entries.forEach((MapEntry entry) => { mappedConfig.addAll({entry.key.toString(): entry.value.toString()}) }); - if (entry['treatments'] == null) { - entry['treatments'] = entry['treatments'] ?? []; + if (entry[_keyTreatments] == null) { + entry[_keyTreatments] = entry[_keyTreatments] ?? []; + } + + if (entry[_keySets] == null) { + entry[_keySets] = []; } - if (entry['sets'] == null) { - entry['sets'] = []; + if (entry[_keyImpressionsDisabled] == null) { + entry[_keyImpressionsDisabled] = false; } + if (entry[_keyPrerequisites] == null) { + entry[_keyPrerequisites] = []; + } + + final List prereqRaw = (entry[_keyPrerequisites] as List?) ?? []; + final Set prerequisites = + prereqRaw.map((el) => Prerequisite.fromEntry(el)).toSet(); + return SplitView( - entry['name'], - entry['trafficType'], - entry['killed'], - (entry['treatments'] as List).map((el) => el as String).toList(), - entry['changeNumber'], + entry[_keyName], + entry[_keyTrafficType], + entry[_keyKilled], + (entry[_keyTreatments] as List).map((el) => el as String).toList(), + entry[_keyChangeNumber], mappedConfig, - entry['defaultTreatment'] ?? '', - (entry['sets'] as List).map((el) => el as String).toList() - ); + entry[_keyDefaultTreatment] ?? '', + (entry[_keySets] as List).map((el) => el as String).toList(), + entry[_keyImpressionsDisabled] ?? false, + prerequisites); } @override String toString() { return '''SplitView = { - name: $name, - trafficType: $trafficType, - killed: $killed, - treatments: ${treatments.toString()}, - changeNumber: $changeNumber, - config: $configs, - defaultTreatment: $defaultTreatment, - sets: ${sets.toString()} + $_keyName: $name, + $_keyTrafficType: $trafficType, + $_keyKilled: $killed, + $_keyTreatments: ${treatments.toString()}, + $_keyChangeNumber: $changeNumber, + $_keyConfigs: $configs, + $_keyDefaultTreatment: $defaultTreatment, + $_keySets: ${sets.toString()}, + $_keyImpressionsDisabled: $impressionsDisabled }'''; } } diff --git a/splitio_platform_interface/lib/splitio_platform_interface.dart b/splitio_platform_interface/lib/splitio_platform_interface.dart index a367fdf..247c3a2 100644 --- a/splitio_platform_interface/lib/splitio_platform_interface.dart +++ b/splitio_platform_interface/lib/splitio_platform_interface.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:splitio_platform_interface/method_channel_platform.dart'; import 'package:splitio_platform_interface/split_configuration.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; import 'package:splitio_platform_interface/split_impression.dart'; import 'package:splitio_platform_interface/split_result.dart'; import 'package:splitio_platform_interface/split_view.dart'; @@ -12,7 +13,9 @@ export 'package:splitio_platform_interface/impressions/impressions_method_call_h export 'package:splitio_platform_interface/method_call_handler.dart'; export 'package:splitio_platform_interface/method_channel_platform.dart'; export 'package:splitio_platform_interface/split_configuration.dart'; +export 'package:splitio_platform_interface/split_evaluation_options.dart'; export 'package:splitio_platform_interface/split_impression.dart'; +export 'package:splitio_platform_interface/split_prerequisite.dart'; export 'package:splitio_platform_interface/split_result.dart'; export 'package:splitio_platform_interface/split_view.dart'; export 'package:splitio_platform_interface/splitio_platform_interface.dart'; @@ -62,7 +65,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -70,7 +74,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -78,7 +83,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -86,7 +92,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -94,7 +101,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -102,7 +110,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -110,7 +119,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -118,7 +128,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } diff --git a/splitio_platform_interface/pubspec.yaml b/splitio_platform_interface/pubspec.yaml index 72db712..6b2a0e1 100644 --- a/splitio_platform_interface/pubspec.yaml +++ b/splitio_platform_interface/pubspec.yaml @@ -2,7 +2,7 @@ name: splitio_platform_interface description: A common platform interface for the splitio plugin. # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.5.0 +version: 2.0.0 repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_platform_interface environment: diff --git a/splitio_platform_interface/test/method_channel_platform_test.dart b/splitio_platform_interface/test/method_channel_platform_test.dart index 5dceddf..3692397 100644 --- a/splitio_platform_interface/test/method_channel_platform_test.dart +++ b/splitio_platform_interface/test/method_channel_platform_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:splitio_platform_interface/method_channel_platform.dart'; import 'package:splitio_platform_interface/split_configuration.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; void main() { const MethodChannel _channel = MethodChannel('splitio'); @@ -65,6 +66,184 @@ void main() { }); }); + group('evaluationOptions serialization', () { + test('getTreatment includes evaluationOptions when non-empty', () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentWithConfig includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatments includes evaluationOptions when non-empty', () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentsWithConfig includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentsByFlagSet includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsByFlagSet( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSet: 'set_1', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsByFlagSet'); + expect(methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentsByFlagSets includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsByFlagSets( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSets: ['set_1', 'set_2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsByFlagSets'); + expect(methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSet includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsWithConfigByFlagSet( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSet: 'set_1', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsWithConfigByFlagSet'); + expect(methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSets includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsWithConfigByFlagSets( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSets: ['set_1', 'set_2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsWithConfigByFlagSets'); + expect(methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + }); + group('evaluation', () { test('getTreatment without attributes', () async { _platform.getTreatment( @@ -509,7 +688,11 @@ void main() { 'apiKey': 'api-key', 'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key', - 'sdkConfiguration': {'logLevel': 'debug', 'streamingEnabled': false, 'readyTimeout' : 10}, + 'sdkConfiguration': { + 'logLevel': 'debug', + 'streamingEnabled': false, + 'readyTimeout': 10 + }, }); }); }); @@ -631,6 +814,7 @@ void main() { expect(impression.appliedRule, 'appliedRule'); expect(impression.changeNumber, 200); expect(impression.attributes, {}); + expect(impression.properties, {'prop1': 'value1', 'prop2': 'value2'}); }), ); _simulateMethodInvocation('impressionLog', key: 'matching-key', arguments: { @@ -641,7 +825,8 @@ void main() { 'time': 3000, 'appliedRule': 'appliedRule', 'changeNumber': 200, - 'attributes': {} + 'attributes': {}, + 'properties': {'prop1': 'value1', 'prop2': 'value2'} }); }); diff --git a/splitio_platform_interface/test/prerequisites_test.dart b/splitio_platform_interface/test/prerequisites_test.dart new file mode 100644 index 0000000..c3c0909 --- /dev/null +++ b/splitio_platform_interface/test/prerequisites_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/split_prerequisite.dart'; + +void main() { + group('Prerequisite', () { + test('fromEntry creates correct instance', () { + final entry = { + 'n': 'feature1', + 't': ['on', 'off'] + }; + final prereq = Prerequisite.fromEntry(entry); + expect(prereq.name, 'feature1'); + expect(prereq.treatments, {'on', 'off'}); + }); + + test('equality and hashCode', () { + final a = Prerequisite('feat', {'a', 'b'}); + final b = Prerequisite('feat', {'b', 'a'}); + final c = Prerequisite('feat2', {'a', 'b'}); + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + expect(a, isNot(equals(c))); + }); + + test('toString contains name and treatments', () { + final prereq = Prerequisite('myFeature', {'t1'}); + final str = prereq.toString(); + expect(str, contains('myFeature')); + expect(str, contains('t1')); + }); + }); +} diff --git a/splitio_platform_interface/test/rollout_cache_configuration_test.dart b/splitio_platform_interface/test/rollout_cache_configuration_test.dart new file mode 100644 index 0000000..c7c3306 --- /dev/null +++ b/splitio_platform_interface/test/rollout_cache_configuration_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/split_rollout_cache_configuration.dart'; + +void main() { + test('negative expirationDays defaults to 10', () { + var config = RolloutCacheConfiguration(expirationDays: -1); + + expect(config.expirationDays, 10); + }); + + test('zero expirationDays defaults to 10', () { + var config = RolloutCacheConfiguration(expirationDays: 0); + + expect(config.expirationDays, 10); + }); + + test('default values', () { + var config = RolloutCacheConfiguration(); + + expect(config.expirationDays, 10); + expect(config.clearOnInit, false); + }); +} \ No newline at end of file diff --git a/splitio_platform_interface/test/split_evaluation_options_test.dart b/splitio_platform_interface/test/split_evaluation_options_test.dart new file mode 100644 index 0000000..a473c33 --- /dev/null +++ b/splitio_platform_interface/test/split_evaluation_options_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; + +void main() { + group('EvaluationOptions', () { + test('default constructor has empty properties and is unmodifiable', () { + final eo = EvaluationOptions(); + expect(eo.properties, isEmpty); + expect(() => eo.properties['x'] = 1, throwsA(isA())); + expect(() => eo.properties.remove('x'), throwsA(isA())); + expect(() => eo.properties.clear(), throwsA(isA())); + }); + + test('constructor with properties stores them', () { + final props = {'a': 1, 'b': true, 'c': 'x'}; + final eo = EvaluationOptions(props); + expect(eo.properties, equals(props)); + }); + + test('internal map is decoupled from external map', () { + final props = {'k': 42}; + final eo = EvaluationOptions(props); + // Not the same instance + expect(identical(eo.properties, props), isFalse); + // Mutating the original does not change eo.properties + props['k'] = 99; + expect(eo.properties['k'], 42); + }); + + test('properties getter is unmodifiable', () { + final eo = EvaluationOptions({'k': 1}); + expect(() => eo.properties['k'] = 2, throwsA(isA())); + expect(() => eo.properties.addAll({'z': 0}), + throwsA(isA())); + }); + + test('toJson returns a map with properties key', () { + final props = {'x': 1, 'y': 'z'}; + final eo = EvaluationOptions(props); + final json = eo.toJson(); + expect(json.keys, contains('properties')); + expect(json['properties'], equals(props)); + }); + + test('toJson returns empty properties for empty options', () { + const eo = EvaluationOptions.empty(); + final json = eo.toJson(); + expect(json, equals({'properties': const {}})); + }); + }); +} diff --git a/splitio_platform_interface/test/split_impression_test.dart b/splitio_platform_interface/test/split_impression_test.dart index b993d2a..689599d 100644 --- a/splitio_platform_interface/test/split_impression_test.dart +++ b/splitio_platform_interface/test/split_impression_test.dart @@ -12,6 +12,7 @@ void main() { 'appliedRule': 'appliedRule', 'changeNumber': 12512512, 'attributes': {'good': true}, + 'properties': {'bad': false, 'number': 10.5}, }; Impression expectedImpression = Impression( @@ -23,6 +24,7 @@ void main() { 'appliedRule', 12512512, {'good': true}, + {'bad': false, 'number': 10.5}, ); Impression impression = Impression.fromMap(sourceMap); @@ -34,5 +36,6 @@ void main() { expect(impression.appliedRule, expectedImpression.appliedRule); expect(impression.changeNumber, expectedImpression.changeNumber); expect(impression.attributes, expectedImpression.attributes); + expect(impression.properties, expectedImpression.properties); }); } diff --git a/splitio_platform_interface/test/split_view_test.dart b/splitio_platform_interface/test/split_view_test.dart index 9a5aee1..f3040b5 100644 --- a/splitio_platform_interface/test/split_view_test.dart +++ b/splitio_platform_interface/test/split_view_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/split_prerequisite.dart'; import 'package:splitio_platform_interface/split_view.dart'; void main() { @@ -24,6 +25,17 @@ void main() { 'trafficType': 'default', 'defaultTreatment': 'on', 'sets': ['set1', 'set2'], + 'impressionsDisabled': true, + 'prerequisites': [ + { + 'n:': 'pre1', + 't': ['on', 'off'] + }, + { + 'n': 'pre2', + 't': ['off'] + } + ], }); expect(splitView?.name, 'my_split'); @@ -34,5 +46,10 @@ void main() { expect(splitView?.trafficType, 'default'); expect(splitView?.defaultTreatment, 'on'); expect(splitView?.sets, ['set1', 'set2']); + expect(splitView?.impressionsDisabled, true); + expect(splitView?.prerequisites, { + Prerequisite('pre1', {'on', 'off'}), + Prerequisite('pre2', {'off'}) + }); }); } diff --git a/splitio_platform_interface/test/splitio_configuration_test.dart b/splitio_platform_interface/test/splitio_configuration_test.dart index a98d9ce..64b758c 100644 --- a/splitio_platform_interface/test/splitio_configuration_test.dart +++ b/splitio_platform_interface/test/splitio_configuration_test.dart @@ -1,6 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:splitio_platform_interface/split_certificate_pinning_configuration.dart'; import 'package:splitio_platform_interface/split_configuration.dart'; +import 'package:splitio_platform_interface/split_rollout_cache_configuration.dart'; import 'package:splitio_platform_interface/split_sync_config.dart'; void main() { @@ -34,7 +35,8 @@ void main() { certificatePinningConfiguration: CertificatePinningConfiguration() .addPin('host1', 'pin1') .addPin('host2', 'pin3') - .addPin('host1', 'pin2')); + .addPin('host1', 'pin2'), + rolloutCacheConfiguration: RolloutCacheConfiguration(expirationDays: 15, clearOnInit: true)); expect(config.configurationMap['eventFlushInterval'], 2000); expect(config.configurationMap['eventsPerPush'], 300); @@ -71,6 +73,8 @@ void main() { 'host1': ['pin1', 'pin2'], 'host2': ['pin3'] }); + expect(config.configurationMap['rolloutCacheConfiguration']['expirationDays'], 15); + expect(config.configurationMap['rolloutCacheConfiguration']['clearOnInit'], true); }); test('no special values leaves map empty', () async {