diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index ed8d977..fe24b49 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -5,6 +5,16 @@ on: branches: [ "master" ] pull_request: branches: [ "master" ] + # workflow_dispatch: + # inputs: + # sdk_branch: + # description: "Specify the SDK branch" + # required: false + # default: "master" + # testapp_branch: + # description: "Specify the test app branch" + # required: false + # default: "master" jobs: unit_test_coverage: @@ -36,15 +46,34 @@ jobs: repository: 'optimizely/travisci-tools' path: 'home/runner/travisci-tools' ref: 'master' + # Set SDK Branch based on input or PR/Push + # - name: Set SDK Branch and Test App Branch + # run: | + # # If manually triggered + # if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + # echo "SDK_BRANCH=${{ github.event.inputs.sdk_branch || 'master' }}" >> $GITHUB_ENV + # echo "TESTAPP_BRANCH=${{ github.event.inputs.testapp_branch || 'master' }}" >> $GITHUB_ENV + # # If triggered by PR + # elif [[ "${{ github.event_name }}" == "pull_request" ]]; then + # echo "SDK_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + # # If triggered by push + # else + # echo "SDK_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + # echo "TRAVIS_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + # fi - name: set SDK Branch if PR + env: + HEAD_REF: ${{ github.head_ref }} if: ${{ github.event_name == 'pull_request' }} run: | - echo "SDK_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV - name: set SDK Branch if not pull request + env: + REF_NAME: ${{ github.ref_name }} if: ${{ github.event_name != 'pull_request' }} run: | - echo "SDK_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV - echo "TRAVIS_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV + echo "TRAVIS_BRANCH=$REF_NAME" >> $GITHUB_ENV - name: Trigger build env: SDK: android @@ -75,14 +104,18 @@ jobs: path: 'home/runner/travisci-tools' ref: 'master' - name: set SDK Branch if PR + env: + HEAD_REF: ${{ github.head_ref }} if: ${{ github.event_name == 'pull_request' }} run: | - echo "SDK_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV - name: set SDK Branch if not pull request + env: + REF_NAME: ${{ github.ref_name }} if: ${{ github.event_name != 'pull_request' }} run: | - echo "SDK_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV - echo "TRAVIS_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV + echo "TRAVIS_BRANCH=$REF_NAME" >> $GITHUB_ENV - name: Trigger build env: SDK: ios diff --git a/CHANGELOG.md b/CHANGELOG.md index b07c65b..6e530c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,81 @@ # Optimizely Flutter SDK Changelog +## 3.0.1 +Jun 4th, 2025 + +### Functionality Enhancements + +* Add experiment id and variation id added into decision notification payload ([#80](https://github.com/optimizely/optimizely-flutter-sdk/pull/80)) + +## 3.0.0 +November 28th, 2024 + +### Breaking Changes +* VUID configuration is now independent of ODP ([#78](https://github.com/optimizely/optimizely-flutter-sdk/pull/78)) +* When VUID is disabled: + * `vuid` is not generated or saved. + * `client-initialized` event will not auto fired on SDK init. + * `vuid` is not included in the odp events as a default attribute. + * `createUserContext()` will be rejected if `userId` is not provided. + +## 2.0.1 +July 25, 2024 + +### Bug Fixes + +* Migration of flutter's gradle plugins ([#74](https://github.com/optimizely/optimizely-flutter-sdk/pull/74)). + +## 2.0.0 +January 23, 2024 + +### New Features + +The 2.0.0 release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) ([#52](https://github.com/optimizely/optimizely-flutter-sdk/pull/52), [#57](https://github.com/optimizely/optimizely-flutter-sdk/pull/57), [#72](https://github.com/optimizely/optimizely-flutter-sdk/pull/72)). + +You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can be used as a single source of truth for these segments in any Optimizely or 3rd party tool. + +With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Customer Success Manager. + +This version includes the following changes: + +* New API added to `OptimizelyUserContext`: + + - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays. + + - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities. + +* New APIs added to `OptimizelyFlutterSdk`: + + - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. + + - `createUserContext()` with anonymous user IDs: user-contexts can be created without a userId. The SDK will create and use a persistent `VUID` specific to a device when userId is not provided. + +For details, refer to our documentation pages: + +* [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) + +* [Client SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-client-side-sdks) + +* [Initialize Flutter SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-flutter) + +* [OptimizelyUserContext Flutter SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-flutter) + +* [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-flutter) + +* [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-flutter) + + +### Bug Fixes + +* Crash fixed, fetchQualifiedSegments without options ([#64](https://github.com/optimizely/optimizely-flutter-sdk/pull/64)). +* Fix proguard for logback and dart version ([#68](https://github.com/optimizely/optimizely-flutter-sdk/pull/68)). + +### Functionality Enhancements + +* Add specific client name support to track event ([#72](https://github.com/optimizely/optimizely-flutter-sdk/pull/72)). +* Update Github Issue Templates ([#65](https://github.com/optimizely/optimizely-flutter-sdk/pull/65)). +* Add configurable log level support ([#63](https://github.com/optimizely/optimizely-flutter-sdk/pull/63)). + ## 2.0.0-beta September 21, 2023 diff --git a/README.md b/README.md index e8a6edf..1b125d5 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Other Flutter platforms are not currently supported by this SDK. To add the flutter-sdk to your project dependencies, include the following in your app's pubspec.yaml: ``` - optimizely_flutter_sdk: ^1.0.1 + optimizely_flutter_sdk: ^3.0.1 ``` Then run diff --git a/android/build.gradle b/android/build.gradle index 03427ea..5d4e3ff 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,24 +1,17 @@ +plugins { + id "com.android.library" +} + group 'com.optimizely.optimizely_flutter_sdk' version '1.0' -buildscript { - - def version_name = System.getenv('TRAVIS_TAG') - if (version_name != null) { - rootProject.ext.version_name = version_name - } else { - rootProject.ext.version_name = 'debugVersion' - } - - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' - } +def version_name = System.getenv('TRAVIS_TAG') +if (version_name != null) { + rootProject.ext.version_name = version_name +} else { + rootProject.ext.version_name = 'debugVersion' } + configurations { all*.exclude group: 'com.google.guava', module: 'listenablefuture' } @@ -29,7 +22,6 @@ rootProject.allprojects { } } -apply plugin: 'com.android.library' ext { compile_sdk_version = 32 @@ -71,7 +63,6 @@ android { } - dependencies { implementation 'androidx.multidex:multidex:2.0.0' implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' @@ -83,7 +74,7 @@ dependencies { implementation 'org.slf4j:slf4j-api:2.0.7' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10" - implementation "com.optimizely.ab:android-sdk:4.0.0-beta2" + implementation "com.optimizely.ab:android-sdk:5.0.1" implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4' implementation ('com.google.guava:guava:19.0') { exclude group:'com.google.guava', module:'listenablefuture' diff --git a/android/proguard-rules.txt b/android/proguard-rules.txt index 7fede45..1564ced 100644 --- a/android/proguard-rules.txt +++ b/android/proguard-rules.txt @@ -10,5 +10,6 @@ # Optimizely -keep class com.optimizely.optimizely_flutter_sdk.** {*;} -keep class com.fasterxml.jackson.** {*;} +# Logback +-keep class ch.qos.** { *; } ##---------------End: proguard configuration ---------- - diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java index 82e3384..cf85e3d 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java @@ -55,6 +55,7 @@ import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.*; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.DISABLE_ODP; +import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.ENABLE_VUID; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.SEGMENTS_CACHE_SIZE; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.SEGMENTS_CACHE_TIMEOUT_IN_SECONDS; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.TIMEOUT_FOR_ODP_EVENT_IN_SECONDS; @@ -78,6 +79,8 @@ public class OptimizelyFlutterClient { protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @NonNull Result result) { String sdkKey = argumentsParser.getSdkKey(); + String sdkName = Utils.sdkName; + String sdkVersion = argumentsParser.getSdkVersion(); if (sdkKey == null) { result.success(createResponse(ErrorMessage.INVALID_PARAMS)); return; @@ -142,6 +145,7 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N int timeoutForSegmentFetchInSecs = 10; int timeoutForOdpEventInSecs = 10; boolean disableOdp = false; + boolean enableVuid = false; Map sdkSettings = argumentsParser.getOptimizelySdkSettings(); if (sdkSettings != null) { if (sdkSettings.containsKey(SEGMENTS_CACHE_SIZE)) { @@ -159,6 +163,9 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N if (sdkSettings.containsKey(DISABLE_ODP)) { disableOdp = (boolean) sdkSettings.get(DISABLE_ODP); } + if (sdkSettings.containsKey(ENABLE_VUID)) { + enableVuid = (boolean) sdkSettings.get(ENABLE_VUID); + } } // Creating new instance OptimizelyManager.Builder optimizelyManagerBuilder = OptimizelyManager.builder() @@ -172,10 +179,14 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N .withODPSegmentCacheTimeout(segmentsCacheTimeoutInSecs, TimeUnit.SECONDS) .withTimeoutForODPSegmentFetch(timeoutForSegmentFetchInSecs) .withTimeoutForODPEventDispatch(timeoutForOdpEventInSecs) - .withSDKKey(sdkKey); + .withSDKKey(sdkKey) + .withClientInfo(sdkName, sdkVersion); if (disableOdp) { optimizelyManagerBuilder.withODPDisabled(); } + if (enableVuid) { + optimizelyManagerBuilder.withVuidEnabled(); + } OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context); optimizelyManager.initialize(context, null, (OptimizelyClient client) -> { @@ -468,7 +479,7 @@ protected void getVuid(ArgumentsParser argumentsParser, @NonNull Result result) if (!isOptimizelyClientValid(sdkKey, optimizelyClient, result)) { return; } - result.success(createResponse(true, Collections.singletonMap(RequestParameterKey.VUID, optimizelyClient.getVuid()), "")); + result.success(createResponse(optimizelyClient.getVuid() != null, Collections.singletonMap(RequestParameterKey.VUID, optimizelyClient.getVuid()), "")); } /// Checks if the user is qualified for the given segment. diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java index 88087c7..6d2741f 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java @@ -31,6 +31,10 @@ public ArgumentsParser(Map arguments) { public String getSdkKey() { return (String) arguments.get(Constants.RequestParameterKey.SDK_KEY); } + + public String getSdkVersion() { + return (String) arguments.get(Constants.RequestParameterKey.SDK_VERSION); + } public Integer getNotificationID() { return (Integer) arguments.get(Constants.RequestParameterKey.NOTIFICATION_ID); diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java index 453b1d6..62f0ce9 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java @@ -58,6 +58,7 @@ public static class NotificationType { public static class RequestParameterKey { public static final String SDK_KEY = "sdkKey"; + public static final String SDK_VERSION = "sdkVersion"; public static final String USER_ID = "userId"; public static final String USER_CONTEXT_ID = "userContextId"; public static final String NOTIFICATION_ID = "id"; @@ -95,6 +96,7 @@ public static class RequestParameterKey { public static final String TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS = "timeoutForSegmentFetchInSecs"; public static final String TIMEOUT_FOR_ODP_EVENT_IN_SECONDS = "timeoutForOdpEventInSecs"; public static final String DISABLE_ODP = "disableOdp"; + public static final String ENABLE_VUID = "enableVuid"; } public static class ErrorMessage { diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java index b3020f1..ba4e5a4 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java @@ -37,7 +37,8 @@ import ch.qos.logback.classic.Logger; public class Utils { - + public static String sdkName = "flutter/android-sdk"; + public static String getRandomUUID() { return UUID.randomUUID().toString(); } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 89fc349..415ec79 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,10 @@ + +plugins { + id "com.android.application" + id "org.jetbrains.kotlin.android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +13,6 @@ if (localPropertiesFile.exists()) { } } -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' @@ -21,10 +23,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdkVersion 32 ndkVersion flutter.ndkVersion @@ -57,3 +55,7 @@ android { flutter { source '../..' } + +dependencies { + implementation 'com.android.support:multidex:1.0.3' +} diff --git a/example/android/build.gradle b/example/android/build.gradle index c133c66..81717b6 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,18 +1,8 @@ -buildscript { - ext.kotlin_version = '1.6.10' - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} ext { - android_sdk_version = "4.0.0-beta3" + android_sdk_version = "4.0.0" } + allprojects { repositories { google() @@ -28,6 +18,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bc..5710b01 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,34 @@ -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 + } + + settings.ext.flutterSdkPath = flutterSdkPath() + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + resolutionStrategy { + eachPlugin { + if (requested.id.namespace == 'dev.flutter') { + useModule("dev.flutter:${requested.id.name}:${requested.version}") + } + } + } +} -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.2.1" apply false + id "org.jetbrains.kotlin.android" version "1.6.10" apply false +} + +include ":app" -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" diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index e5be618..a29370a 100644 --- a/ios/Classes/HelperClasses/Constants.swift +++ b/ios/Classes/HelperClasses/Constants.swift @@ -114,6 +114,8 @@ struct RequestParameterKey { static let timeoutForSegmentFetchInSecs = "timeoutForSegmentFetchInSecs" static let timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs" static let disableOdp = "disableOdp" + static let enableVuid = "enableVuid" + static let sdkVersion = "sdkVersion"; } struct ResponseKey { diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/Classes/HelperClasses/Utils.swift index 6cac144..41b39c1 100644 --- a/ios/Classes/HelperClasses/Utils.swift +++ b/ios/Classes/HelperClasses/Utils.swift @@ -18,7 +18,7 @@ import Foundation import Optimizely public class Utils: NSObject { - + static var sdkName = "flutter/swift-sdk" /// Converts and returns dart map to native map static func getTypedMap(arguments: Any?) -> [String: Any]? { guard let args = arguments as? Dictionary else { diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 287b056..7c093c4 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -118,6 +118,10 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { var timeoutForSegmentFetchInSecs: Int = 10 var timeoutForOdpEventInSecs: Int = 10 var disableOdp: Bool = false + var enableVuid: Bool = false + var sdkVersion = parameters[RequestParameterKey.sdkVersion] as? String + var sdkName = Utils.sdkName + if let sdkSettings = parameters[RequestParameterKey.optimizelySdkSettings] as? Dictionary { if let cacheSize = sdkSettings[RequestParameterKey.segmentsCacheSize] as? Int { segmentsCacheSize = cacheSize @@ -134,8 +138,11 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { if let isOdpDisabled = sdkSettings[RequestParameterKey.disableOdp] as? Bool { disableOdp = isOdpDisabled } + if let isEnableVuid = sdkSettings[RequestParameterKey.enableVuid] as? Bool { + enableVuid = isEnableVuid + } } - let optimizelySdkSettings = OptimizelySdkSettings(segmentsCacheSize: segmentsCacheSize, segmentsCacheTimeoutInSecs: segmentsCacheTimeoutInSecs, timeoutForSegmentFetchInSecs: timeoutForSegmentFetchInSecs, timeoutForOdpEventInSecs: timeoutForOdpEventInSecs, disableOdp: disableOdp) + let optimizelySdkSettings = OptimizelySdkSettings(segmentsCacheSize: segmentsCacheSize, segmentsCacheTimeoutInSecs: segmentsCacheTimeoutInSecs, timeoutForSegmentFetchInSecs: timeoutForSegmentFetchInSecs, timeoutForOdpEventInSecs: timeoutForOdpEventInSecs, disableOdp: disableOdp, enableVuid: enableVuid, sdkName: sdkName, sdkVersion: sdkVersion) // Datafile Download Interval var datafilePeriodicDownloadInterval = 10 * 60 // seconds @@ -371,7 +378,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { } else { userContextsTracker[sdkKey] = [userContextId: userContext] } - result(self.createResponse(success: true, result: [RequestParameterKey.userContextId: userContextId])) + result(self.createResponse(success: userContext != nil, result: [RequestParameterKey.userContextId: userContextId])) } /// Returns userId for the user context. @@ -439,7 +446,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { return } - result(self.createResponse(success: true, result: [RequestParameterKey.vuid: optimizelyClient.vuid])) + result(self.createResponse(success: optimizelyClient.vuid != nil, result: [RequestParameterKey.vuid: optimizelyClient.vuid])) } /// Checks if the user is qualified for the given segment. diff --git a/ios/optimizely_flutter_sdk.podspec b/ios/optimizely_flutter_sdk.podspec index 2a528d5..2aa6953 100644 --- a/ios/optimizely_flutter_sdk.podspec +++ b/ios/optimizely_flutter_sdk.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'OptimizelySwiftSDK', '4.0.0-beta' + s.dependency 'OptimizelySwiftSDK', '5.1.1' s.platform = :ios, '10.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/lib/package_info.dart b/lib/package_info.dart new file mode 100644 index 0000000..0bdd780 --- /dev/null +++ b/lib/package_info.dart @@ -0,0 +1,7 @@ +// Purpose: Package information for the Optimizely Flutter SDK +// Note: This info should be synced with pubspec.yaml + +class PackageInfo { + static const String name = 'optimizely_flutter_sdk'; + static const String version = '3.0.1'; +} diff --git a/lib/src/data_objects/decide_response.dart b/lib/src/data_objects/decide_response.dart index dc2809a..29f8cdf 100644 --- a/lib/src/data_objects/decide_response.dart +++ b/lib/src/data_objects/decide_response.dart @@ -77,6 +77,7 @@ class BaseDecideResponse extends BaseResponse { if (json[Constants.responseResult] is Map) { final decisionsMap = Map.from(json[Constants.responseResult]); + // ignore: unnecessary_set_literal decisionsMap.forEach((k, v) => { if (v is Map) {_decisions[k] = Decision(Map.from(v))} diff --git a/lib/src/data_objects/get_vuid_response.dart b/lib/src/data_objects/get_vuid_response.dart index 4be045b..e5fcfb0 100644 --- a/lib/src/data_objects/get_vuid_response.dart +++ b/lib/src/data_objects/get_vuid_response.dart @@ -18,7 +18,7 @@ import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; class GetVuidResponse extends BaseResponse { - String vuid = ""; + String? vuid; GetVuidResponse(Map json) : super(json) { if (json[Constants.responseResult] is Map) { diff --git a/lib/src/data_objects/optimizely_config.dart b/lib/src/data_objects/optimizely_config.dart index 51afbef..b771cc0 100644 --- a/lib/src/data_objects/optimizely_config.dart +++ b/lib/src/data_objects/optimizely_config.dart @@ -33,6 +33,7 @@ class OptimizelyConfig { if (optimizelyConfig[Constants.experimentsMap] is Map) { final experimentsMapDynamic = Map.from(optimizelyConfig[Constants.experimentsMap]); + // ignore: unnecessary_set_literal experimentsMapDynamic.forEach((k, v) => { if (v is Map) { @@ -45,6 +46,7 @@ class OptimizelyConfig { if (optimizelyConfig[Constants.featuresMap] is Map) { final featuresMapDynamic = Map.from(optimizelyConfig[Constants.featuresMap]); + // ignore: unnecessary_set_literal featuresMapDynamic.forEach((k, v) => { if (v is Map) { @@ -288,6 +290,7 @@ class OptimizelyExperiment { if (parsedJson[Constants.variationsMap] is Map) { final variationsMapDynamic = Map.from(parsedJson[Constants.variationsMap]); + // ignore: unnecessary_set_literal variationsMapDynamic.forEach((k, v) => { if (v is Map) { @@ -338,6 +341,7 @@ class OptimizelyVariation { if (parsedJson[Constants.variablesMap] is Map) { final variablesMapDynamic = Map.from(parsedJson[Constants.variablesMap]); + // ignore: unnecessary_set_literal variablesMapDynamic.forEach((k, v) => { if (v is Map) { diff --git a/lib/src/data_objects/sdk_settings.dart b/lib/src/data_objects/sdk_settings.dart index 412432a..448a2e8 100644 --- a/lib/src/data_objects/sdk_settings.dart +++ b/lib/src/data_objects/sdk_settings.dart @@ -25,6 +25,8 @@ class SDKSettings { final int timeoutForOdpEventInSecs; // Set this flag to true (default = false) to disable ODP features final bool disableOdp; + // Set this flag to true (default = false) to enable VUID feature + final bool enableVuid; const SDKSettings({ this.segmentsCacheSize = 100, // Default segmentsCacheSize @@ -33,5 +35,6 @@ class SDKSettings { 10, // Default timeoutForSegmentFetchInSecs this.timeoutForOdpEventInSecs = 10, // Default timeoutForOdpEventInSecs this.disableOdp = false, // Default disableOdp + this.enableVuid = false, // Default disableVuid }); } diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index f96ad24..a0869b9 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -18,6 +18,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; +import 'package:optimizely_flutter_sdk/package_info.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/activate_listener_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/activate_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; @@ -66,8 +67,11 @@ class OptimizelyClientWrapper { _channel.setMethodCallHandler(methodCallHandler); final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions); final convertedLogLevel = Utils.convertLogLevel(defaultLogLevel); + const sdkVersion = PackageInfo.version; + Map requestDict = { Constants.sdkKey: sdkKey, + Constants.sdkVersion: sdkVersion, Constants.datafilePeriodicDownloadInterval: datafilePeriodicDownloadInterval, Constants.optimizelyDecideOption: convertedOptions, @@ -86,6 +90,7 @@ class OptimizelyClientWrapper { sdkSettings.timeoutForSegmentFetchInSecs, Constants.timeoutForOdpEventInSecs: sdkSettings.timeoutForOdpEventInSecs, Constants.disableOdp: sdkSettings.disableOdp, + Constants.enableVuid: sdkSettings.enableVuid, }; requestDict[Constants.optimizelySdkSettings] = optimizelySdkSettings; @@ -366,7 +371,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } @@ -412,7 +416,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } @@ -435,7 +438,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } @@ -459,7 +461,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index d5bc3ff..2bb5421 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -58,10 +58,13 @@ class Constants { // Request parameter keys static const String id = "id"; static const String sdkKey = "sdkKey"; + static const String sdkVersion = "sdkVersion"; static const String userContextId = "userContextId"; static const String userContext = "userContext"; static const String experiment = "experiment"; + static const String experimentId = "experimentId"; static const String variation = "variation"; + static const String variationId = "variationId"; static const String userId = "userId"; static const String vuid = "vuid"; static const String experimentKey = "experimentKey"; @@ -130,6 +133,7 @@ class Constants { "timeoutForSegmentFetchInSecs"; static const String timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs"; static const String disableOdp = "disableOdp"; + static const String enableVuid = "enableVuid"; // Response keys static const String responseSuccess = "success"; diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index 782178e..8b18b13 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -15,7 +15,6 @@ ///**************************************************************************/ import 'dart:io' show Platform; - import 'package:optimizely_flutter_sdk/src/user_context/optimizely_user_context.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; @@ -101,4 +100,5 @@ class Utils { // OptimizelyLogLevel.debug -> "debug" return logLevel.toString().split('.').last; } + } diff --git a/pubspec.yaml b/pubspec.yaml index c2f0293..188939d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: optimizely_flutter_sdk description: This repository houses the Flutter SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts. -version: 2.0.0-beta +version: 3.0.1 homepage: https://github.com/optimizely/optimizely-flutter-sdk environment: - sdk: '>=2.16.2 <=3.1.2' + sdk: '>=2.16.2 <4.0.0' flutter: ">=2.5.0" dependencies: diff --git a/test/optimizely_flutter_sdk_test.dart b/test/optimizely_flutter_sdk_test.dart index debf769..862c4b0 100644 --- a/test/optimizely_flutter_sdk_test.dart +++ b/test/optimizely_flutter_sdk_test.dart @@ -23,7 +23,6 @@ import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; import 'dart:io'; import 'dart:convert'; - import 'test_utils.dart'; void main() { @@ -57,7 +56,6 @@ void main() { SDKSettings sdkSettings = const SDKSettings(); int datafilePeriodicDownloadInterval = 0; String defaultLogLevel = "error"; - const MethodChannel channel = MethodChannel("optimizely_flutter_sdk"); dynamic mockOptimizelyConfig; @@ -107,6 +105,7 @@ void main() { timeoutForOdpEventInSecs: settings[Constants.timeoutForOdpEventInSecs], disableOdp: settings[Constants.disableOdp], + enableVuid: settings[Constants.enableVuid], ); } @@ -176,9 +175,19 @@ void main() { }; case Constants.createUserContextMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); - if (methodCall.arguments[Constants.userId] != null) { + var resultUserId = userContextId; + if (methodCall.arguments[Constants.userId] == null) { + if (sdkSettings.enableVuid) { + resultUserId = vuid; + } else { + return { + Constants.responseSuccess: false, + }; + } + } else if (methodCall.arguments[Constants.userId] != null) { expect(methodCall.arguments[Constants.userId], equals(userId)); } + if (methodCall.arguments[Constants.attributes]["abc"] != null) { expect(methodCall.arguments[Constants.attributes]["abc"], equals(attributes["abc"])); @@ -186,7 +195,7 @@ void main() { expect(methodCall.arguments[Constants.userContextId], isNull); return { Constants.responseSuccess: true, - Constants.responseResult: {Constants.userContextId: userContextId}, + Constants.responseResult: {Constants.userContextId: resultUserId}, }; case Constants.getUserIdMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); @@ -267,6 +276,8 @@ void main() { case Constants.getVuidMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); expect(methodCall.arguments[Constants.userContextId], isNull); + expect(methodCall.arguments[Constants.vuid], isNull); + var vuid = sdkSettings.enableVuid ? "vuid_123" : null; return { Constants.responseSuccess: true, Constants.responseResult: {Constants.vuid: vuid}, @@ -377,6 +388,7 @@ void main() { tearDown(() { tester?.setMockMethodCallHandler(channel, null); + sdkSettings = const SDKSettings(); }); group("Integration: OptimizelyFlutterSdk MethodChannel", () { @@ -557,7 +569,8 @@ void main() { }); test("with a valid defaultLogLevel parameter", () async { - var sdk = OptimizelyFlutterSdk(testSDKKey, defaultLogLevel: OptimizelyLogLevel.debug); + var sdk = OptimizelyFlutterSdk(testSDKKey, + defaultLogLevel: OptimizelyLogLevel.debug); var response = await sdk.initializeClient(); @@ -650,8 +663,17 @@ void main() { expect(userContext, isNotNull); }); - test("should succeed null userId", () async { + test("should fail when disable vuid and userId null", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); + sdk.initializeClient(); + var userContext = await sdk.createUserContext(attributes: attributes); + expect(userContext, isNull); + }); + + test("should succed when enable vuid and userId null", () async { + const settings = SDKSettings(enableVuid: true); + var sdk = OptimizelyFlutterSdk(testSDKKey, sdkSettings: settings); + sdk.initializeClient(); var userContext = await sdk.createUserContext(attributes: attributes); expect(userContext, isNotNull); }); @@ -662,10 +684,11 @@ void main() { expect(userContext, isNotNull); }); - test("should succeed null userId and attributes", () async { + test("should not succeed null userId and attributes", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); + sdk.initializeClient(); var userContext = await sdk.createUserContext(); - expect(userContext, isNotNull); + expect(userContext, isNull); }); }); @@ -769,11 +792,20 @@ void main() { }); group("getVuid()", () { - test("should succeed", () async { + test("by default should return null vuid", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); + sdk.initializeClient(); var response = await sdk.getVuid(); expect(response.success, isTrue); - expect(response.vuid, equals(vuid)); + expect(response.vuid, isNull); + }); + test("should return vuid when enableVuid true", () async { + const settings = SDKSettings(enableVuid: true); + var sdk = OptimizelyFlutterSdk(testSDKKey, sdkSettings: settings); + sdk.initializeClient(); + var response = await sdk.getVuid(); + expect(response.success, isTrue); + expect(response.vuid, "vuid_123"); }); }); @@ -1553,4 +1585,76 @@ void main() { }); }); }); + + group("Test client_name and client_version event parsing", () { + test("Test track event with swift sdk name and version", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + + TrackListenerResponse? response; + + await sdk.addTrackNotificationListener((msg) { + response = msg; + }); + var callHandler = OptimizelyClientWrapper.methodCallHandler; + tester?.setMockMethodCallHandler(channel, callHandler); + TestUtils.sendTestTrackClientNameAndVersion( + callHandler, 0, testSDKKey, "flutter/swift-sdk", "2.0.0"); + + expect(response == null, false); + + expect(response!.eventTags!["client_name"], "flutter/swift-sdk"); + + }); + + test("Test track event with android sdk name and version", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + + var responses = []; + + await sdk.addTrackNotificationListener((msg) { + responses.add(msg); + }); + + var callHandler = OptimizelyClientWrapper.methodCallHandler; + tester?.setMockMethodCallHandler(channel, callHandler); + + TestUtils.sendTestTrackClientNameAndVersion( + callHandler, 0, testSDKKey, "flutter/swift-sdk", "2.0.0"); + TestUtils.sendTestTrackClientNameAndVersion( + callHandler, 0, testSDKKey, "flutter/android-sdk", "2.0.0"); + + expect(responses.length == 2, true); + + expect(responses[0]!.eventTags!["client_name"], "flutter/swift-sdk"); + expect(responses[0]!.eventTags!["client_version"], "2.0.0"); + expect(responses[1]!.eventTags!["client_name"], "flutter/android-sdk"); + expect(responses[1]!.eventTags!["client_version"], "2.0.0"); + + }); + + test("Test log event with client sdk name and version", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + + var responses = []; + + await sdk.addLogEventNotificationListener((msg) { + responses.add(msg); + }); + var callHandler = OptimizelyClientWrapper.methodCallHandler; + tester?.setMockMethodCallHandler(channel, callHandler); + TestUtils.sendTestClientNameAndVersionLogEventNotification( + callHandler, 0, testSDKKey, "flutter/android-sdk", "2.0.0"); + + TestUtils.sendTestClientNameAndVersionLogEventNotification( + callHandler, 0, testSDKKey, "flutter/swift-sdk", "2.0.0"); + + expect(responses.length == 2, true); + + expect(responses[0]!.params!["client_name"], "flutter/android-sdk"); + expect(responses[0]!.params!["client_version"], "2.0.0"); + expect(responses[1]!.params!["client_name"], "flutter/swift-sdk"); + expect(responses[1]!.params!["client_version"], "2.0.0"); + }); + + }); } diff --git a/test/test_utils.dart b/test/test_utils.dart index e08eb73..350c35e 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -89,7 +89,14 @@ class TestUtils { handler(MethodCall(Constants.decisionCallBackListener, { Constants.id: id, Constants.sdkKey: sdkKey, - Constants.payload: {Constants.type: "$id", Constants.userId: "test"} + Constants.payload: { + Constants.type: "$id", + Constants.userId: "test", + Constants.decisionInfo: const { + Constants.experimentId: "experiment_12345", + Constants.variationId: "variation_12345", + }, + } })); } @@ -106,13 +113,62 @@ class TestUtils { })); } + static sendTestClientNameAndVersionLogEventNotification( + Function(MethodCall message) handler, int id, String sdkKey, String clientName, String sdkVersion) { + var payload = { + Constants.url: "$id", + Constants.params: { + "test": id, + "client_name": clientName, + "client_version": sdkVersion + } + }; + handler(MethodCall(Constants.logEventCallbackListener, { + Constants.id: id, + Constants.sdkKey: sdkKey, + Constants.payload: payload + })); + } + static sendTestTrackNotifications( Function(MethodCall message) handler, int id, String sdkKey) { var payload = { Constants.eventKey: "$id", Constants.userId: "test", Constants.attributes: {"test": id}, - Constants.eventTags: {"testTag": id} + Constants.eventTags: { + "testTag": id, + "nestedTag": { + "string_key": "stringValue", + "int_key": 123, + "double_key": 123.456, + "bool_key": true + } + } + }; + handler(MethodCall(Constants.trackCallBackListener, { + Constants.id: id, + Constants.sdkKey: sdkKey, + Constants.payload: payload + })); + } + + static sendTestTrackClientNameAndVersion(Function(MethodCall message) handler, int id, String sdkKey, String clientName, String sdkVersion) { + var payload = { + Constants.eventKey: "$id", + Constants.userId: "test", + Constants.attributes: {"test": id}, + Constants.eventTags: { + "testTag": id, + "nestedTag": { + "string_key": "stringValue", + "int_key": 123, + "double_key": 123.456, + "bool_key": true + }, + "client_name": clientName, + "client_version": sdkVersion + } }; handler(MethodCall(Constants.trackCallBackListener, { Constants.id: id, @@ -144,7 +200,11 @@ class TestUtils { static bool testDecisionNotificationPayload( List notifications, int id, int actualID) { if (notifications[id].type != "$actualID" || - notifications[id].userId != "test") { + notifications[id].userId != "test" || + notifications[id].decisionInfo[Constants.experimentId] != + "experiment_12345" || + notifications[id].decisionInfo[Constants.variationId] != + "variation_12345") { return false; } return true;