diff --git a/splitio/CHANGELOG.md b/splitio/CHANGELOG.md index 5fe4e9a..371938a 100644 --- a/splitio/CHANGELOG.md +++ b/splitio/CHANGELOG.md @@ -1,15 +1,26 @@ +# 1.2.1 (Jan 28, 2026) +* Bug fix: corrected asset path for loading the Browser SDK. + +# 1.2.1-rc.1 (Jan 28, 2026) + +# 1.2.0 (Jan 28, 2026) +* Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. +* Updated Android SDK to `5.4.2`, iOS SDK to `3.6.0`, and Browser SDK to `1.6.1` + +# 1.2.0-rc.1 (Jan 28, 2026) + # 1.1.0 (Jan 16, 2026) -- Added Web support via the `splitio_web` package, the Web implementation of `splitio` based on the Split Browser SDK `1.6.0`. +* Added Web support via the `splitio_web` package, the Web implementation of `splitio` based on the Split Browser SDK `1.6.0`. # 1.1.0-rc.1 (Jan 15, 2026) # 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. +* 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) diff --git a/splitio/analysis_options.yaml b/splitio/analysis_options.yaml index a5744c1..5cf0db1 100644 --- a/splitio/analysis_options.yaml +++ b/splitio/analysis_options.yaml @@ -2,3 +2,7 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +linter: + rules: + - public_member_api_docs diff --git a/splitio/example/ios/Podfile.lock b/splitio/example/ios/Podfile.lock index e004592..c23d777 100644 --- a/splitio/example/ios/Podfile.lock +++ b/splitio/example/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - Flutter (1.0.0) - - Split (3.3.2) - - splitio_ios (0.8.0): + - Split (3.6.0) + - splitio_ios (0.9.0): - Flutter - - Split (~> 3.3.2) + - Split (~> 3.6.0) DEPENDENCIES: - Flutter (from `Flutter`) @@ -20,9 +20,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/splitio_ios/ios" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Split: 0d4962a6c15180e1857c1a3753e1ae9c91a6150b - splitio_ios: 438ad21d0dfe467670f8b9508773b77b16a71d6b + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + Split: 1e2176aacd6421029bea41413401389d86e3d50a + splitio_ios: ad4f484a6c478bf7285e417ea8252371f66b54ff PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 diff --git a/splitio/example/lib/main.dart b/splitio/example/lib/main.dart index 998a90b..a27a0a5 100644 --- a/splitio/example/lib/main.dart +++ b/splitio/example/lib/main.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'package:flutter/material.dart'; import 'package:splitio/splitio.dart'; diff --git a/splitio/example/pubspec.lock b/splitio/example/pubspec.lock index 2e7d49c..c944234 100644 --- a/splitio/example/pubspec.lock +++ b/splitio/example/pubspec.lock @@ -171,39 +171,39 @@ packages: path: ".." relative: true source: path - version: "1.1.0" + version: "1.2.1" splitio_android: dependency: transitive description: name: splitio_android - sha256: "344bf82de6694cffb8dd80a96ee734e31bb838d2b4693fb046e0fc98e31512ca" + sha256: "7c0e5ad4ccdf4990120a7a6a2f067d274c107ab9830fd20b1d54bf3f0392ac02" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" splitio_ios: dependency: transitive description: name: splitio_ios - sha256: "1c078bc49bf7b30df6ca50accb6a9eecf592ec607e20e77b1c6ecdabc7a44dc9" + sha256: c15cd7dfd195df107ae07c030fd742dcabb0610c769466d6d7e3890d22a91bc2 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" splitio_platform_interface: dependency: transitive description: name: splitio_platform_interface - sha256: "8bcb1cab9f5fffb7b79cfeeaf6c80d82f8ede55c8d6ca7578ec78653f3f9e499" + sha256: faa022814c7b2fc7b5f68b80b088e1638e06783419075176d1adadcf787897db url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" splitio_web: dependency: transitive description: name: splitio_web - sha256: "29dc1a55d80c026afb0f0bad378c81e44ee1e9da5faaf9e931f35b5dfd3ff5f3" + sha256: "656cf71d4ec900cf1ad1b9eae20bbabe797de5c99f49584672161de9fe133afb" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" stack_trace: dependency: transitive description: diff --git a/splitio/lib/split_client.dart b/splitio/lib/split_client.dart index 883a9f4..cfa2c00 100644 --- a/splitio/lib/split_client.dart +++ b/splitio/lib/split_client.dart @@ -1,5 +1,6 @@ import 'package:splitio_platform_interface/splitio_platform_interface.dart'; +/// Abstract class representing a Split client. abstract class SplitClient { /// Performs an evaluation for the [featureFlagName] feature flag. /// @@ -187,11 +188,13 @@ abstract class SplitClient { Future whenTimeout(); } +/// Default implementation of the Split client. class DefaultSplitClient implements SplitClient { final SplitioPlatform _platform; final String _matchingKey; final String? _bucketingKey; + /// Creates a new instance of the Split client. DefaultSplitClient(this._platform, this._matchingKey, this._bucketingKey); @override diff --git a/splitio/lib/splitio.dart b/splitio/lib/splitio.dart index a037b11..ae56ab1 100644 --- a/splitio/lib/splitio.dart +++ b/splitio/lib/splitio.dart @@ -12,9 +12,13 @@ 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'; export 'package:splitio_platform_interface/split_rollout_cache_configuration.dart'; +export 'package:splitio_platform_interface/split_fallback_treatment.dart'; +export 'package:splitio_platform_interface/split_fallback_treatments_configuration.dart'; +/// Callback function type for client readiness events. typedef ClientReadinessCallback = void Function(SplitClient splitClient); +/// Main class for interacting with the Split Flutter SDK. class Splitio { final String _sdkKey; @@ -112,12 +116,14 @@ class Splitio { return client; } + /// Gets the list of all feature flag names. Future> splitNames() async { List splitNames = await _platform.splitNames(); return splitNames; } + /// Gets the list of all feature flag views. Future> splits() async { return _platform.splits(); } @@ -128,14 +134,22 @@ class Splitio { return _platform.impressionsStream(); } + /// Gets a specific feature flag view. + /// + /// Returns null if the provided feature flag name is not found. Future split(String splitName) async { return _platform.split(splitName: splitName); } + /// Gets the user consent status. Future getUserConsent() async { return _platform.getUserConsent(); } + /// Sets the user consent status. + /// + /// [enabled] is a boolean that enables (`UserConsent.granted`) + /// or disables (`UserConsent.declined`) data collection. Future setUserConsent(bool enabled) async { return _platform.setUserConsent(enabled); } diff --git a/splitio/pubspec.yaml b/splitio/pubspec.yaml index a762f9f..89f2b62 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: 1.1.0 +version: 1.2.1 homepage: https://split.io/ repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio/ @@ -21,10 +21,10 @@ flutter: dependencies: flutter: sdk: flutter - splitio_android: ^1.0.0 - splitio_ios: ^1.0.0 - splitio_web: ^1.0.0 - splitio_platform_interface: ^2.0.0 + splitio_android: ^1.1.0 + splitio_ios: ^1.1.0 + splitio_web: ^1.1.1 + splitio_platform_interface: ^2.1.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/splitio_android/CHANGELOG.md b/splitio_android/CHANGELOG.md index 6f47087..aa4545a 100644 --- a/splitio_android/CHANGELOG.md +++ b/splitio_android/CHANGELOG.md @@ -1,5 +1,11 @@ +# 1.1.0 (Jan 28, 2026) +* Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. +* Updated Android SDK to `5.4.2`. + +# 1.1.0-rc.1 (Jan 28, 2026) + # 1.0.0 (Aug 14, 2025) -- Updated Android SDK to `5.3.1`. +* Updated Android SDK to `5.3.1`. # 1.0.0-rc.1 (Aug 14, 2025) diff --git a/splitio_android/analysis_options.yaml b/splitio_android/analysis_options.yaml index a5744c1..5cf0db1 100644 --- a/splitio_android/analysis_options.yaml +++ b/splitio_android/analysis_options.yaml @@ -2,3 +2,7 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +linter: + rules: + - public_member_api_docs diff --git a/splitio_android/android/build.gradle b/splitio_android/android/build.gradle index 0cd5200..723d466 100644 --- a/splitio_android/android/build.gradle +++ b/splitio_android/android/build.gradle @@ -38,7 +38,7 @@ android { } dependencies { - implementation 'io.split.client:android-client:5.3.1' + implementation 'io.split.client:android-client:5.4.2' 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/SplitClientConfigHelper.java b/splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java index 2df8e0d..da35b35 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 @@ -19,6 +19,8 @@ import io.split.android.client.network.CertificatePinningConfiguration; import io.split.android.client.shared.UserConsent; import io.split.android.client.utils.logger.SplitLogLevel; +import io.split.android.client.fallback.FallbackTreatmentsConfiguration; +import io.split.android.client.fallback.FallbackTreatment; class SplitClientConfigHelper { @@ -55,6 +57,11 @@ class SplitClientConfigHelper { 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"; + private static final String FALLBACK_TREATMENTS = "fallbackTreatments"; + private static final String FALLBACK_TREATMENTS_GLOBAL = "global"; + private static final String FALLBACK_TREATMENTS_BY_FLAG = "byFlag"; + private static final String FALLBACK_TREATMENT_VALUE = "treatment"; + private static final String FALLBACK_TREATMENT_CONFIG = "config"; /** * Creates a {@link SplitClientConfig} object from a map. @@ -262,6 +269,29 @@ static SplitClientConfig fromMap(@NonNull Map configurationMap, } } + Map fallbackTreatments = getObjectMap(configurationMap, FALLBACK_TREATMENTS); + if (fallbackTreatments != null) { + Map global = getObjectMap(fallbackTreatments, FALLBACK_TREATMENTS_GLOBAL); + Map byFlag = getObjectMap(fallbackTreatments, FALLBACK_TREATMENTS_BY_FLAG); + if (global != null || byFlag != null) { + FallbackTreatmentsConfiguration.Builder fallbackTreatmentsBuilder = FallbackTreatmentsConfiguration.builder(); + if (global != null) { + fallbackTreatmentsBuilder.global(new FallbackTreatment(getString(global, FALLBACK_TREATMENT_VALUE), getString(global, FALLBACK_TREATMENT_CONFIG))); + } + if (byFlag != null) { + Map byFlagMap = new HashMap<>(); + for (Map.Entry entry : byFlag.entrySet()) { + Map byFlagFallbackTreatment = getObjectMap(byFlag, entry.getKey()); + if (byFlagFallbackTreatment != null) { + byFlagMap.put(entry.getKey(), new FallbackTreatment(getString(byFlagFallbackTreatment, FALLBACK_TREATMENT_VALUE), getString(byFlagFallbackTreatment, FALLBACK_TREATMENT_CONFIG))); + } + } + fallbackTreatmentsBuilder.byFlag(byFlagMap); + } + builder.fallbackTreatments(fallbackTreatmentsBuilder.build()); + } + } + return builder.serviceEndpoints(serviceEndpointsBuilder.build()).build(); } 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 23a5215..ebd20f6 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 @@ -27,6 +27,8 @@ import io.split.android.client.utils.logger.LogPrinter; import io.split.android.client.utils.logger.Logger; import io.split.android.client.utils.logger.SplitLogLevel; +import io.split.android.client.fallback.FallbackTreatmentsConfiguration; +import io.split.android.client.fallback.FallbackTreatment; public class SplitClientConfigHelperTest { @@ -235,4 +237,33 @@ public void rolloutCacheConfigurationValuesAreMappedCorrectly() { assertEquals(5, splitClientConfig.rolloutCacheConfiguration().getExpirationDays()); assertTrue(splitClientConfig.rolloutCacheConfiguration().isClearOnInit()); } + + @Test + public void fallbackTreatmentsValuesAreMappedCorrectly() { + Map globalFallbackTreatment = new HashMap<>(); + globalFallbackTreatment.put("treatment", "global-control"); + globalFallbackTreatment.put("config", "global-config"); + + Map feature1FallbackTreatment = new HashMap<>(); + feature1FallbackTreatment.put("treatment", "feature1-control"); + feature1FallbackTreatment.put("config", null); + + Map byFlagFallbackTreatments = new HashMap<>(); + byFlagFallbackTreatments.put("feature1", feature1FallbackTreatment); + + Map fallbackTreatmentsValues = new HashMap<>(); + fallbackTreatmentsValues.put("global", globalFallbackTreatment); + fallbackTreatmentsValues.put("byFlag", byFlagFallbackTreatments); + + Map configValues = new HashMap<>(); + configValues.put("fallbackTreatments", fallbackTreatmentsValues); + + SplitClientConfig splitClientConfig = SplitClientConfigHelper + .fromMap(configValues, mock(ImpressionListener.class)); + + FallbackTreatmentsConfiguration fallbackTreatmentsConfiguration = splitClientConfig.fallbackTreatments(); + + assertEquals(new FallbackTreatment("global-control", "global-config"), fallbackTreatmentsConfiguration.getGlobal()); + assertEquals(Map.of("feature1", new FallbackTreatment("feature1-control", null)), fallbackTreatmentsConfiguration.getByFlag()); + } } diff --git a/splitio_android/pubspec.yaml b/splitio_android/pubspec.yaml index fc2bbbe..772cc56 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: 1.0.0 +version: 1.1.0 environment: sdk: ">=2.16.2 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - splitio_platform_interface: ^2.0.0 + splitio_platform_interface: ^2.1.0 dev_dependencies: flutter_test: diff --git a/splitio_ios/CHANGELOG.md b/splitio_ios/CHANGELOG.md index d0935b9..6262687 100644 --- a/splitio_ios/CHANGELOG.md +++ b/splitio_ios/CHANGELOG.md @@ -1,5 +1,11 @@ +# 1.1.0 (Jan 28, 2026) +* Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. +* Updated iOS SDK to `3.6.0`. + +# 1.1.0-rc.1 (Jan 28, 2026) + # 1.0.0 (Aug 14, 2025) -- iOS SDK to `3.3.2` +* Updated iOS SDK to `3.3.2` # 1.0.0-rc.1 (Aug 14, 2025) diff --git a/splitio_ios/analysis_options.yaml b/splitio_ios/analysis_options.yaml index a5744c1..5cf0db1 100644 --- a/splitio_ios/analysis_options.yaml +++ b/splitio_ios/analysis_options.yaml @@ -2,3 +2,7 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +linter: + rules: + - public_member_api_docs diff --git a/splitio_ios/example/ios/Podfile.lock b/splitio_ios/example/ios/Podfile.lock index ea8a88c..5b23eda 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.3.2) - - splitio_ios (0.8.0): + - Split (3.6.0) + - splitio_ios (0.9.0): - Flutter - - Split (~> 3.3.2) + - Split (~> 3.6.0) DEPENDENCIES: - Flutter (from `Flutter`) @@ -20,10 +20,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/splitio_ios/ios" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Split: 0d4962a6c15180e1857c1a3753e1ae9c91a6150b - splitio_ios: 438ad21d0dfe467670f8b9508773b77b16a71d6b + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + Split: 1e2176aacd6421029bea41413401389d86e3d50a + splitio_ios: ad4f484a6c478bf7285e417ea8252371f66b54ff -PODFILE CHECKSUM: aed42fc5c94ade572556b7ed357c5c57f1bd83a2 +PODFILE CHECKSUM: ba5baa820782b9e142d3ba97a6b84de9feaaceda COCOAPODS: 1.16.2 diff --git a/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift b/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift index 2c29a06..8475535 100644 --- a/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift +++ b/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift @@ -154,4 +154,29 @@ class SplitClientConfigHelperTests: XCTestCase { XCTAssertEqual(5, actualConfig.expirationDays) XCTAssertTrue(actualConfig.clearOnInit) } + + func testFallbackTreatmentsConfigurationValuesAreMappedCorrectly() { + let configValues = [ + "fallbackTreatments": [ + "global": [ + "treatment": "on", + "config": "{\"key\": \"value\"}" + ], + "byFlag": [ + "feature1": [ + "treatment": "off", + "config": nil + ] + ] + ] + ] + + let splitClientConfig: SplitClientConfig = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) + let actualConfig = splitClientConfig.fallbackTreatments + + XCTAssertEqual("on", actualConfig.global?.treatment) + XCTAssertEqual("{\"key\": \"value\"}", actualConfig.global?.config) + XCTAssertEqual("off", actualConfig.byFlag["feature1"]?.treatment) + XCTAssertNil(actualConfig.byFlag["feature1"]?.config) + } } diff --git a/splitio_ios/ios/Classes/SplitClientConfigHelper.swift b/splitio_ios/ios/Classes/SplitClientConfigHelper.swift index abe76d0..63c8d22 100644 --- a/splitio_ios/ios/Classes/SplitClientConfigHelper.swift +++ b/splitio_ios/ios/Classes/SplitClientConfigHelper.swift @@ -36,6 +36,11 @@ class SplitClientConfigHelper { 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 private let FALLBACK_TREATMENTS = "fallbackTreatments"; + static private let FALLBACK_TREATMENTS_GLOBAL = "global"; + static private let FALLBACK_TREATMENTS_BY_FLAG = "byFlag"; + static private let FALLBACK_TREATMENT_VALUE = "treatment"; + static private let FALLBACK_TREATMENT_CONFIG = "config"; static func fromMap(configurationMap: [String: Any?], impressionListener: SplitImpressionListener?) -> SplitClientConfig { let config = SplitClientConfig() @@ -253,6 +258,30 @@ class SplitClientConfigHelper { config.rolloutCacheConfiguration = rolloutCacheConfigurationBuilder.build() } + if let fallbackTreatmentConfig = configurationMap[FALLBACK_TREATMENTS] as? [String: Any?] { + let fallbackTreatmentConfiguration = FallbackTreatmentsConfig.builder() + + if let globalTreatment = fallbackTreatmentConfig[FALLBACK_TREATMENTS_GLOBAL] as? [String: Any?] { + fallbackTreatmentConfiguration.global(FallbackTreatment( + treatment: globalTreatment[FALLBACK_TREATMENT_VALUE] as! String, + config: globalTreatment[FALLBACK_TREATMENT_CONFIG] as? String + )) + } + + if let byFlagTreatments = fallbackTreatmentConfig[FALLBACK_TREATMENTS_BY_FLAG] as? [String: [String: Any?]] { + var byFlag: [String: FallbackTreatment] = [:] + for (key, value) in byFlagTreatments { + byFlag[key] = FallbackTreatment( + treatment: value[FALLBACK_TREATMENT_VALUE] as! String, + config: value[FALLBACK_TREATMENT_CONFIG] as? String + ) + } + fallbackTreatmentConfiguration.byFlag(byFlag) + } + + config.fallbackTreatments = fallbackTreatmentConfiguration.build() + } + return config } diff --git a/splitio_ios/ios/splitio_ios.podspec b/splitio_ios/ios/splitio_ios.podspec index bb24308..cdbf4e7 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.8.0' + s.version = '0.9.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.3.2' + s.dependency 'Split', '~> 3.6.0' 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 8c6637b..5035e48 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: 1.0.0 +version: 1.1.0 environment: sdk: ">=2.16.2 <4.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - splitio_platform_interface: ^2.0.0 + splitio_platform_interface: ^2.1.0 dev_dependencies: flutter_test: diff --git a/splitio_platform_interface/CHANGELOG.md b/splitio_platform_interface/CHANGELOG.md index cf67b5e..7088e82 100644 --- a/splitio_platform_interface/CHANGELOG.md +++ b/splitio_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.1.0 (Jan 28, 2026) +* Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. + +# 2.1.0-rc.1 (Jan 28, 2026) + # 2.0.0 (Aug 14, 2025) # 2.0.0-rc.1 (Aug 14, 2025) diff --git a/splitio_platform_interface/analysis_options.yaml b/splitio_platform_interface/analysis_options.yaml index a5744c1..5cf0db1 100644 --- a/splitio_platform_interface/analysis_options.yaml +++ b/splitio_platform_interface/analysis_options.yaml @@ -2,3 +2,7 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +linter: + rules: + - public_member_api_docs diff --git a/splitio_platform_interface/lib/events/split_method_call_handler.dart b/splitio_platform_interface/lib/events/split_method_call_handler.dart index 6a0db02..4c58431 100644 --- a/splitio_platform_interface/lib/events/split_method_call_handler.dart +++ b/splitio_platform_interface/lib/events/split_method_call_handler.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:splitio_platform_interface/method_call_handler.dart'; +/// Handler for Split SDK events class SplitEventMethodCallHandler implements MethodCallHandler { static const String _eventClientReady = 'clientReady'; static const String _eventClientReadyFromCache = 'clientReadyFromCache'; @@ -24,6 +25,7 @@ class SplitEventMethodCallHandler implements MethodCallHandler { _eventClientTimeout: false, }; + /// Creates a new instance of the Split event method call handler. SplitEventMethodCallHandler(this._matchingKey, this._bucketingKey); @override @@ -48,26 +50,32 @@ class SplitEventMethodCallHandler implements MethodCallHandler { } } + /// Returns Future that is completed when the SDK client is ready. Future onReady() { return _onEvent(_eventClientReady); } + /// Returns Future that is completed when the SDK client is ready from cache. Future onReadyFromCache() { return _onEvent(_eventClientReadyFromCache); } + /// Returns Stream that emits when the SDK client is updated. Stream onUpdated() { return _updateStreamCompleter.stream; } + /// Returns Future that is completed when the SDK client times out. Future onTimeout() { return _onEvent(_eventClientTimeout); } + /// Cleans up resources. void destroy() { _updateStreamCompleter.close(); } + /// Returns Future that is completed when the specified SDK event occurs. Future _onEvent(String sdkEvent) { if (_triggeredClientEvents.containsKey(sdkEvent) && _triggeredClientEvents[sdkEvent] == true) { diff --git a/splitio_platform_interface/lib/impressions/impressions_method_call_handler.dart b/splitio_platform_interface/lib/impressions/impressions_method_call_handler.dart index ea4e4a4..c68e8fd 100644 --- a/splitio_platform_interface/lib/impressions/impressions_method_call_handler.dart +++ b/splitio_platform_interface/lib/impressions/impressions_method_call_handler.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:splitio_platform_interface/method_call_handler.dart'; import 'package:splitio_platform_interface/split_impression.dart'; +/// Handles impressions method calls. class ImpressionsMethodCallHandler extends StreamMethodCallHandler { final _streamController = StreamController(); diff --git a/splitio_platform_interface/lib/method_call_handler.dart b/splitio_platform_interface/lib/method_call_handler.dart index 6ef88ac..3a28da6 100644 --- a/splitio_platform_interface/lib/method_call_handler.dart +++ b/splitio_platform_interface/lib/method_call_handler.dart @@ -1,7 +1,11 @@ +/// Abstract class for handling method calls. abstract class MethodCallHandler { + /// Handles a method call with the given method name and arguments. Future handle(String methodName, dynamic methodArguments); } +/// Abstract class for handling stream method calls. abstract class StreamMethodCallHandler extends MethodCallHandler { + /// Returns a stream of the given type. Stream stream(); } diff --git a/splitio_platform_interface/lib/method_channel_platform.dart b/splitio_platform_interface/lib/method_channel_platform.dart index aaf0b2c..0d66439 100644 --- a/splitio_platform_interface/lib/method_channel_platform.dart +++ b/splitio_platform_interface/lib/method_channel_platform.dart @@ -6,7 +6,9 @@ const String _controlTreatment = 'control'; const SplitResult _controlResult = SplitResult(_controlTreatment, null); const MethodChannel _methodChannel = MethodChannel('splitio'); +/// Method channel platform implementation. class MethodChannelPlatform extends SplitioPlatform { + /// Returns the method channel. MethodChannel get methodChannel => _methodChannel; final Map _handlers = {}; @@ -15,6 +17,8 @@ class MethodChannelPlatform extends SplitioPlatform { ImpressionsMethodCallHandler(); @visibleForTesting + + /// Handles method calls from the platform. Future handle(MethodCall call) async { _impressionsMethodCallHandler.handle(call.method, call.arguments); for (MethodCallHandler handler in _handlers.values) { diff --git a/splitio_platform_interface/lib/split_certificate_pinning_configuration.dart b/splitio_platform_interface/lib/split_certificate_pinning_configuration.dart index 78b9a0b..0c9f839 100644 --- a/splitio_platform_interface/lib/split_certificate_pinning_configuration.dart +++ b/splitio_platform_interface/lib/split_certificate_pinning_configuration.dart @@ -1,9 +1,11 @@ +/// Certificate pinning configuration. class CertificatePinningConfiguration { - final Map> _pins = {}; + /// Returns the pins. Map> get pins => _pins; + /// Adds a pin for the given host. CertificatePinningConfiguration addPin(String host, String pin) { pin = pin.trim(); if (pin.isEmpty) { @@ -17,4 +19,4 @@ class CertificatePinningConfiguration { _pins[host]?.add(pin); return this; } -} \ No newline at end of file +} diff --git a/splitio_platform_interface/lib/split_configuration.dart b/splitio_platform_interface/lib/split_configuration.dart index 1d54fdd..90ce1be 100644 --- a/splitio_platform_interface/lib/split_configuration.dart +++ b/splitio_platform_interface/lib/split_configuration.dart @@ -1,8 +1,11 @@ import 'package:splitio_platform_interface/split_certificate_pinning_configuration.dart'; +import 'package:splitio_platform_interface/split_fallback_treatments_configuration.dart'; import 'package:splitio_platform_interface/split_sync_config.dart'; import 'package:splitio_platform_interface/split_rollout_cache_configuration.dart'; +/// Split configuration. class SplitConfiguration { + /// The configuration map. final Map configurationMap = {}; /// Initializes the Split configuration. @@ -48,6 +51,10 @@ class SplitConfiguration { /// [readyTimeout] Maximum amount of time in seconds to wait before firing the SDK_READY_TIMED_OUT event. Defaults to 10 seconds. /// /// [certificatePinningConfiguration] Certificate pinning configuration. Pins need to have the format of a base64 SHA-256 or base64 SHA-1 hashes of the SPKI (ex.: "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y="). Not supported in Web. + /// + /// [rolloutCacheConfiguration] Rollout cache configuration. + /// + /// [fallbackTreatments] Fallback treatments configuration. SplitConfiguration({ int? featuresRefreshRate, int? segmentsRefreshRate, @@ -76,6 +83,7 @@ class SplitConfiguration { int? readyTimeout = 10, CertificatePinningConfiguration? certificatePinningConfiguration, RolloutCacheConfiguration? rolloutCacheConfiguration, + FallbackTreatmentsConfiguration? fallbackTreatments, }) { if (featuresRefreshRate != null) { configurationMap['featuresRefreshRate'] = featuresRefreshRate; @@ -195,19 +203,59 @@ class SplitConfiguration { 'clearOnInit': rolloutCacheConfiguration.clearOnInit }; } + + if (fallbackTreatments != null) { + configurationMap['fallbackTreatments'] = { + 'global': fallbackTreatments.global, + 'byFlag': fallbackTreatments.byFlag + }; + } } } +/// Impressions mode. enum ImpressionsMode { + /// Debug impressions mode. debug, + + /// Optimized impressions mode. optimized, + + /// None impressions mode. none, } +/// User consent. enum UserConsent { + /// The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. granted, + + /// The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. declined, + + /// The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its + /// internal storage, and eventually either sends them or not if the consent status is updated to 'GRANTED' or + /// 'DECLINED' respectively. The status can be updated at any time with the `UserConsent.setStatus` factory method. unknown, } -enum SplitLogLevel { verbose, debug, info, warning, error, none } +/// Split log level. +enum SplitLogLevel { + /// Verbose log level. + verbose, + + /// Debug log level. + debug, + + /// Info log level. + info, + + /// Warning log level. + warning, + + /// Error log level. + error, + + /// None log level. + none +} diff --git a/splitio_platform_interface/lib/split_evaluation_options.dart b/splitio_platform_interface/lib/split_evaluation_options.dart index 2a603a8..9986514 100644 --- a/splitio_platform_interface/lib/split_evaluation_options.dart +++ b/splitio_platform_interface/lib/split_evaluation_options.dart @@ -1,17 +1,22 @@ import 'dart:collection'; +/// Options for evaluation. class EvaluationOptions { final Map _properties; + /// Impression properties. Map get properties => UnmodifiableMapView(_properties); + /// Creates a new EvaluationOptions instance. const EvaluationOptions.empty() : _properties = const {}; + /// Creates a new EvaluationOptions instance. factory EvaluationOptions([Map properties = const {}]) { return EvaluationOptions._( Map.unmodifiable(Map.from(properties))); } + /// Converts the EvaluationOptions to a JSON map. Map toJson() => { 'properties': _properties, }; diff --git a/splitio_platform_interface/lib/split_fallback_treatment.dart b/splitio_platform_interface/lib/split_fallback_treatment.dart new file mode 100644 index 0000000..bca1dc5 --- /dev/null +++ b/splitio_platform_interface/lib/split_fallback_treatment.dart @@ -0,0 +1,11 @@ +/// Fallback treatment for when the feature flag cannot be evaluated. +class FallbackTreatment { + /// The treatment value. + final String treatment; + + /// The treatment configuration. + final String? config; + + /// Creates a new FallbackTreatment instance. + const FallbackTreatment(this.treatment, [this.config]); +} diff --git a/splitio_platform_interface/lib/split_fallback_treatments_configuration.dart b/splitio_platform_interface/lib/split_fallback_treatments_configuration.dart new file mode 100644 index 0000000..3281ae5 --- /dev/null +++ b/splitio_platform_interface/lib/split_fallback_treatments_configuration.dart @@ -0,0 +1,23 @@ +import 'package:splitio_platform_interface/split_fallback_treatment.dart'; + +/// Configuration for fallback treatments. +class FallbackTreatmentsConfiguration { + Map? _global; + Map>? _byFlag; + + /// Global fallback treatment. + Map? get global => _global; + + /// Fallback treatments by flag. + Map>? get byFlag => _byFlag; + + /// Creates a new FallbackTreatmentsConfiguration instance. + FallbackTreatmentsConfiguration( + {FallbackTreatment? global, Map? byFlag}) { + _global = global != null + ? {'treatment': global.treatment, 'config': global.config} + : null; + _byFlag = byFlag?.map((key, value) => + MapEntry(key, {'treatment': value.treatment, 'config': value.config})); + } +} diff --git a/splitio_platform_interface/lib/split_impression.dart b/splitio_platform_interface/lib/split_impression.dart index b3df44d..de7c15b 100644 --- a/splitio_platform_interface/lib/split_impression.dart +++ b/splitio_platform_interface/lib/split_impression.dart @@ -1,17 +1,37 @@ +/// Represents an impression when a feature flag is evaluated. class Impression { + /// The traffic matching key. final String? key; + + /// The traffic bucketing key. final String? bucketingKey; + + /// The name of the feature flag. final String? split; + + /// The treatment value. final String? treatment; + + /// The impression timestamp. final num? time; + + /// The rule label. final String? appliedRule; + + /// The version of the feature flag. final num? changeNumber; + + /// The attributes. final Map attributes; + + /// The impression properties. final Map? properties; + /// Creates a new Impression instance. Impression(this.key, this.bucketingKey, this.split, this.treatment, this.time, this.appliedRule, this.changeNumber, this.attributes, this.properties); + /// Creates a new Impression instance from a map. static Impression fromMap(Map map) { Map? properties; if (map['properties'] != null) { diff --git a/splitio_platform_interface/lib/split_prerequisite.dart b/splitio_platform_interface/lib/split_prerequisite.dart index b8be081..02e47df 100644 --- a/splitio_platform_interface/lib/split_prerequisite.dart +++ b/splitio_platform_interface/lib/split_prerequisite.dart @@ -1,15 +1,20 @@ +/// Prerequisite class. class Prerequisite { final String _name; final Set _treatments; + /// The feature flag name of the prerequisite. String get name => _name; + /// The treatments of the prerequisite. Set get treatments => _treatments; + /// Creates a new Prerequisite instance. Prerequisite(this._name, this._treatments); - static Prerequisite fromEntry(el) { + /// Creates a Prerequisite instance from a map. + static Prerequisite fromEntry(Map el) { final String name = (el['n'] ?? el['n:'] ?? '').toString(); final List rawTreatments = (el['t'] as List?) ?? []; final Set treatments = @@ -26,7 +31,8 @@ class Prerequisite { }'''; } - equals(Prerequisite other) { + /// Checks if this Prerequisite is equal to another Prerequisite. + bool equals(Prerequisite other) { return name == other.name && treatments == other.treatments; } diff --git a/splitio_platform_interface/lib/split_result.dart b/splitio_platform_interface/lib/split_result.dart index f1c0896..fe6c4fc 100644 --- a/splitio_platform_interface/lib/split_result.dart +++ b/splitio_platform_interface/lib/split_result.dart @@ -4,9 +4,13 @@ /// /// The [config] contains the configuration for the split, if any. May be null. class SplitResult { + /// The treatment of the split. final String treatment; + + /// The configuration of the split, if any. May be null. final String? config; + /// Creates a new SplitResult instance. const SplitResult(this.treatment, this.config); @override diff --git a/splitio_platform_interface/lib/split_rollout_cache_configuration.dart b/splitio_platform_interface/lib/split_rollout_cache_configuration.dart index 9c2b0f3..340bc5d 100644 --- a/splitio_platform_interface/lib/split_rollout_cache_configuration.dart +++ b/splitio_platform_interface/lib/split_rollout_cache_configuration.dart @@ -1,12 +1,17 @@ +/// Rollout cache configuration class. class RolloutCacheConfiguration { - late int _expirationDays; late bool _clearOnInit; + /// The expiration days of the cache. int get expirationDays => _expirationDays; + + /// Whether to clear the cache on initialization. bool get clearOnInit => _clearOnInit; - RolloutCacheConfiguration({int expirationDays = 10, bool clearOnInit = false}) { + /// Creates a new RolloutCacheConfiguration instance. + RolloutCacheConfiguration( + {int expirationDays = 10, bool clearOnInit = false}) { if (expirationDays < 1) { expirationDays = 10; } diff --git a/splitio_platform_interface/lib/split_sync_config.dart b/splitio_platform_interface/lib/split_sync_config.dart index bc47f8d..1b0ed05 100644 --- a/splitio_platform_interface/lib/split_sync_config.dart +++ b/splitio_platform_interface/lib/split_sync_config.dart @@ -1,14 +1,19 @@ +/// Sync configuration class for controlling which feature flags to fetch. class SyncConfig { late Set _names; late Set _prefixes; late Set _sets; + /// Names of the feature flags to fetch. Set get names => _names; + /// Prefixes of the feature flags to fetch. Set get prefixes => _prefixes; + /// Flag sets of the feature flags to fetch. Set get sets => _sets; + /// Creates a new SyncConfig instance passing an optional list of names and prefixes. SyncConfig( {List names = const [], List prefixes = const []}) { _names = names.toSet(); @@ -16,12 +21,14 @@ class SyncConfig { _sets = {}; } + /// Creates a new SyncConfig instance passing an optional set of names and prefixes. SyncConfig.fromSet( {Set names = const {}, Set prefixes = const {}}) { _names = names; _prefixes = prefixes; } + /// Creates a new SyncConfig instance passing a list of flag sets. SyncConfig.flagSets(List sets) { _sets = sets.toSet(); _names = {}; diff --git a/splitio_platform_interface/lib/split_view.dart b/splitio_platform_interface/lib/split_view.dart index 8908cf6..b4a2244 100644 --- a/splitio_platform_interface/lib/split_view.dart +++ b/splitio_platform_interface/lib/split_view.dart @@ -2,6 +2,7 @@ import 'dart:core'; import 'package:splitio_platform_interface/split_prerequisite.dart'; +/// Represents a feature flag class SplitView { static const String _keyName = 'name'; static const String _keyTrafficType = 'trafficType'; @@ -14,17 +15,37 @@ class SplitView { static const String _keyImpressionsDisabled = 'impressionsDisabled'; static const String _keyPrerequisites = 'prerequisites'; + /// The name of the feature flag. String name; + + /// The traffic type of the feature flag. String trafficType; + + /// Whether the feature flag is killed. bool killed = false; + + /// The treatments of the feature flag. List treatments = []; + + /// The change number of the feature flag. int? changeNumber; + + /// The configurations per treatment of the feature flag. Map configs = {}; + + /// The default treatment of the feature flag. String defaultTreatment; + + /// The sets of the feature flag. List sets = []; + + /// Whether impressions are disabled for the feature flag. bool impressionsDisabled = false; + + /// The prerequisites of the feature flag. Set prerequisites = {}; + /// Creates a new SplitView instance. SplitView(this.name, this.trafficType, this.killed, this.treatments, this.changeNumber, this.configs, [this.defaultTreatment = '', @@ -32,6 +53,7 @@ class SplitView { this.impressionsDisabled = false, this.prerequisites = const {}]); + /// Creates a SplitView instance from a map entry. static SplitView? fromEntry(Map? entry) { if (entry == null || entry.isEmpty) { return null; diff --git a/splitio_platform_interface/lib/splitio_platform_interface.dart b/splitio_platform_interface/lib/splitio_platform_interface.dart index 247c3a2..435c1a2 100644 --- a/splitio_platform_interface/lib/splitio_platform_interface.dart +++ b/splitio_platform_interface/lib/splitio_platform_interface.dart @@ -222,12 +222,14 @@ abstract class _ClientPlatform { /// [SplitioPlatform] methods. abstract class SplitioPlatform extends PlatformInterface with _FactoryPlatform, _ClientPlatform { + /// Creates a new SplitioPlatform instance. SplitioPlatform() : super(token: _token); static SplitioPlatform _instance = MethodChannelPlatform(); static final Object _token = Object(); + /// The instance of SplitioPlatform that is used to communicate with the native platform. static SplitioPlatform get instance => _instance; /// Platform-specific plugins should set this with their own platform-specific diff --git a/splitio_platform_interface/pubspec.yaml b/splitio_platform_interface/pubspec.yaml index 6b2a0e1..411ef85 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: 2.0.0 +version: 2.1.0 repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_platform_interface environment: diff --git a/splitio_platform_interface/test/split_fallback_treatments_configuration_test.dart b/splitio_platform_interface/test/split_fallback_treatments_configuration_test.dart new file mode 100644 index 0000000..2dd3990 --- /dev/null +++ b/splitio_platform_interface/test/split_fallback_treatments_configuration_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/split_fallback_treatment.dart'; +import 'package:splitio_platform_interface/split_fallback_treatments_configuration.dart'; + +void main() { + test('global and by flag fallback treatments', () { + var config = FallbackTreatmentsConfiguration( + global: const FallbackTreatment('custom-treatment')); + + expect(config.global, + equals({'treatment': 'custom-treatment', 'config': null})); + expect(config.byFlag, null); + + config = FallbackTreatmentsConfiguration(byFlag: { + 'flag1': const FallbackTreatment('custom-treatment-1', 'custom-config-1'), + 'flag2': const FallbackTreatment('custom-treatment-2', 'custom-config-2') + }); + expect( + config.byFlag, + equals({ + 'flag1': { + 'treatment': 'custom-treatment-1', + 'config': 'custom-config-1' + }, + 'flag2': { + 'treatment': 'custom-treatment-2', + 'config': 'custom-config-2' + } + })); + expect(config.global, null); + }); + + test('default values', () { + var config = FallbackTreatmentsConfiguration(); + + expect(config.global, null); + expect(config.byFlag, null); + }); +} diff --git a/splitio_platform_interface/test/splitio_configuration_test.dart b/splitio_platform_interface/test/splitio_configuration_test.dart index 64b758c..c627edb 100644 --- a/splitio_platform_interface/test/splitio_configuration_test.dart +++ b/splitio_platform_interface/test/splitio_configuration_test.dart @@ -1,6 +1,8 @@ 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_fallback_treatment.dart'; +import 'package:splitio_platform_interface/split_fallback_treatments_configuration.dart'; import 'package:splitio_platform_interface/split_rollout_cache_configuration.dart'; import 'package:splitio_platform_interface/split_sync_config.dart'; @@ -36,7 +38,16 @@ void main() { .addPin('host1', 'pin1') .addPin('host2', 'pin3') .addPin('host1', 'pin2'), - rolloutCacheConfiguration: RolloutCacheConfiguration(expirationDays: 15, clearOnInit: true)); + rolloutCacheConfiguration: + RolloutCacheConfiguration(expirationDays: 15, clearOnInit: true), + fallbackTreatments: FallbackTreatmentsConfiguration( + global: const FallbackTreatment('custom-treatment'), + byFlag: { + 'flag1': const FallbackTreatment( + 'custom-treatment-flag1', 'config-flag1'), + 'flag2': const FallbackTreatment('custom-treatment-flag2'), + }, + )); expect(config.configurationMap['eventFlushInterval'], 2000); expect(config.configurationMap['eventsPerPush'], 300); @@ -73,8 +84,22 @@ void main() { 'host1': ['pin1', 'pin2'], 'host2': ['pin3'] }); - expect(config.configurationMap['rolloutCacheConfiguration']['expirationDays'], 15); - expect(config.configurationMap['rolloutCacheConfiguration']['clearOnInit'], true); + expect( + config.configurationMap['rolloutCacheConfiguration']['expirationDays'], + 15); + expect(config.configurationMap['rolloutCacheConfiguration']['clearOnInit'], + true); + expect(config.configurationMap['fallbackTreatments']['global'], + equals({'treatment': 'custom-treatment', 'config': null})); + expect( + config.configurationMap['fallbackTreatments']['byFlag'], + equals({ + 'flag1': { + 'treatment': 'custom-treatment-flag1', + 'config': 'config-flag1' + }, + 'flag2': {'treatment': 'custom-treatment-flag2', 'config': null} + })); }); test('no special values leaves map empty', () async { diff --git a/splitio_web/CHANGELOG.md b/splitio_web/CHANGELOG.md index ccade9c..1ef2281 100644 --- a/splitio_web/CHANGELOG.md +++ b/splitio_web/CHANGELOG.md @@ -1,7 +1,15 @@ -# 1.0.1 (January XX, 2026) -- Updated Browser SDK to `1.6.1`. +# 1.1.1 (Jan 28, 2026) +* Bug fix: corrected asset path for loading the Browser SDK. -# 1.0.0 (January 16, 2026) -- Initial release. Web implementation of `splitio` based on Split Browser SDK `1.6.0`. +# 1.1.1-rc.1 (Jan 28, 2026) -# 1.0.0-rc.1 (January 15, 2026) +# 1.1.0 (Jan 28, 2026) +* Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. +* Updated Browser SDK to `1.6.1`. + +# 1.1.0-rc.1 (Jan 28, 2026) + +# 1.0.0 (Jan 16, 2026) +* Initial release. Web implementation of `splitio` based on Split Browser SDK `1.6.0`. + +# 1.0.0-rc.1 (Jan 15, 2026) diff --git a/splitio_web/analysis_options.yaml b/splitio_web/analysis_options.yaml index a5744c1..5cf0db1 100644 --- a/splitio_web/analysis_options.yaml +++ b/splitio_web/analysis_options.yaml @@ -2,3 +2,7 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +linter: + rules: + - public_member_api_docs diff --git a/splitio_web/lib/splitio_web.dart b/splitio_web/lib/splitio_web.dart index 6a0102f..0e77822 100644 --- a/splitio_web/lib/splitio_web.dart +++ b/splitio_web/lib/splitio_web.dart @@ -91,7 +91,7 @@ class SplitioWeb extends SplitioPlatform { final script = document.createElement('script') as HTMLScriptElement; script.type = 'text/javascript'; script.src = - 'assets/packages/splitio_web/web/split-browser-1.6.0.full.min.js'; + 'assets/packages/splitio_web/web/split-browser.full.min.js'; // Wait for script to load final completer = Completer(); @@ -321,6 +321,12 @@ class SplitioWeb extends SplitioPlatform { config.impressionListener = impressionListener; } + + if (configuration.configurationMap['fallbackTreatments'] != null) { + final fallbackTreatments = configuration.configurationMap['fallbackTreatments'] as Map; + // FallbackTreatmentsConfiguration has a compatible structure with JSFallbackTreatmentsConfiguration + config.fallbackTreatments = fallbackTreatments.jsify() as JSFallbackTreatmentsConfiguration; + } } return config; diff --git a/splitio_web/lib/src/js_interop.dart b/splitio_web/lib/src/js_interop.dart index df58d6e..42bf8d1 100644 --- a/splitio_web/lib/src/js_interop.dart +++ b/splitio_web/lib/src/js_interop.dart @@ -3,6 +3,7 @@ import 'package:splitio_platform_interface/splitio_platform_interface.dart'; // JS SDK types +/// SplitIO.ImpressionDTO @JS() extension type JSImpressionDTO._(JSObject _) implements JSObject { external JSString feature; @@ -16,6 +17,7 @@ extension type JSImpressionDTO._(JSObject _) implements JSObject { external JSString? properties; } +/// SplitIO.ImpressionData @JS() extension type JSImpressionData._(JSObject _) implements JSObject { external JSImpressionDTO impression; @@ -25,30 +27,43 @@ extension type JSImpressionData._(JSObject _) implements JSObject { external JSString sdkLanguageVersion; } +/// SplitIO.ILogger @JS() extension type JSILogger._(JSObject _) implements JSObject { + /// Log a debug level message external JSAny? debug(JSString message); + + /// Log an info level message external JSAny? info(JSString message); + + /// Log a warning level message external JSAny? warn(JSString message); + + /// Log an error level message external JSAny? error(JSString message); } +/// SplitIO.IImpressionListener @JS() extension type JSImpressionListener._(JSObject _) implements JSObject { + /// Log an impression external JSVoid logImpression(JSImpressionData impression); } +/// SplitIO.IClientSideSettings['core'] @JS() extension type JSConfigurationCore._(JSObject _) implements JSObject { external JSString authorizationKey; external JSAny key; // string | SplitKey } +/// SplitIO.IClientSideSettings['startup'] @JS() extension type JSConfigurationStartup._(JSObject _) implements JSObject { external JSNumber? readyTimeout; } +/// SplitIO.IClientSideSettings['scheduler'] @JS() extension type JSConfigurationScheduler._(JSObject _) implements JSObject { external JSNumber? featuresRefreshRate; @@ -61,6 +76,7 @@ extension type JSConfigurationScheduler._(JSObject _) implements JSObject { external JSNumber? eventsPushRate; } +/// SplitIO.IClientSideSettings['urls'] @JS() extension type JSConfigurationUrls._(JSObject _) implements JSObject { external JSString? sdk; @@ -70,12 +86,14 @@ extension type JSConfigurationUrls._(JSObject _) implements JSObject { external JSString? telemetry; } +/// SplitIO.IClientSideSettings['sync']['splitFilters'] @JS() extension type JSSplitFilter._(JSObject _) implements JSObject { external JSString type; external JSArray values; } +/// SplitIO.IClientSideSettings['sync'] @JS() extension type JSConfigurationSync._(JSObject _) implements JSObject { external JSString? impressionsMode; @@ -83,6 +101,7 @@ extension type JSConfigurationSync._(JSObject _) implements JSObject { external JSArray? splitFilters; } +/// SplitIO.IClientSideSettings['storage'] @JS() extension type JSConfigurationStorage._(JSObject _) implements JSObject { external JSString? type; @@ -90,6 +109,15 @@ extension type JSConfigurationStorage._(JSObject _) implements JSObject { external JSBoolean? clearOnInit; } +/// SplitIO.IClientSideSettings['fallbackTreatments'] +@JS() +extension type JSFallbackTreatmentsConfiguration._(JSObject _) + implements JSObject { + external JSTreatmentWithConfig? global; + external JSObject? byFlag; +} + +/// SplitIO.IClientSideSettings @JS() extension type JSConfiguration._(JSObject _) implements JSObject { external JSConfigurationCore core; @@ -102,20 +130,27 @@ extension type JSConfiguration._(JSObject _) implements JSObject { external JSImpressionListener? impressionListener; external JSAny? debug; external JSAny? storage; + external JSFallbackTreatmentsConfiguration? fallbackTreatments; } +/// SplitIO.ISettings @JS() extension type JSISettings._(JSObject _) implements JSConfiguration { external JSILogger log; external JSImpressionListener? impressionListener; } +/// SplitIO.IUserConsentAPI @JS() extension type JSIUserConsentAPI._(JSObject _) implements JSObject { + /// SplitIO.IUserConsentAPI['setStatus'] external JSBoolean setStatus(JSBoolean userConsent); + + /// SplitIO.IUserConsentAPI['getStatus'] external JSString getStatus(); } +/// SplitIO.EventConsts @JS() extension type JSEventConsts._(JSObject _) implements JSObject { // ignore: non_constant_identifier_names @@ -128,6 +163,7 @@ extension type JSEventConsts._(JSObject _) implements JSObject { external JSString SDK_UPDATE; } +/// SplitIO.ReadinessStatus @JS() extension type JSReadinessStatus._(JSObject _) implements JSObject { external JSBoolean isReady; @@ -135,18 +171,21 @@ extension type JSReadinessStatus._(JSObject _) implements JSObject { external JSBoolean hasTimedout; } +/// SplitIO.TreatmentWithConfig @JS() extension type JSTreatmentWithConfig._(JSObject _) implements JSObject { external JSString treatment; external JSString? config; } +/// SplitIO.SplitView['prerequisites'] @JS() extension type JSPrerequisite._(JSObject _) implements JSObject { external JSString flagName; external JSArray treatments; } +/// SplitIO.SplitView @JS() extension type JSSplitView._(JSObject _) implements JSObject { external JSString name; @@ -161,73 +200,132 @@ extension type JSSplitView._(JSObject _) implements JSObject { external JSArray prerequisites; } +/// SplitIO.EvaluationOptions @JS() extension type JSEvaluationOptions._(JSObject _) implements JSObject { external JSObject properties; } +/// SplitIO.IBrowserClient @JS() extension type JSIBrowserClient._(JSObject _) implements JSObject { + /// SplitIO.IBrowserClient['getTreatment'] external JSString getTreatment(JSString flagName, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['getTreatments'] external JSObject getTreatments(JSArray flagNames, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['getTreatmentWithConfig'] external JSTreatmentWithConfig getTreatmentWithConfig(JSString flagName, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['getTreatmentsWithConfig'] external JSObject getTreatmentsWithConfig(JSArray flagNames, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['getTreatmentsByFlagSet'] external JSObject getTreatmentsByFlagSet(JSString flagSetName, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['getTreatmentsByFlagSets'] external JSObject getTreatmentsByFlagSets(JSArray flagSetNames, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['getTreatmentsWithConfigByFlagSet'] external JSObject getTreatmentsWithConfigByFlagSet(JSString flagSetName, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['getTreatmentsWithConfigByFlagSets'] external JSObject getTreatmentsWithConfigByFlagSets( JSArray flagSetNames, JSObject attributes, JSEvaluationOptions evaluationOptions); + + /// SplitIO.IBrowserClient['track'] external JSBoolean track(JSString? trafficType, JSString eventType, JSNumber? value, JSObject? attributes); + + /// SplitIO.IBrowserClient['setAttribute'] external JSBoolean setAttribute( JSString attributeName, JSAny? attributeValue); + + /// SplitIO.IBrowserClient['getAttribute'] external JSAny getAttribute(JSString attributeName); + + /// SplitIO.IBrowserClient['removeAttribute'] external JSBoolean removeAttribute(JSString attributeName); + + /// SplitIO.IBrowserClient['setAttributes'] external JSBoolean setAttributes(JSObject attributes); + + /// SplitIO.IBrowserClient['getAttributes'] external JSObject getAttributes(); + + /// SplitIO.IBrowserClient['clearAttributes'] external JSBoolean clearAttributes(); + + /// SplitIO.IBrowserClient['flush'] external JSPromise flush(); + + /// SplitIO.IBrowserClient['destroy'] external JSPromise destroy(); + + /// SplitIO.IBrowserClient['on'] external JSVoid on(JSString event, JSFunction listener); + + /// SplitIO.IBrowserClient['off'] external JSVoid off(JSString event, JSFunction listener); + + /// SplitIO.IBrowserClient['emit'] external JSVoid emit(JSString event); // ignore: non_constant_identifier_names external JSEventConsts Event; + + /// SplitIO.IBrowserClient['getStatus'] external JSReadinessStatus getStatus(); } +/// SplitIO.IManager @JS() extension type JSIManager._(JSObject _) implements JSObject { + /// SplitIO.IManager['names'] external JSArray names(); + + /// SplitIO.IManager['split'] external JSSplitView? split(JSString name); + + /// SplitIO.IManager['splits'] external JSArray splits(); } +/// SplitIO.IBrowserSDK @JS() extension type JSIBrowserSDK._(JSObject _) implements JSObject { + /// SplitIO.IBrowserSDK['client'] external JSIBrowserClient client(JSAny? key); + + /// SplitIO.IBrowserSDK['manager'] external JSIManager manager(); + + /// SplitIO.IBrowserSDK['settings'] external JSISettings settings; // ignore: non_constant_identifier_names external JSIUserConsentAPI UserConsent; } +/// SplitIO.LoggerFactory @JS() extension type JSLoggerFactory._(JSFunction _) implements JSFunction { + /// A callable function external JSObject call(); } +/// Type of the `window.splitio` object @JS() extension type JSBrowserSDKPackage._(JSObject _) implements JSObject { + /// Browser SDK factory constructor // ignore: non_constant_identifier_names external JSIBrowserSDK SplitFactory(JSConfiguration config); // ignore: non_constant_identifier_names @@ -244,41 +342,52 @@ extension type JSBrowserSDKPackage._(JSObject _) implements JSObject { // Conversion utils: JS to Dart types +/// Get the keys of a JavaScript object @JS('Object.keys') external JSArray objectKeys(JSObject obj); +/// Get a property from a JavaScript object @JS('Reflect.get') external JSAny? reflectGet(JSObject target, JSString propertyKey); +/// Set a property on a JavaScript object @JS('Reflect.set') external JSAny? reflectSet(JSObject target, JSString propertyKey, JSAny? value); +/// Parse a JSON string into a JavaScript object @JS('JSON.parse') external JSObject jsonParse(JSString obj); +/// Convert a JavaScript array to a Dart list List jsArrayToList(JSArray obj) => (obj.dartify() as List).cast(); +/// Convert a JavaScript object to a Dart map Map jsObjectToMap(JSObject obj) => (obj.dartify() as Map).cast(); +/// Convert a JavaScript value to a Dart value Object? jsAnyToDart(JSAny? value) => value.dartify(); // Conversion utils: JS SDK to Flutter SDK types +/// Convert a JavaScript SplitIO.Treatments object to a Dart map Map jsTreatmentsToMap(JSObject obj) { return jsObjectToMap(obj).map((k, v) => MapEntry(k, v as String)); } +/// Convert a JavaScript SplitIO.TreatmentsWithConfig object to a Dart map Map jsTreatmentsWithConfigToMap(JSObject obj) { return jsObjectToMap(obj).map((k, v) => MapEntry( k, SplitResult(v['treatment'] as String, v['config'] as String?))); } +/// Convert a JavaScript SplitIO.TreatmentWithConfig object to a Dart SplitResult SplitResult jsTreatmentWithConfigToSplitResult(JSTreatmentWithConfig obj) { return SplitResult(obj.treatment.toDart, obj.config?.toDart); } +/// Convert a JavaScript prerequisite object to a Dart Prerequisite Prerequisite jsPrerequisiteToPrerequisite(JSPrerequisite obj) { return Prerequisite( obj.flagName.toDart, @@ -286,6 +395,7 @@ Prerequisite jsPrerequisiteToPrerequisite(JSPrerequisite obj) { ); } +/// Convert a JavaScript SplitIO.SplitView object to a Dart SplitView SplitView jsSplitViewToSplitView(JSSplitView obj) { return SplitView( obj.name.toDart, @@ -300,6 +410,7 @@ SplitView jsSplitViewToSplitView(JSSplitView obj) { obj.prerequisites.toDart.map(jsPrerequisiteToPrerequisite).toSet()); } +/// Convert a JavaScript SplitIO.ImpressionData object to a Dart Impression Impression jsImpressionDataToImpression(JSImpressionData obj) { return Impression( obj.impression.keyName.toDart, @@ -316,6 +427,7 @@ Impression jsImpressionDataToImpression(JSImpressionData obj) { ); } +/// Build a JavaScript SplitIO.SplitKey object from a matching key and optional bucketing key JSAny buildJsKey(String matchingKey, String? bucketingKey) { if (bucketingKey != null) { return { @@ -326,6 +438,7 @@ JSAny buildJsKey(String matchingKey, String? bucketingKey) { return matchingKey.toJS; } +/// Build a string index from a matching key and optional bucketing key String buildKeyString(String matchingKey, String? bucketingKey) { return bucketingKey == null ? matchingKey : '${matchingKey}_$bucketingKey'; } diff --git a/splitio_web/pubspec.yaml b/splitio_web/pubspec.yaml index 72e04bb..452b2d2 100644 --- a/splitio_web/pubspec.yaml +++ b/splitio_web/pubspec.yaml @@ -1,7 +1,7 @@ name: splitio_web description: The official Web implementation of splitio Flutter plugin. repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_web -version: 1.0.0 +version: 1.1.1 environment: sdk: ">=3.3.0 <4.0.0" # using Dart 3.3+ extension types for JS interop @@ -15,12 +15,12 @@ flutter: pluginClass: SplitioWeb fileName: splitio_web.dart assets: - - web/split-browser-1.6.1.full.min.js + - web/split-browser.full.min.js dependencies: flutter: sdk: flutter - splitio_platform_interface: ^2.0.0 + splitio_platform_interface: ^2.1.0 flutter_web_plugins: sdk: flutter web: ">=0.5.0 <2.0.0" diff --git a/splitio_web/test/splitio_web_test.dart b/splitio_web/test/splitio_web_test.dart index 13c386e..44151c7 100644 --- a/splitio_web/test/splitio_web_test.dart +++ b/splitio_web/test/splitio_web_test.dart @@ -7,6 +7,8 @@ import 'package:splitio_web/src/js_interop.dart'; 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'; +import 'package:splitio_platform_interface/split_fallback_treatment.dart'; +import 'package:splitio_platform_interface/split_fallback_treatments_configuration.dart'; import 'utils/js_interop_test_utils.dart'; extension on web.Window { @@ -685,39 +687,47 @@ void main() { matchingKey: 'matching-key', bucketingKey: 'bucketing-key', sdkConfiguration: SplitConfiguration( - featuresRefreshRate: 1, - segmentsRefreshRate: 2, - impressionsRefreshRate: 3, - telemetryRefreshRate: 4, - eventsQueueSize: 5, - impressionsQueueSize: 6, - eventFlushInterval: 7, - eventsPerPush: 8, // unsupported in Web - trafficType: 'user', - // ignore: deprecated_member_use - enableDebug: false, // deprecated, logLevel has precedence - streamingEnabled: false, - persistentAttributesEnabled: true, // unsupported in Web - impressionListener: true, - sdkEndpoint: 'https://sdk.domain/api', - eventsEndpoint: 'https://events.domain/api', - authServiceEndpoint: 'https://auth.domain/api/v2', - streamingServiceEndpoint: 'https://streaming.domain/sse', - telemetryServiceEndpoint: 'https://telemetry.domain/api/v1', - syncConfig: SyncConfig( - names: ['flag_1', 'flag_2'], prefixes: ['prefix_1']), - impressionsMode: ImpressionsMode.none, - syncEnabled: true, - userConsent: UserConsent.granted, - encryptionEnabled: true, // unsupported in Web - logLevel: SplitLogLevel.info, - readyTimeout: 1, - certificatePinningConfiguration: CertificatePinningConfiguration() - .addPin('host', 'pin'), // unsupported in Web - rolloutCacheConfiguration: RolloutCacheConfiguration( - expirationDays: 100, - clearOnInit: true, - ))); + featuresRefreshRate: 1, + segmentsRefreshRate: 2, + impressionsRefreshRate: 3, + telemetryRefreshRate: 4, + eventsQueueSize: 5, + impressionsQueueSize: 6, + eventFlushInterval: 7, + eventsPerPush: 8, // unsupported in Web + trafficType: 'user', + // ignore: deprecated_member_use + enableDebug: false, // deprecated, logLevel has precedence + streamingEnabled: false, + persistentAttributesEnabled: true, // unsupported in Web + impressionListener: true, + sdkEndpoint: 'https://sdk.domain/api', + eventsEndpoint: 'https://events.domain/api', + authServiceEndpoint: 'https://auth.domain/api/v2', + streamingServiceEndpoint: 'https://streaming.domain/sse', + telemetryServiceEndpoint: 'https://telemetry.domain/api/v1', + syncConfig: + SyncConfig(names: ['flag_1', 'flag_2'], prefixes: ['prefix_1']), + impressionsMode: ImpressionsMode.none, + syncEnabled: true, + userConsent: UserConsent.granted, + encryptionEnabled: true, // unsupported in Web + logLevel: SplitLogLevel.info, + readyTimeout: 1, + certificatePinningConfiguration: CertificatePinningConfiguration() + .addPin('host', 'pin'), // unsupported in Web + rolloutCacheConfiguration: RolloutCacheConfiguration( + expirationDays: 100, + clearOnInit: true, + ), + fallbackTreatments: FallbackTreatmentsConfiguration( + global: const FallbackTreatment('global-treatment'), + byFlag: { + 'flag_1': const FallbackTreatment('fallback_1', 'config_1'), + 'flag_2': const FallbackTreatment('fallback_2', null) + }, + ), + )); expect(mock.calls[mock.calls.length - 5].methodName, 'SplitFactory'); final actual = @@ -776,6 +786,13 @@ void main() { 'impressionListener': { 'logImpression': (actual as Map)['impressionListener'] ['logImpression'] + }, + 'fallbackTreatments': { + 'global': {'treatment': 'global-treatment', 'config': null}, + 'byFlag': { + 'flag_1': {'treatment': 'fallback_1', 'config': 'config_1'}, + 'flag_2': {'treatment': 'fallback_2', 'config': null} + } } })); diff --git a/splitio_web/web/split-browser-1.6.1.full.min.js b/splitio_web/web/split-browser.full.min.js similarity index 100% rename from splitio_web/web/split-browser-1.6.1.full.min.js rename to splitio_web/web/split-browser.full.min.js