diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml new file mode 100644 index 0000000..c4796a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -0,0 +1,82 @@ +name: 🐞 Bug +description: File a bug/issue +title: "[BUG] " +labels: ["bug", "needs-triage"] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: SDK Version + description: Version of the SDK in use? + validations: + required: true +- type: textarea + attributes: + label: flutter Version + description: Version of flutter in use? + validations: + required: true +- type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 1. With this config... + 1. Run '...' + 1. See error... + validations: + required: true +- type: textarea + attributes: + label: Link + description: Link to code demonstrating the problem. + validations: + required: false +- type: textarea + attributes: + label: Logs + description: Logs/stack traces related to the problem (⚠️do not include sensitive information). + validations: + required: false +- type: dropdown + attributes: + label: Severity + description: What is the severity of the problem? + multiple: true + options: + - Blocking development + - Affecting users + - Minor issue + validations: + required: false +- type: textarea + attributes: + label: Workaround/Solution + description: Do you have any workaround or solution in mind for the problem? + validations: + required: false +- type: textarea + attributes: + label: "Recent Change" + description: Has this issue started happening after an update or experiment change? + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml new file mode 100644 index 0000000..79c5324 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml @@ -0,0 +1,45 @@ +name: ✨Enhancement +description: Create a new ticket for a Enhancement/Tech-initiative for the benefit of the SDK which would be considered for a minor version update. +title: "[ENHANCEMENT] <title>" +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: "Description" + description: Briefly describe the enhancement in a few sentences. + placeholder: Short description... + validations: + required: true + - type: textarea + id: benefits + attributes: + label: "Benefits" + description: How would the enhancement benefit to your product or usage? + placeholder: Benefits... + validations: + required: true + - type: textarea + id: detail + attributes: + label: "Detail" + description: How would you like the enhancement to work? Please provide as much detail as possible + placeholder: Detailed description... + validations: + required: false + - type: textarea + id: examples + attributes: + label: "Examples" + description: Are there any examples of this enhancement in other products/services? If so, please provide links or references. + placeholder: Links/References... + validations: + required: false + - type: textarea + id: risks + attributes: + label: "Risks/Downsides" + description: Do you think this enhancement could have any potential downsides or risks? + placeholder: Risks/Downsides... + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md new file mode 100644 index 0000000..a061f33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md @@ -0,0 +1,4 @@ +<!-- + Thanks for filing in issue! Are you requesting a new feature? If so, please share your feedback with us on the following link. +--> +## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d28ef3d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 💡Feature Requests + url: https://feedback.optimizely.com/ + about: Feedback requesting a new feature can be shared here. \ No newline at end of file diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 03fb086..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: @@ -24,7 +34,7 @@ jobs: - name: Upload coverage to Coveralls uses: coverallsapp/github-action@master with: - github-token: ${{ secrets.CI_USER_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} integration_android_tests: runs-on: ubuntu-latest @@ -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 0a7fb68..6e530c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,115 @@ # Optimizely Flutter SDK Changelog -## [1.0.0-beta] - November 3rd, 2022 + +## 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 + +### New Features + +* Add ODP for iOS ([#52](https://github.com/optimizely/optimizely-flutter-sdk/pull/52)). +* Add ODP for Android ([#57](https://github.com/optimizely/optimizely-flutter-sdk/pull/57)). + +### Bug Fixes + +* Crash fixed, fetchQualifiedSegments without options ([#64](https://github.com/optimizely/optimizely-flutter-sdk/pull/64)). + +### Functionality Enhancements + +* 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)). + +## 1.0.1 +May 8, 2023 + +**Official General Availability (GA) release** + +### Bug Fixes + +* Fix "no serializer found" error ([#51](https://github.com/optimizely/optimizely-flutter-sdk/pull/51)). + +## 1.0.1-beta +March 10, 2022 + +* We updated our README.md and other non-functional code to reflect that this SDK supports both Optimizely Feature Experimentation and Optimizely Full Stack. ([#44](https://github.com/optimizely/optimizely-flutter-sdk/pull/44)). + +## 1.0.0-beta +November 3, 2022 + **Beta release of the Optimizely X Full Stack Flutter SDK.** ### New Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f25fd55 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing to the Optimizely Flutter SDK +We welcome contributions and feedback! All contributors must sign our [Contributor License Agreement (CLA)](https://docs.google.com/a/optimizely.com/forms/d/e/1FAIpQLSf9cbouWptIpMgukAKZZOIAhafvjFCV8hS00XJLWQnWDFtwtA/viewform) to be eligible to contribute. Please read the [README](README.md) to set up your development environment, then read the guidelines below for information on submitting your code. + +## Development process + +1. Fork the repository and create your branch from master. +2. Please follow the [commit message guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines) for each commit message. +3. Make sure to add tests! +4. `git push` your changes to GitHub. +5. Open a PR from your fork into the master branch of the original repo. +6. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`. +7. Open a pull request from `YOUR_NAME/branch_name` to `master`. +8. A repository maintainer will review your pull request and, if all goes well, squash and merge it! + +## Pull request acceptance criteria + +* **All code must have test coverage.** Changes in functionality should have accompanying unit tests. Bug fixes should have accompanying regression tests. +* Tests are located in `OptimizelySDK.Tests` with one file per class. + +## License + +All contributions are under the CLA mentioned above. For this project, Optimizely uses the Apache 2.0 license, and so asks that by contributing your code, you agree to license your contribution under the terms of the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). Your contributions should also include the following header: + +``` +/// ************************************************************************** +/// Copyright YEAR, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// https://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ +``` + +The YEAR above should be the year of the contribution. If work on the file has been done over multiple years, list each year in the section above. Example: Optimizely writes the file and releases it in 2014. No changes are made in 2015. Change made in 2016. YEAR should be �2014, 2016�. + +## Contact +If you have questions, please contact developers@optimizely.com. \ No newline at end of file diff --git a/README.md b/README.md index 5995611..1b125d5 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,68 @@ # Optimizely Flutter SDK +[![Pub Version](https://img.shields.io/pub/v/optimizely_flutter_sdk?color=blueviolet)](https://pub.dev/packages/optimizely_flutter_sdk) +[![Pub](https://img.shields.io/pub/v/optimizely_flutter_sdk.svg)](https://pub.dev/packages/optimizely_flutter_sdk) [![Apache 2.0](https://img.shields.io/github/license/nebula-plugins/gradle-extra-configurations-plugin.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![Build Status](https://github.com/optimizely/optimizely-flutter-sdk/actions/workflows/flutter.yml/badge.svg?branch=master)](https://github.com/optimizely/optimizely-flutter-sdk/actions) -[![Pub](https://img.shields.io/pub/v/optimizely_flutter_sdk.svg)](https://pub.dev/packages/optimizely_flutter_sdk) [![Coverage Status](https://coveralls.io/repos/github/optimizely/optimizely-flutter-sdk/badge.svg?branch=master)](https://coveralls.io/github/optimizely/optimizely-flutter-sdk?branch=master) -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. +This repository houses the Flutter SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy). + +Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome). + +Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feature-flagging/) for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap. -## Getting Started +## Get Started -### Using the SDK -Refer to the [Flutter SDK developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/install-sdk-flutter) for instructions on getting started with using the SDK. +Refer to the [Flutter SDK's developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/flutter-sdk) for detailed instructions on getting started with using the SDK. ### Requirements See the [pubspec.yaml](https://github.com/optimizely/optimizely-flutter-sdk/blob/master/pubspec.yaml) file for Flutter version requirements. -On the Android platform, the SDK requires a minimum SDK version of 14 or higher and compile SDK version of 32. +On the Android platform, the SDK requires a minimum SDK version of 21 or higher and compile SDK version of 32. On the iOS platform, the SDK requires a minimum version of 10.0. Other Flutter platforms are not currently supported by this SDK. -### Installing the SDK +### Install the SDK To add the flutter-sdk to your project dependencies, include the following in your app's pubspec.yaml: ``` - optimizely_flutter_sdk: ^1.0.0-beta + optimizely_flutter_sdk: ^3.0.1 ``` -Then, import the package in your application code: +Then run -``` - import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; +```bash + flutter pub get ``` -## Usage +## Use the Flutter SDK -### Instantiation +### Initialization -A sample code for SDK initialization: +Import the package in your application code: +```dart + import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; ``` - var flutterSDK = OptimizelyFlutterSdk("my_sdk_key"); + +Instantiate the SDK, adding your SDK Key and initializing the client: + +```dart + var flutterSDK = OptimizelyFlutterSdk("your_sdk_key"); var response = await flutterSDK.initializeClient(); ``` ### Feature Rollouts -``` +```dart import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; // Also supports eventOptions, datafilePeriodicDownloadInterval, datafileHostOptions and defaultDecideOptions - var flutterSDK = OptimizelyFlutterSdk("my_sdk_key"); + var flutterSDK = OptimizelyFlutterSdk("your_sdk_key"); - // instantiate a client var response = await flutterSDK.initializeClient(); // User attributes are optional and used for targeting and results segmentation @@ -64,11 +70,46 @@ A sample code for SDK initialization: "state": "California", "likes_donuts": true }; - var user = await flutterSDK.createUserContext("optimizely end user", attributes); + var user = await flutterSDK.createUserContext("user_id", attributes); var decideReponse = await user!.decide("binary_feature"); ``` -## Testing in Terminal +## SDK Development + +### Unit Tests + +To run [unit tests](https://docs.flutter.dev/cookbook/testing/unit/introduction) using terminal, simply use the following command: + +```bash +flutter test test/optimizely_flutter_sdk_test.dart +``` + +### Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md). + +### Other Optimizely SDKs + +- Agent - https://github.com/optimizely/agent + +- Android - https://github.com/optimizely/android-sdk + +- C# - https://github.com/optimizely/csharp-sdk + +- Flutter - https://github.com/optimizely/optimizely-flutter-sdk + +- Go - https://github.com/optimizely/go-sdk + +- Java - https://github.com/optimizely/java-sdk + +- JavaScript - https://github.com/optimizely/javascript-sdk + +- PHP - https://github.com/optimizely/php-sdk + +- Python - https://github.com/optimizely/python-sdk + +- React - https://github.com/optimizely/react-sdk + +- Ruby - https://github.com/optimizely/ruby-sdk -1. To run [unit tests](https://docs.flutter.dev/cookbook/testing/unit/introduction) using terminal, simply use the following command: -`flutter test test/optimizely_flutter_sdk_test.dart` +- Swift - https://github.com/optimizely/swift-sdk diff --git a/android/build.gradle b/android/build.gradle index d2e1bf2..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,12 +22,11 @@ rootProject.allprojects { } } -apply plugin: 'com.android.library' ext { compile_sdk_version = 32 build_tools_version = "30.0.3" - min_sdk_version = 14 + min_sdk_version = 21 target_sdk_version = 29 } @@ -49,6 +41,9 @@ android { versionName version_name buildConfigField "String", "CLIENT_VERSION", "\"$version_name\"" multiDexEnabled true + + // these rules will be merged to app's proguard rules + consumerProguardFiles './proguard-rules.txt' } compileOptions { @@ -61,21 +56,26 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } - dependencies { implementation 'androidx.multidex:multidex:2.0.0' implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' - implementation group: 'org.slf4j', name: 'slf4j-android', version: '1.7.25' + + //"logback-android" required for programmatic control of global sl4j log level. + // - default log configuration in /assets/logback.xml + // - [ref] https://github.com/tony19/logback-android + implementation 'com.github.tony19:logback-android:3.0.0' + implementation 'org.slf4j:slf4j-api:2.0.7' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10" - implementation "com.optimizely.ab:android-sdk:3.13.2" - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + 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 new file mode 100644 index 0000000..1564ced --- /dev/null +++ b/android/proguard-rules.txt @@ -0,0 +1,15 @@ +# Add project specific ProGuard rules here. +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# 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/assets/logback.xml b/android/src/main/assets/logback.xml new file mode 100644 index 0000000..8e6e0d6 --- /dev/null +++ b/android/src/main/assets/logback.xml @@ -0,0 +1,18 @@ +<configuration + xmlns="https://tony19.github.io/logback-android/xml" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd" +> + <appender name="logcat" class="ch.qos.logback.classic.android.LogcatAppender"> + <tagEncoder> + <pattern>Optimizely</pattern> + </tagEncoder> + <encoder> + <pattern>%msg</pattern> + </encoder> + </appender> + + <root level="DEBUG"> + <appender-ref ref="logcat" /> + </root> +</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 2e3bd69..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 @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2022, Optimizely, Inc. and contributors * + * Copyright 2022-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -46,6 +46,7 @@ import com.optimizely.ab.notification.NotificationCenter; import com.optimizely.ab.notification.TrackNotification; import com.optimizely.ab.notification.UpdateConfigNotification; +import com.optimizely.ab.odp.ODPSegmentOption; import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecision; @@ -53,6 +54,12 @@ import com.optimizely.optimizely_flutter_sdk.helper_classes.Utils; 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; +import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Utils.getNotificationListenerType; import java.util.Collections; @@ -72,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; @@ -91,6 +100,8 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N maxQueueSize = argumentsParser.getEventMaxQueueSize(); } + Utils.setDefaultLogLevel(argumentsParser.getDefaultLogLevel()); + DefaultEventHandler eventHandler = DefaultEventHandler.getInstance(context); eventHandler.setDispatchInterval(-1L); NotificationCenter notificationCenter = new NotificationCenter(); @@ -127,16 +138,56 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N notificationIdsTracker.remove(sdkKey); List<OptimizelyDecideOption> defaultDecideOptions = argumentsParser.getDecideOptions(); + + // SDK Settings Default Values + int segmentsCacheSize = 100; + int segmentsCacheTimeoutInSecs = 600; + int timeoutForSegmentFetchInSecs = 10; + int timeoutForOdpEventInSecs = 10; + boolean disableOdp = false; + boolean enableVuid = false; + Map<String, Object> sdkSettings = argumentsParser.getOptimizelySdkSettings(); + if (sdkSettings != null) { + if (sdkSettings.containsKey(SEGMENTS_CACHE_SIZE)) { + segmentsCacheSize = (Integer) sdkSettings.get(SEGMENTS_CACHE_SIZE); + } + if (sdkSettings.containsKey(SEGMENTS_CACHE_TIMEOUT_IN_SECONDS)) { + segmentsCacheTimeoutInSecs = (Integer) sdkSettings.get(SEGMENTS_CACHE_TIMEOUT_IN_SECONDS); + } + if (sdkSettings.containsKey(TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS)) { + timeoutForSegmentFetchInSecs = (Integer) sdkSettings.get(TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS); + } + if (sdkSettings.containsKey(TIMEOUT_FOR_ODP_EVENT_IN_SECONDS)) { + timeoutForOdpEventInSecs = (Integer) sdkSettings.get(TIMEOUT_FOR_ODP_EVENT_IN_SECONDS); + } + 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 optimizelyManager = OptimizelyManager.builder() + OptimizelyManager.Builder optimizelyManagerBuilder = OptimizelyManager.builder() .withEventProcessor(batchProcessor) .withEventHandler(eventHandler) .withNotificationCenter(notificationCenter) .withDatafileDownloadInterval(datafilePeriodicDownloadInterval, TimeUnit.SECONDS) .withErrorHandler(new RaiseExceptionErrorHandler()) .withDefaultDecideOptions(defaultDecideOptions) + .withODPSegmentCacheSize(segmentsCacheSize) + .withODPSegmentCacheTimeout(segmentsCacheTimeoutInSecs, TimeUnit.SECONDS) + .withTimeoutForODPSegmentFetch(timeoutForSegmentFetchInSecs) + .withTimeoutForODPEventDispatch(timeoutForOdpEventInSecs) .withSDKKey(sdkKey) - .build(context); + .withClientInfo(sdkName, sdkVersion); + if (disableOdp) { + optimizelyManagerBuilder.withODPDisabled(); + } + if (enableVuid) { + optimizelyManagerBuilder.withVuidEnabled(); + } + OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context); optimizelyManager.initialize(context, null, (OptimizelyClient client) -> { if (client.isValid()) { @@ -158,14 +209,14 @@ protected void createUserContext(ArgumentsParser argumentsParser, @NonNull Resul String userId = argumentsParser.getUserId(); Map<String, Object> attributes = argumentsParser.getAttributes(); - if (userId == null) { - result.success(createResponse(ErrorMessage.INVALID_PARAMS)); - return; - } try { String userContextId = Utils.getRandomUUID(); - - OptimizelyUserContext optlyUserContext = optimizelyClient.createUserContext(userId, attributes); + OptimizelyUserContext optlyUserContext; + if (userId != null) { + optlyUserContext = optimizelyClient.createUserContext(userId, attributes); + } else { + optlyUserContext = optimizelyClient.createUserContext(attributes); + } if (optlyUserContext != null) { if (userContextsTracker.containsKey(sdkKey)) { userContextsTracker.get(sdkKey).put(userContextId, optlyUserContext); @@ -390,6 +441,109 @@ protected void removeAllForcedDecisions(ArgumentsParser argumentsParser, @NonNul result.success(createResponse()); } + /// Returns an array of segments that the user is qualified for. + protected void getQualifiedSegments(ArgumentsParser argumentsParser, @NonNull Result result) { + String sdkKey = argumentsParser.getSdkKey(); + OptimizelyUserContext userContext = getUserContext(argumentsParser); + if (!isUserContextValid(sdkKey, userContext, result)) { + return; + } + List<String> qualifiedSegments = userContext.getQualifiedSegments(); + if (qualifiedSegments != null) { + result.success(createResponse(Collections.singletonMap(RequestParameterKey.QUALIFIED_SEGMENTS, qualifiedSegments))); + } else { + result.success(createResponse(ErrorMessage.QUALIFIED_SEGMENTS_NOT_FOUND)); + } + } + + /// Sets qualified segments for the user context. + protected void setQualifiedSegments(ArgumentsParser argumentsParser, @NonNull Result result) { + String sdkKey = argumentsParser.getSdkKey(); + OptimizelyUserContext userContext = getUserContext(argumentsParser); + if (!isUserContextValid(sdkKey, userContext, result)) { + return; + } + List<String> qualifiedSegments = argumentsParser.getQualifiedSegments(); + if (qualifiedSegments == null) { + result.success(createResponse(ErrorMessage.INVALID_PARAMS)); + return; + } + userContext.setQualifiedSegments(qualifiedSegments); + result.success(createResponse()); + } + + /// Returns the device vuid. + protected void getVuid(ArgumentsParser argumentsParser, @NonNull Result result) { + String sdkKey = argumentsParser.getSdkKey(); + OptimizelyClient optimizelyClient = getOptimizelyClient(sdkKey); + if (!isOptimizelyClientValid(sdkKey, optimizelyClient, result)) { + return; + } + result.success(createResponse(optimizelyClient.getVuid() != null, Collections.singletonMap(RequestParameterKey.VUID, optimizelyClient.getVuid()), "")); + } + + /// Checks if the user is qualified for the given segment. + protected void isQualifiedFor(ArgumentsParser argumentsParser, @NonNull Result result) { + String sdkKey = argumentsParser.getSdkKey(); + OptimizelyUserContext userContext = getUserContext(argumentsParser); + if (!isUserContextValid(sdkKey, userContext, result)) { + return; + } + String segment = argumentsParser.getSegment(); + if (segment == null) { + result.success(createResponse(ErrorMessage.INVALID_PARAMS)); + return; + } + + result.success(createResponse(userContext.isQualifiedFor(segment))); + } + + /// Send an event to the ODP server. + protected void sendODPEvent(ArgumentsParser argumentsParser, @NonNull Result result) { + String sdkKey = argumentsParser.getSdkKey(); + OptimizelyClient optimizelyClient = getOptimizelyClient(sdkKey); + if (!isOptimizelyClientValid(sdkKey, optimizelyClient, result)) { + return; + } + String action = argumentsParser.getAction(); + if (action == null || action.isEmpty()) { + result.success(createResponse(ErrorMessage.INVALID_PARAMS)); + return; + } + + String type = argumentsParser.getType(); + Map<String, String> identifiers = argumentsParser.getIdentifiers(); + if (identifiers == null) { + identifiers = new HashMap<>(); + } + Map<String, Object> data = argumentsParser.getData(); + if (data == null) { + data = new HashMap<>(); + } + + optimizelyClient.sendODPEvent(type, action, identifiers, data); + result.success(createResponse()); + } + + /// Fetch all qualified segments for the user context. + protected void fetchQualifiedSegments(ArgumentsParser argumentsParser, @NonNull Result result) { + String sdkKey = argumentsParser.getSdkKey(); + OptimizelyUserContext userContext = getUserContext(argumentsParser); + if (!isUserContextValid(sdkKey, userContext, result)) { + return; + } + List<ODPSegmentOption> segmentOptions = argumentsParser.getSegmentOptions(); + + try { + userContext.fetchQualifiedSegments((fetchQualifiedResult) -> { + result.success(createResponse(fetchQualifiedResult)); + },segmentOptions); + + } catch (Exception ex) { + result.success(createResponse(ex.getMessage())); + } + } + protected void close(ArgumentsParser argumentsParser, @NonNull Result result) { String sdkKey = argumentsParser.getSdkKey(); OptimizelyClient optimizelyClient = getOptimizelyClient(sdkKey); diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java index edc49c7..89f787c 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2022, Optimizely, Inc. and contributors * + * Copyright 2022-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -119,6 +119,30 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { removeAllForcedDecisions(argumentsParser, result); break; } + case APIs.GET_QUALIFIED_SEGMENTS: { + getQualifiedSegments(argumentsParser, result); + break; + } + case APIs.SET_QUALIFIED_SEGMENTS: { + setQualifiedSegments(argumentsParser, result); + break; + } + case APIs.GET_VUID: { + getVuid(argumentsParser, result); + break; + } + case APIs.IS_QUALIFIED_FOR: { + isQualifiedFor(argumentsParser, result); + break; + } + case APIs.SEND_ODP_EVENT: { + sendODPEvent(argumentsParser, result); + break; + } + case APIs.FETCH_QUALIFIED_SEGMENTS: { + fetchQualifiedSegments(argumentsParser, result); + break; + } case APIs.CLOSE: { close(argumentsParser, result); break; 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 d726f21..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 @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2022, Optimizely, Inc. and contributors * + * Copyright 2022-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -15,6 +15,7 @@ ***************************************************************************/ package com.optimizely.optimizely_flutter_sdk.helper_classes; +import com.optimizely.ab.odp.ODPSegmentOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import java.util.List; @@ -30,6 +31,10 @@ public ArgumentsParser(Map<String, ?> 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); @@ -71,6 +76,10 @@ public List<OptimizelyDecideOption> getDecideOptions() { return Utils.getDecideOptions((List<String>) arguments.get(Constants.RequestParameterKey.DECIDE_OPTIONS)); } + public String getDefaultLogLevel() { + return (String) arguments.get(Constants.RequestParameterKey.DEFAULT_LOG_LEVEL); + } + public String getFlagKey() { return (String) arguments.get(Constants.RequestParameterKey.FLAG_KEY); } @@ -110,4 +119,36 @@ public String getDatafileHostPrefix() { public String getExperimentKey() { return (String) arguments.get(Constants.RequestParameterKey.EXPERIMENT_KEY); } + + public List<String> getQualifiedSegments() { + return (List<String>) arguments.get(Constants.RequestParameterKey.QUALIFIED_SEGMENTS); + } + + public String getSegment() { + return (String) arguments.get(Constants.RequestParameterKey.SEGMENT); + } + + public String getAction() { + return (String) arguments.get(Constants.RequestParameterKey.ACTION); + } + + public String getType() { + return (String) arguments.get(Constants.RequestParameterKey.ODP_EVENT_TYPE); + } + + public Map<String, String> getIdentifiers() { + return (Map<String, String>) arguments.get(Constants.RequestParameterKey.IDENTIFIERS); + } + + public Map<String, Object> getData() { + return (Map<String, Object>) arguments.get(Constants.RequestParameterKey.DATA); + } + + public List<ODPSegmentOption> getSegmentOptions() { + return Utils.getSegmentOptions((List<String>) arguments.get(Constants.RequestParameterKey.OPTIMIZELY_SEGMENT_OPTION)); + } + + public Map<String, Object> getOptimizelySdkSettings() { + return (Map<String, Object>) arguments.get(Constants.RequestParameterKey.OPTIMIZELY_SDK_SETTINGS); + } } 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 8e1b40b..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 @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2022, Optimizely, Inc. and contributors * + * Copyright 2022-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -38,6 +38,14 @@ public static class APIs { public static final String REMOVE_NOTIFICATION_LISTENER = "removeNotificationListener"; public static final String CLEAR_ALL_NOTIFICATION_LISTENERS = "clearAllNotificationListeners"; public static final String CLEAR_NOTIFICATION_LISTENERS = "clearNotificationListeners"; + + // ODP APIs constants + public static final String SEND_ODP_EVENT = "sendOdpEvent"; + public static final String GET_VUID = "getVuid"; + public static final String GET_QUALIFIED_SEGMENTS = "getQualifiedSegments"; + public static final String SET_QUALIFIED_SEGMENTS = "setQualifiedSegments"; + public static final String IS_QUALIFIED_FOR = "isQualifiedFor"; + public static final String FETCH_QUALIFIED_SEGMENTS = "fetchQualifiedSegments"; } public static class NotificationType { @@ -50,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"; @@ -59,6 +68,7 @@ public static class RequestParameterKey { public static final String ATTRIBUTES = "attributes"; public static final String DECIDE_KEYS = "keys"; public static final String DECIDE_OPTIONS = "optimizelyDecideOption"; + public static final String DEFAULT_LOG_LEVEL = "defaultLogLevel"; public static final String EVENT_BATCH_SIZE = "eventBatchSize"; public static final String EVENT_TIME_INTERVAL = "eventTimeInterval"; public static final String EVENT_MAX_QUEUE_SIZE = "eventMaxQueueSize"; @@ -71,6 +81,22 @@ public static class RequestParameterKey { public static final String VARIATION_KEY = "variationKey"; public static final String DATAFILE_HOST_PREFIX = "datafileHostPrefix"; public static final String DATAFILE_HOST_SUFFIX = "datafileHostSuffix"; + + public static final String VUID = "vuid"; + public static final String QUALIFIED_SEGMENTS = "qualifiedSegments"; + public static final String SEGMENT = "segment"; + public static final String ACTION = "action"; + public static final String IDENTIFIERS = "identifiers"; + public static final String DATA = "data"; + public static final String ODP_EVENT_TYPE = "type"; + public static final String OPTIMIZELY_SEGMENT_OPTION = "optimizelySegmentOption"; + public static final String OPTIMIZELY_SDK_SETTINGS = "optimizelySdkSettings"; + public static final String SEGMENTS_CACHE_SIZE = "segmentsCacheSize"; + public static final String SEGMENTS_CACHE_TIMEOUT_IN_SECONDS = "segmentsCacheTimeoutInSecs"; + 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 { @@ -80,6 +106,7 @@ public static class ErrorMessage { public static final String OPTIMIZELY_CLIENT_NOT_FOUND = "Optimizely client not found."; public static final String USER_CONTEXT_NOT_FOUND = "User context not found."; public static final String USER_CONTEXT_NOT_CREATED = "User context not created."; + public static final String QUALIFIED_SEGMENTS_NOT_FOUND = "Qualified Segments not found."; } public static class DecisionListenerKeys { @@ -124,4 +151,16 @@ public static class DecideOption { public static final String INCLUDE_REASONS = "includeReasons"; public static final String EXCLUDE_VARIABLES = "excludeVariables"; } + + public static class SegmentOption { + public static final String IGNORE_CACHE = "ignoreCache"; + public static final String RESET_CACHE = "resetCache"; + } + + public static class LogLevel { + public static final String ERROR = "error"; + public static final String WARNING = "warning"; + public static final String INFO = "info"; + public static final String DEBUG = "debug"; + } } 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 7a4fb5d..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 @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2022, Optimizely, Inc. and contributors * + * Copyright 2022-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import androidx.annotation.Nullable; import static com.optimizely.ab.notification.DecisionNotification.FeatureVariableDecisionNotificationBuilder.SOURCE_INFO; @@ -29,10 +30,15 @@ import com.optimizely.ab.notification.DecisionNotification; import com.optimizely.ab.notification.TrackNotification; import com.optimizely.ab.notification.UpdateConfigNotification; +import com.optimizely.ab.odp.ODPSegmentOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import org.slf4j.LoggerFactory; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; public class Utils { - + public static String sdkName = "flutter/android-sdk"; + public static String getRandomUUID() { return UUID.randomUUID().toString(); } @@ -66,6 +72,26 @@ public static List<OptimizelyDecideOption> getDecideOptions(List<String> options return convertedOptions; } + public static List<ODPSegmentOption> getSegmentOptions(List<String> options) { + if(options == null) { + return null; + } + List<ODPSegmentOption> convertedOptions = new ArrayList<>(); + for(String option: options) { + switch(option) { + case Constants.SegmentOption.IGNORE_CACHE: + convertedOptions.add(ODPSegmentOption.IGNORE_CACHE); + break; + case Constants.SegmentOption.RESET_CACHE: + convertedOptions.add(ODPSegmentOption.RESET_CACHE); + break; + default: + break; + } + } + return convertedOptions; + } + public static Class getNotificationListenerType(String notificationType) { if (notificationType == null || notificationType.isEmpty()) { return null; @@ -83,4 +109,33 @@ public static Class getNotificationListenerType(String notificationType) { } return listenerClass; } + + // SLF4J log level control: + // - logback logger (ch.qos.logback) is the only option available that supports global log level control programmatically (not only via configuration file) + // - "logback-android" logger (com.github.tony19:logback-android) is integrated in build.gradle. + // - log-level control is not integrated into the native android-sdk core since this solution depends on logback logger. + + public static void setDefaultLogLevel(@Nullable String logLevel) { + Level defaultLogLevel = Utils.mapLogLevel(logLevel); + Logger rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); + rootLogger.setLevel(defaultLogLevel); + } + + public static Level mapLogLevel(@Nullable String logLevel) { + Level level = Level.INFO; + + if (logLevel == null || logLevel.isEmpty()) { + return level; + } + + switch (logLevel) { + case Constants.LogLevel.ERROR: level = Level.ERROR; break; + case Constants.LogLevel.WARNING: level = Level.WARN; break; + case Constants.LogLevel.INFO: level = Level.INFO; break; + case Constants.LogLevel.DEBUG: level = Level.DEBUG; break; + default: {} + } + return level; + } + } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index e643787..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,12 +23,8 @@ 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 flutter.compileSdkVersion + compileSdkVersion 32 ndkVersion flutter.ndkVersion compileOptions { @@ -39,8 +37,8 @@ android { applicationId "com.optimizely.optimizely_flutter_sdk_example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion + minSdkVersion 21 + targetSdkVersion 32 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -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 fbba653..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 = "3.13.2" + 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/example/ios/Podfile b/example/ios/Podfile index 9411102..313ea4a 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '10.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/lib/main.dart b/example/lib/main.dart index 18f89e2..e7db8fa 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -32,6 +32,7 @@ class _MyAppState extends State<MyApp> { datafilePeriodicDownloadInterval: 10 * 60, eventOptions: const EventOptions( batchSize: 1, timeInterval: 60, maxQueueSize: 10000), + defaultLogLevel: OptimizelyLogLevel.debug, defaultDecideOptions: defaultOptions); var response = await flutterSDK.initializeClient(); @@ -43,7 +44,8 @@ class _MyAppState extends State<MyApp> { var randomUserName = "${rng.nextInt(1000)}"; // Create user context - var userContext = await flutterSDK.createUserContext(randomUserName); + var userContext = + await flutterSDK.createUserContext(userId: randomUserName); // Set attributes response = await userContext!.setAttributes({ diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 154f82d..9f89128 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 90B9B075FD9D5B075E83BE96 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A8DB69DD1CEB9DE6D5A4070 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -52,9 +53,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 019D04A8C81D7599D2E638FA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; }; - 33CC10ED2044A3C60003C045 /* optimizely_flutter_sdk_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "optimizely_flutter_sdk_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* optimizely_flutter_sdk_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = optimizely_flutter_sdk_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; @@ -66,7 +68,10 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; }; + 5A8DB69DD1CEB9DE6D5A4070 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; + 7EEA78DE428A25C4C9195EC6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; + 8D05B77804EBF39FD6D6D079 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -75,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 90B9B075FD9D5B075E83BE96 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +105,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + F74646C1D5F338EF32BE10E1 /* Pods */, ); sourceTree = "<group>"; }; @@ -148,10 +155,22 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 5A8DB69DD1CEB9DE6D5A4070 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = "<group>"; }; + F74646C1D5F338EF32BE10E1 /* Pods */ = { + isa = PBXGroup; + children = ( + 7EEA78DE428A25C4C9195EC6 /* Pods-Runner.debug.xcconfig */, + 8D05B77804EBF39FD6D6D079 /* Pods-Runner.release.xcconfig */, + 019D04A8C81D7599D2E638FA /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -159,11 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 834CC0346DE4E8E368DFDFD4 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + F96AA93D7120056FD027BD50 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -270,6 +291,45 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 834CC0346DE4E8E368DFDFD4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F96AA93D7120056FD027BD50 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ <FileRef location = "group:Runner.xcodeproj"> </FileRef> + <FileRef + location = "group:Pods/Pods.xcodeproj"> + </FileRef> </Workspace> diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index 22fdef9..a29370a 100644 --- a/ios/Classes/HelperClasses/Constants.swift +++ b/ios/Classes/HelperClasses/Constants.swift @@ -38,6 +38,14 @@ struct API { static let removeNotificationListener = "removeNotificationListener" static let clearNotificationListeners = "clearNotificationListeners" static let clearAllNotificationListeners = "clearAllNotificationListeners" + + // ODP + static let sendOdpEvent = "sendOdpEvent" + static let getVuid = "getVuid" + static let getQualifiedSegments = "getQualifiedSegments" + static let setQualifiedSegments = "setQualifiedSegments" + static let isQualifiedFor = "isQualifiedFor" + static let fetchQualifiedSegments = "fetchQualifiedSegments" } struct NotificationType { @@ -56,6 +64,11 @@ struct DecideOption { static let excludeVariables = "excludeVariables" } +struct SegmentOption { + static let ignoreCache = "ignoreCache" + static let resetCache = "resetCache" +} + struct RequestParameterKey { static let sdkKey = "sdkKey" static let userId = "userId" @@ -77,12 +90,32 @@ struct RequestParameterKey { static let eventTags = "eventTags" static let reasons = "reasons" static let decideOptions = "optimizelyDecideOption" + static let defaultLogLevel = "defaultLogLevel" static let eventBatchSize = "eventBatchSize" static let eventTimeInterval = "eventTimeInterval" static let eventMaxQueueSize = "eventMaxQueueSize" static let datafilePeriodicDownloadInterval = "datafilePeriodicDownloadInterval" static let datafileHostPrefix = "datafileHostPrefix" static let datafileHostSuffix = "datafileHostSuffix" + + // ODP + static let vuid = "vuid" + static let qualifiedSegments = "qualifiedSegments" + static let segment = "segment" + static let action = "action" + static let identifiers = "identifiers" + static let data = "data" + static let type = "type" + static let optimizelySegmentOption = "optimizelySegmentOption" + + static let optimizelySdkSettings = "optimizelySdkSettings" + static let segmentsCacheSize = "segmentsCacheSize" + static let segmentsCacheTimeoutInSecs = "segmentsCacheTimeoutInSecs" + static let timeoutForSegmentFetchInSecs = "timeoutForSegmentFetchInSecs" + static let timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs" + static let disableOdp = "disableOdp" + static let enableVuid = "enableVuid" + static let sdkVersion = "sdkVersion"; } struct ResponseKey { @@ -97,6 +130,7 @@ struct ErrorMessage { static let optimizelyConfigNotFound = "No optimizely config found." static let optlyClientNotFound = "Optimizely client not found." static let userContextNotFound = "User context not found." + static let qualifiedSegmentsNotFound = "Qualified Segments not found." } //Sohail: There is one issue, can we make sure the types remain same, probably we will need to write unit test separately for type. diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/Classes/HelperClasses/Utils.swift index 7062bae..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<String, Any?> else { @@ -152,6 +152,24 @@ public class Utils: NSObject { return convertedOptions } + /// Converts and returns string segment options to array of OptimizelySegmentOption + static func getSegmentOptions(options: [String]?) -> [OptimizelySegmentOption]? { + guard let finalOptions = options else { + return nil + } + var convertedOptions = [OptimizelySegmentOption]() + for option in finalOptions { + switch option { + case SegmentOption.ignoreCache: + convertedOptions.append(OptimizelySegmentOption.ignoreCache) + case SegmentOption.resetCache: + convertedOptions.append(OptimizelySegmentOption.resetCache) + default: break + } + } + return convertedOptions + } + static func convertDecisionToDictionary(decision: OptimizelyDecision?) -> [String: Any?] { let userContext: [String: Any?] = [RequestParameterKey.userId : decision?.userContext.userId, @@ -184,4 +202,17 @@ public class Utils: NSObject { return nil } } + + static func getDefaultLogLevel(_ logLevel: String) -> OptimizelyLogLevel { + var defaultLogLevel: OptimizelyLogLevel + switch logLevel { + case "error": defaultLogLevel = OptimizelyLogLevel.error + case "warning": defaultLogLevel = OptimizelyLogLevel.warning + case "info": defaultLogLevel = OptimizelyLogLevel.info + case "debug": defaultLogLevel = OptimizelyLogLevel.debug + default: defaultLogLevel = OptimizelyLogLevel.info + } + return defaultLogLevel; + } + } diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 2d1e594..7c093c4 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -45,7 +45,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { /// Part of FlutterPlugin protocol to handle communication with flutter sdk public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - + switch call.method { case API.initialize: initialize(call, result: result) case API.addNotificationListener: addNotificationListener(call, result: result) @@ -67,6 +67,14 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { case API.removeForcedDecision: removeForcedDecision(call, result: result) case API.removeAllForcedDecisions: removeAllForcedDecisions(call, result: result) case API.close: close(call, result: result) + + // ODP + case API.getQualifiedSegments: getQualifiedSegments(call, result: result) + case API.setQualifiedSegments: setQualifiedSegments(call, result: result) + case API.getVuid: getVuid(call, result: result) + case API.isQualifiedFor: isQualifiedFor(call, result: result) + case API.sendOdpEvent: sendOdpEvent(call, result: result) + case API.fetchQualifiedSegments: fetchQualifiedSegments(call, result: result) default: result(FlutterMethodNotImplemented) } } @@ -98,6 +106,43 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { decideOptions = options } let defaultDecideOptions = Utils.getDecideOptions(options: decideOptions) + + var defaultLogLevel = OptimizelyLogLevel.info + if let logLevel = parameters[RequestParameterKey.defaultLogLevel] as? String { + defaultLogLevel = Utils.getDefaultLogLevel(logLevel) + } + + // SDK Settings Default Values + var segmentsCacheSize: Int = 100 + var segmentsCacheTimeoutInSecs: Int = 600 + 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<String, Any?> { + if let cacheSize = sdkSettings[RequestParameterKey.segmentsCacheSize] as? Int { + segmentsCacheSize = cacheSize + } + if let segmentsCacheTimeout = sdkSettings[RequestParameterKey.segmentsCacheTimeoutInSecs] as? Int { + segmentsCacheTimeoutInSecs = segmentsCacheTimeout + } + if let timeoutForSegmentFetch = sdkSettings[RequestParameterKey.timeoutForSegmentFetchInSecs] as? Int { + timeoutForSegmentFetchInSecs = timeoutForSegmentFetch + } + if let timeoutForOdpEvent = sdkSettings[RequestParameterKey.timeoutForOdpEventInSecs] as? Int { + timeoutForOdpEventInSecs = timeoutForOdpEvent + } + 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, enableVuid: enableVuid, sdkName: sdkName, sdkVersion: sdkVersion) // Datafile Download Interval var datafilePeriodicDownloadInterval = 10 * 60 // seconds @@ -119,7 +164,14 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { optimizelyClientsTracker.removeValue(forKey: sdkKey) // Creating new instance - let optimizelyInstance = OptimizelyClient(sdkKey:sdkKey, eventDispatcher: eventDispatcher, datafileHandler: datafileHandler, periodicDownloadInterval: datafilePeriodicDownloadInterval, defaultDecideOptions: defaultDecideOptions) + let optimizelyInstance = OptimizelyClient( + sdkKey:sdkKey, + eventDispatcher: eventDispatcher, + datafileHandler: datafileHandler, + periodicDownloadInterval: datafilePeriodicDownloadInterval, + defaultLogLevel: defaultLogLevel, + defaultDecideOptions: defaultDecideOptions, + settings: optimizelySdkSettings) optimizelyInstance.start{ [weak self] res in switch res { @@ -198,7 +250,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { return } - + if let type = parameters[RequestParameterKey.notificationType] as? String, let convertedNotificationType = Utils.getNotificationType(type: type) { // Remove listeners only for the provided type optimizelyClient.notificationCenter?.clearNotificationListeners(type: convertedNotificationType) @@ -302,7 +354,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { let success = optimizelyClient.setForcedVariation(experimentKey: experimentKey, userId: userId, variationKey: variationKey) result(self.createResponse(success: success)) } - + /// Creates a context of the user for which decision APIs will be called. /// A user context will only be created successfully when the SDK is fully configured using initializeClient. func createUserContext(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -312,19 +364,21 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { return } - guard let userId = parameters[RequestParameterKey.userId] as? String else { - result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) - return - } let userContextId = uuid - let userContext = optimizelyClient.createUserContext(userId: userId, attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any)) + var userContext: OptimizelyUserContext! + + if let userId = parameters[RequestParameterKey.userId] as? String { + userContext = optimizelyClient.createUserContext(userId: userId, attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any)) + } else { + userContext = optimizelyClient.createUserContext(attributes: Utils.getTypedMap(arguments: parameters[RequestParameterKey.attributes] as? Any)) + } if userContextsTracker[sdkKey] != nil { userContextsTracker[sdkKey]![userContextId] = userContext } 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. @@ -359,6 +413,108 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { result(createResponse(success: true)) } + /// Returns an array of segments that the user is qualified for. + func getQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (_, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + guard let qualifiedSegments = userContext.qualifiedSegments else { + result(createResponse(success: false, reason: ErrorMessage.qualifiedSegmentsNotFound)) + return + } + result(createResponse(success: true, result: [RequestParameterKey.qualifiedSegments: qualifiedSegments])) + } + + /// Sets qualified segments for the user context. + func setQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + guard let qualifiedSegments = parameters[RequestParameterKey.qualifiedSegments] as? [String] else { + result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) + return + } + userContext.qualifiedSegments = qualifiedSegments + result(createResponse(success: true)) + } + + /// Returns the device vuid. + func getVuid(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (_, sdkKey) = getParametersAndSdkKey(arguments: call.arguments, result: result) else { + return + } + guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { + return + } + result(self.createResponse(success: optimizelyClient.vuid != nil, result: [RequestParameterKey.vuid: optimizelyClient.vuid])) + } + + /// Checks if the user is qualified for the given segment. + func isQualifiedFor(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + guard let segment = parameters[RequestParameterKey.segment] as? String else { + result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) + return + } + result(self.createResponse(success: userContext.isQualifiedFor(segment: segment))) + } + + /// Send an event to the ODP server. + func sendOdpEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, sdkKey) = getParametersAndSdkKey(arguments: call.arguments, result: result) else { + return + } + guard let optimizelyClient = getOptimizelyClient(sdkKey: sdkKey, result: result) else { + return + } + guard let action = parameters[RequestParameterKey.action] as? String, action != "" else { + result(createResponse(success: false, reason: ErrorMessage.invalidParameters)) + return + } + + var type: String? + var identifiers: [String: String] = [:] + var data: [String: Any?] = [:] + + if let _type = parameters[RequestParameterKey.type] as? String { + type = _type + } + if let _identifiers = parameters[RequestParameterKey.identifiers] as? Dictionary<String, String> { + identifiers = _identifiers + } + if let _data = Utils.getTypedMap(arguments: parameters[RequestParameterKey.data] as? Any) { + data = _data + } + + do { + try optimizelyClient.sendOdpEvent(type: type, action: action, identifiers: identifiers, data: data) + } catch { + print(error.localizedDescription) + } + result(self.createResponse(success: true)) + } + + /// Fetch all qualified segments for the user context. + func fetchQualifiedSegments(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { + return + } + var segmentOptions: [String]? + if let options = parameters[RequestParameterKey.optimizelySegmentOption] as? [String] { + segmentOptions = options + } + + let options = Utils.getSegmentOptions(options: segmentOptions) + do { + try userContext.fetchQualifiedSegments(options: options ?? []) + result(createResponse(success: true)) + } catch { + result(self.createResponse(success: false, reason: error.localizedDescription)) + } + } + /// Tracks an event. func trackEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { diff --git a/ios/optimizely_flutter_sdk.podspec b/ios/optimizely_flutter_sdk.podspec index 195d857..2aa6953 100644 --- a/ios/optimizely_flutter_sdk.podspec +++ b/ios/optimizely_flutter_sdk.podspec @@ -7,13 +7,13 @@ Pod::Spec.new do |s| s.name = 'optimizely_flutter_sdk' s.version = '0.0.1' s.summary = 'Optimizely experiment framework for iOS' - s.homepage = "https://docs.developers.optimizely.com/full-stack/docs" + s.homepage = "https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs" s.license = { :type => "Apache License, Version 2.0", :file => "../LICENSE" } s.author = { "Optimizely" => "support@optimizely.com" } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'OptimizelySwiftSDK', '3.10.1' + 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/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index d534c9d..51dc9af 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -21,10 +21,13 @@ import 'package:optimizely_flutter_sdk/src/data_objects/activate_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/event_options.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/sdk_settings.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_variation_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart'; import 'package:optimizely_flutter_sdk/src/user_context/optimizely_user_context.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; export 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart' show ClientPlatform, ListenerType; @@ -33,7 +36,7 @@ export 'package:optimizely_flutter_sdk/src/user_context/optimizely_forced_decisi export 'package:optimizely_flutter_sdk/src/user_context/optimizely_decision_context.dart' show OptimizelyDecisionContext; export 'package:optimizely_flutter_sdk/src/user_context/optimizely_user_context.dart' - show OptimizelyUserContext, OptimizelyDecideOption; + show OptimizelyUserContext, OptimizelyDecideOption, OptimizelySegmentOption; export 'package:optimizely_flutter_sdk/src/data_objects/decide_response.dart' show Decision; export 'package:optimizely_flutter_sdk/src/data_objects/track_listener_response.dart' @@ -44,8 +47,12 @@ export 'package:optimizely_flutter_sdk/src/data_objects/logevent_listener_respon show LogEventListenerResponse; export 'package:optimizely_flutter_sdk/src/data_objects/event_options.dart' show EventOptions; +export 'package:optimizely_flutter_sdk/src/data_objects/sdk_settings.dart' + show SDKSettings; export 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart' show DatafileHostOptions; +export 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart' + show OptimizelyLogLevel; /// The main client class for the Optimizely Flutter SDK. /// @@ -59,17 +66,22 @@ class OptimizelyFlutterSdk { final int _datafilePeriodicDownloadInterval; final Map<ClientPlatform, DatafileHostOptions> _datafileHostOptions; final Set<OptimizelyDecideOption> _defaultDecideOptions; - OptimizelyFlutterSdk( - this._sdkKey, { - EventOptions eventOptions = const EventOptions(), - int datafilePeriodicDownloadInterval = - 10 * 60, // Default time interval in seconds - Map<ClientPlatform, DatafileHostOptions> datafileHostOptions = const {}, - Set<OptimizelyDecideOption> defaultDecideOptions = const {}, - }) : _eventOptions = eventOptions, + final OptimizelyLogLevel _defaultLogLevel; + final SDKSettings _sdkSettings; + OptimizelyFlutterSdk(this._sdkKey, + {EventOptions eventOptions = const EventOptions(), + int datafilePeriodicDownloadInterval = + 10 * 60, // Default time interval in seconds + Map<ClientPlatform, DatafileHostOptions> datafileHostOptions = const {}, + Set<OptimizelyDecideOption> defaultDecideOptions = const {}, + OptimizelyLogLevel defaultLogLevel = OptimizelyLogLevel.info, + SDKSettings sdkSettings = const SDKSettings()}) + : _eventOptions = eventOptions, _datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval, _datafileHostOptions = datafileHostOptions, - _defaultDecideOptions = defaultDecideOptions; + _defaultDecideOptions = defaultDecideOptions, + _defaultLogLevel = defaultLogLevel, + _sdkSettings = sdkSettings; /// Starts Optimizely SDK (Synchronous) with provided sdkKey. Future<BaseResponse> initializeClient() async { @@ -78,7 +90,9 @@ class OptimizelyFlutterSdk { _eventOptions, _datafilePeriodicDownloadInterval, _datafileHostOptions, - _defaultDecideOptions); + _defaultDecideOptions, + _defaultLogLevel, + _sdkSettings); } /// Use the activate method to start an experiment. @@ -140,17 +154,40 @@ class OptimizelyFlutterSdk { return await OptimizelyClientWrapper.getOptimizelyConfig(_sdkKey); } + /// Send an event to the ODP server. + /// + /// Takes [action] The event action name. + /// Optional [type] The event type (default = "fullstack"). + /// Optional [identifiers] A dictionary for identifiers. + /// Optional [data] A dictionary for associated data. The default event data will be added to this data before sending to the ODP server. + /// Returns [BaseResponse] A object containing success result or reason of failure. + Future<BaseResponse> sendOdpEvent(String action, + {String? type, + Map<String, String> identifiers = const {}, + Map<String, dynamic> data = const {}}) async { + return await OptimizelyClientWrapper.sendOdpEvent(_sdkKey, action, + type: type, identifiers: identifiers, data: data); + } + + /// Returns the device vuid. + /// + /// Returns [GetVuidResponse] A object containing device vuid + Future<GetVuidResponse> getVuid() async { + return await OptimizelyClientWrapper.getVuid(_sdkKey); + } + /// Creates a context of the user for which decision APIs will be called. /// /// NOTE: A user context will only be created successfully when the SDK is fully configured using initializeClient. /// - /// Takes [userId] the [String] user ID to be used for bucketing. + /// Optional [userId] the [String] user ID to be used for bucketing. + /// The device vuid will be used as an userId when userId is not provided. /// Takes [attributes] An Optional [Map] of attribute names to current user attribute values. /// Returns An [OptimizelyUserContext] associated with this OptimizelyClient. - Future<OptimizelyUserContext?> createUserContext(String userId, - [Map<String, dynamic> attributes = const {}]) async { - return await OptimizelyClientWrapper.createUserContext( - _sdkKey, userId, attributes); + Future<OptimizelyUserContext?> createUserContext( + {String? userId, Map<String, dynamic> attributes = const {}}) async { + return await OptimizelyClientWrapper.createUserContext(_sdkKey, + userId: userId, attributes: attributes); } /// Allows user to remove notification listener using id. 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 1f79439..29f8cdf 100644 --- a/lib/src/data_objects/decide_response.dart +++ b/lib/src/data_objects/decide_response.dart @@ -42,14 +42,14 @@ class Decision { enabled = json[Constants.enabled]; } if (json[Constants.userContext] is Map<dynamic, dynamic>) { - Map<String, dynamic> _userContext = + Map<String, dynamic> localUserContext = Map<String, dynamic>.from(json[Constants.userContext]); - if (_userContext[Constants.userId] is String) { - userContext[Constants.userId] = _userContext[Constants.userId]; + if (localUserContext[Constants.userId] is String) { + userContext[Constants.userId] = localUserContext[Constants.userId]; } - if (_userContext[Constants.attributes] is Map<dynamic, dynamic>) { + if (localUserContext[Constants.attributes] is Map<dynamic, dynamic>) { userContext[Constants.attributes] = - Map<String, dynamic>.from(_userContext[Constants.attributes]); + Map<String, dynamic>.from(localUserContext[Constants.attributes]); } } @@ -77,6 +77,7 @@ class BaseDecideResponse extends BaseResponse { if (json[Constants.responseResult] is Map<dynamic, dynamic>) { final decisionsMap = Map<String, dynamic>.from(json[Constants.responseResult]); + // ignore: unnecessary_set_literal decisionsMap.forEach((k, v) => { if (v is Map<dynamic, dynamic>) {_decisions[k] = Decision(Map<String, dynamic>.from(v))} diff --git a/lib/src/data_objects/get_qualified_segments_response.dart b/lib/src/data_objects/get_qualified_segments_response.dart new file mode 100644 index 0000000..e687ebe --- /dev/null +++ b/lib/src/data_objects/get_qualified_segments_response.dart @@ -0,0 +1,33 @@ +/// ************************************************************************** +/// Copyright 2023, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +import 'package:optimizely_flutter_sdk/src/data_objects/base_response.dart'; +import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; + +class GetQualifiedSegmentsResponse extends BaseResponse { + List<String>? qualifiedSegments = []; + + GetQualifiedSegmentsResponse(Map<String, dynamic> json) : super(json) { + qualifiedSegments = null; + if (json[Constants.responseResult] is Map<dynamic, dynamic>) { + var response = Map<String, dynamic>.from(json[Constants.responseResult]); + if (response[Constants.qualifiedSegments] is List<dynamic>) { + qualifiedSegments = + List<String>.from(response[Constants.qualifiedSegments]); + } + } + } +} diff --git a/lib/src/data_objects/get_vuid_response.dart b/lib/src/data_objects/get_vuid_response.dart new file mode 100644 index 0000000..e5fcfb0 --- /dev/null +++ b/lib/src/data_objects/get_vuid_response.dart @@ -0,0 +1,31 @@ +/// ************************************************************************** +/// Copyright 2023, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +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; + + GetVuidResponse(Map<String, dynamic> json) : super(json) { + if (json[Constants.responseResult] is Map<dynamic, dynamic>) { + var response = Map<String, dynamic>.from(json[Constants.responseResult]); + if (response[Constants.vuid] is String) { + vuid = response[Constants.vuid]; + } + } + } +} diff --git a/lib/src/data_objects/log_level.dart b/lib/src/data_objects/log_level.dart new file mode 100644 index 0000000..c2eacd3 --- /dev/null +++ b/lib/src/data_objects/log_level.dart @@ -0,0 +1,22 @@ +/// ************************************************************************** +/// Copyright 2023, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +enum OptimizelyLogLevel { + error, + warning, + info, + debug +} diff --git a/lib/src/data_objects/optimizely_config.dart b/lib/src/data_objects/optimizely_config.dart index 955bfde..b771cc0 100644 --- a/lib/src/data_objects/optimizely_config.dart +++ b/lib/src/data_objects/optimizely_config.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -33,6 +33,7 @@ class OptimizelyConfig { if (optimizelyConfig[Constants.experimentsMap] is Map<dynamic, dynamic>) { final experimentsMapDynamic = Map<String, dynamic>.from(optimizelyConfig[Constants.experimentsMap]); + // ignore: unnecessary_set_literal experimentsMapDynamic.forEach((k, v) => { if (v is Map<dynamic, dynamic>) { @@ -45,6 +46,7 @@ class OptimizelyConfig { if (optimizelyConfig[Constants.featuresMap] is Map<dynamic, dynamic>) { final featuresMapDynamic = Map<String, dynamic>.from(optimizelyConfig[Constants.featuresMap]); + // ignore: unnecessary_set_literal featuresMapDynamic.forEach((k, v) => { if (v is Map<dynamic, dynamic>) { @@ -97,15 +99,21 @@ class OptimizelyConfig { Map<String, dynamic> toJson() { Map<String, dynamic> dynamicExpMap = {}; - experimentsMap.forEach((k, v) => {dynamicExpMap[k] = v.toJson()}); + experimentsMap.forEach((k, v) => dynamicExpMap[k] = v.toJson()); Map<String, dynamic> dynamicFeaturesMap = {}; - featuresMap.forEach((k, v) => {dynamicFeaturesMap[k] = v.toJson()}); + featuresMap.forEach((k, v) => dynamicFeaturesMap[k] = v.toJson()); var dynamicAttributes = []; - attributes.forEach((v) => {dynamicAttributes.add(v.toJson())}); + for (var attribute in attributes) { + dynamicAttributes.add(attribute.toJson()); + } var dynamicEvents = []; - events.forEach((v) => {dynamicEvents.add(v.toJson())}); + for (var event in events) { + dynamicEvents.add(event.toJson()); + } var dynamicAudiences = []; - audiences.forEach((v) => {dynamicAudiences.add(v.toJson())}); + for (var audience in audiences) { + dynamicAudiences.add(audience.toJson()); + } return { 'experimentsMap': dynamicExpMap, @@ -244,10 +252,14 @@ class OptimizelyFeature { Map<String, dynamic> toJson() { var dynamicDeliveryRules = []; - deliveryRules.forEach((v) => {dynamicDeliveryRules.add(v.toJson())}); + for (var deliveryRule in deliveryRules) { + dynamicDeliveryRules.add(deliveryRule.toJson()); + } var dynamicExperimentRules = []; - experimentRules.forEach((v) => {dynamicExperimentRules.add(v.toJson())}); + for (var experimentRule in experimentRules) { + dynamicExperimentRules.add(experimentRule.toJson()); + } return { 'id': id, 'key': key, @@ -278,6 +290,7 @@ class OptimizelyExperiment { if (parsedJson[Constants.variationsMap] is Map<dynamic, dynamic>) { final variationsMapDynamic = Map<String, dynamic>.from(parsedJson[Constants.variationsMap]); + // ignore: unnecessary_set_literal variationsMapDynamic.forEach((k, v) => { if (v is Map<dynamic, dynamic>) { @@ -295,7 +308,7 @@ class OptimizelyExperiment { Map<String, dynamic> toJson() { Map<String, dynamic> dynamicVariationsMap = {}; - variationsMap.forEach((k, v) => {dynamicVariationsMap[k] = v.toJson()}); + variationsMap.forEach((k, v) => dynamicVariationsMap[k] = v.toJson()); return { 'id': id, @@ -328,6 +341,7 @@ class OptimizelyVariation { if (parsedJson[Constants.variablesMap] is Map<dynamic, dynamic>) { final variablesMapDynamic = Map<String, dynamic>.from(parsedJson[Constants.variablesMap]); + // ignore: unnecessary_set_literal variablesMapDynamic.forEach((k, v) => { if (v is Map<dynamic, dynamic>) { @@ -345,7 +359,7 @@ class OptimizelyVariation { Map<String, dynamic> toJson() { Map<String, dynamic> dynamicVariablesMap = {}; - variablesMap.forEach((k, v) => {dynamicVariablesMap[k] = v.toJson()}); + variablesMap.forEach((k, v) => dynamicVariablesMap[k] = v.toJson()); return { 'id': id, diff --git a/lib/src/data_objects/sdk_settings.dart b/lib/src/data_objects/sdk_settings.dart new file mode 100644 index 0000000..448a2e8 --- /dev/null +++ b/lib/src/data_objects/sdk_settings.dart @@ -0,0 +1,40 @@ +/// ************************************************************************** +/// Copyright 2023, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +class SDKSettings { + // The maximum size of audience segments cache (optional. default = 100). Set to zero to disable caching. + final int segmentsCacheSize; + // The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout. + final int segmentsCacheTimeoutInSecs; + // The timeout in seconds of odp segment fetch (optional. default = 10) - OS default timeout will be used if this is set to zero. + final int timeoutForSegmentFetchInSecs; + // The timeout in seconds of odp event dispatch (optional. default = 10) - OS default timeout will be used if this is set to zero. + 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 + this.segmentsCacheTimeoutInSecs = 600, // Default segmentsCacheTimeoutInSecs + this.timeoutForSegmentFetchInSecs = + 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 0d7737d..a0869b9 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -18,10 +18,12 @@ 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'; import 'package:optimizely_flutter_sdk/src/data_objects/get_variation_response.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; @@ -59,19 +61,39 @@ class OptimizelyClientWrapper { EventOptions eventOptions, int datafilePeriodicDownloadInterval, Map<ClientPlatform, DatafileHostOptions> datafileHostOptions, - Set<OptimizelyDecideOption> defaultDecideOptions) async { + Set<OptimizelyDecideOption> defaultDecideOptions, + OptimizelyLogLevel defaultLogLevel, + SDKSettings sdkSettings) async { _channel.setMethodCallHandler(methodCallHandler); final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions); + final convertedLogLevel = Utils.convertLogLevel(defaultLogLevel); + const sdkVersion = PackageInfo.version; + Map<String, dynamic> requestDict = { Constants.sdkKey: sdkKey, + Constants.sdkVersion: sdkVersion, Constants.datafilePeriodicDownloadInterval: datafilePeriodicDownloadInterval, Constants.optimizelyDecideOption: convertedOptions, + Constants.defaultLogLevel: convertedLogLevel, Constants.eventBatchSize: eventOptions.batchSize, Constants.eventTimeInterval: eventOptions.timeInterval, Constants.eventMaxQueueSize: eventOptions.maxQueueSize, }; + // Odp Request params + Map<String, dynamic> optimizelySdkSettings = { + Constants.segmentsCacheSize: sdkSettings.segmentsCacheSize, + Constants.segmentsCacheTimeoutInSecs: + sdkSettings.segmentsCacheTimeoutInSecs, + Constants.timeoutForSegmentFetchInSecs: + sdkSettings.timeoutForSegmentFetchInSecs, + Constants.timeoutForOdpEventInSecs: sdkSettings.timeoutForOdpEventInSecs, + Constants.disableOdp: sdkSettings.disableOdp, + Constants.enableVuid: sdkSettings.enableVuid, + }; + requestDict[Constants.optimizelySdkSettings] = optimizelySdkSettings; + // clearing notification listeners, if they are mapped to the same sdkKey. activateCallbacksById.remove(sdkKey); decisionCallbacksById.remove(sdkKey); @@ -164,6 +186,35 @@ class OptimizelyClientWrapper { return OptimizelyConfigResponse(result); } + /// Send an event to the ODP server. + static Future<BaseResponse> sendOdpEvent(String sdkKey, String action, + {String? type, + Map<String, String> identifiers = const {}, + Map<String, dynamic> data = const {}}) async { + Map<String, dynamic> request = { + Constants.sdkKey: sdkKey, + Constants.action: action, + Constants.identifiers: identifiers, + Constants.data: Utils.convertToTypedMap(data) + }; + if (type != null) { + request[Constants.type] = type; + } + + final result = Map<String, dynamic>.from( + await _channel.invokeMethod(Constants.sendOdpEventMethod, request)); + return BaseResponse(result); + } + + /// Returns the device vuid (read only) + static Future<GetVuidResponse> getVuid(String sdkKey) async { + final result = Map<String, dynamic>.from( + await _channel.invokeMethod(Constants.getVuidMethod, { + Constants.sdkKey: sdkKey, + })); + return GetVuidResponse(result); + } + /// Remove notification listener by notification id. static Future<BaseResponse> removeNotificationListener( String sdkKey, int id) async { @@ -217,15 +268,17 @@ class OptimizelyClientWrapper { /// Creates a context of the user for which decision APIs will be called. /// /// A user context will only be created successfully when the SDK is fully configured using initializeClient. - static Future<OptimizelyUserContext?> createUserContext( - String sdkKey, String userId, - [Map<String, dynamic> attributes = const {}]) async { - final result = Map<String, dynamic>.from( - await _channel.invokeMethod(Constants.createUserContextMethod, { + static Future<OptimizelyUserContext?> createUserContext(String sdkKey, + {String? userId, Map<String, dynamic> attributes = const {}}) async { + Map<String, dynamic> request = { Constants.sdkKey: sdkKey, - Constants.userId: userId, Constants.attributes: Utils.convertToTypedMap(attributes) - })); + }; + if (userId != null) { + request[Constants.userId] = userId; + } + final result = Map<String, dynamic>.from(await _channel.invokeMethod( + Constants.createUserContextMethod, request)); if (result[Constants.responseSuccess] == true) { final response = @@ -318,7 +371,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } @@ -364,7 +416,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } @@ -387,7 +438,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } @@ -411,7 +461,6 @@ class OptimizelyClientWrapper { if (checkCallBackExist(sdkKey, callback)) { // ignore: avoid_print - print("callback already exists."); return -1; } diff --git a/lib/src/user_context/optimizely_user_context.dart b/lib/src/user_context/optimizely_user_context.dart index 4f261d7..906951f 100644 --- a/lib/src/user_context/optimizely_user_context.dart +++ b/lib/src/user_context/optimizely_user_context.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -21,6 +21,7 @@ import 'package:optimizely_flutter_sdk/src/data_objects/decide_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_attributes_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_forced_decision_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_user_id_response.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/get_qualified_segments_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; @@ -43,6 +44,16 @@ enum OptimizelyDecideOption { excludeVariables } +/// Options controlling audience segments. +/// +enum OptimizelySegmentOption { + /// ignore odp cache (save/lookup) + ignoreCache, + + /// resets odp cache + resetCache, +} + /// An object for user contexts that the SDK will use to make decisions for. /// class OptimizelyUserContext { @@ -86,6 +97,62 @@ class OptimizelyUserContext { return BaseResponse(result); } + /// Returns [GetQualifiedSegmentsResponse] object containing an array of segment names that the user is qualified for. + Future<GetQualifiedSegmentsResponse> getQualifiedSegments() async { + final result = Map<String, dynamic>.from( + await _channel.invokeMethod(Constants.getQualifiedSegmentsMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + })); + return GetQualifiedSegmentsResponse(result); + } + + /// Sets qualified segments for the user context. + /// + /// Takes [qualifiedSegments] A [List] of strings specifying qualified segments for the user. + /// Returns [BaseResponse] + Future<BaseResponse> setQualifiedSegments( + List<String> qualifiedSegments) async { + final result = Map<String, dynamic>.from( + await _channel.invokeMethod(Constants.setQualifiedSegmentsMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + Constants.qualifiedSegments: qualifiedSegments + })); + return BaseResponse(result); + } + + /// Checks if the user is qualified for the given segment. + /// + /// Takes [segment] The segment name to check qualification for. + /// Returns [BaseResponse] + Future<BaseResponse> isQualifiedFor(String segment) async { + final result = Map<String, dynamic>.from( + await _channel.invokeMethod(Constants.isQualifiedForMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + Constants.segment: segment + })); + return BaseResponse(result); + } + + /// Fetch all qualified segments for the user context. + /// + /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time using **getQualifiedSegments**. + /// On failure, **qualifiedSegments** will be nil and an error will be returned. + /// Optional [options] A set of [OptimizelySegmentOption] for fetching qualified segments. + /// Returns [BaseResponse] + Future<BaseResponse> fetchQualifiedSegments( + [Set<OptimizelySegmentOption> options = const {}]) async { + final result = Map<String, dynamic>.from( + await _channel.invokeMethod(Constants.fetchQualifiedSegmentsMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + Constants.optimizelySegmentOption: Utils.convertSegmentOptions(options), + })); + return BaseResponse(result); + } + /// Tracks an event. /// /// Takes [eventKey] The event name. diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 07f0f0a..2bb5421 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -46,16 +46,31 @@ class Constants { "clearNotificationListeners"; static const String clearAllNotificationListenersMethod = "clearAllNotificationListeners"; + + // Odp Supported Method Names + static const String sendOdpEventMethod = "sendOdpEvent"; + static const String getVuidMethod = "getVuid"; + static const String getQualifiedSegmentsMethod = "getQualifiedSegments"; + static const String setQualifiedSegmentsMethod = "setQualifiedSegments"; + static const String isQualifiedForMethod = "isQualifiedFor"; + static const String fetchQualifiedSegmentsMethod = "fetchQualifiedSegments"; + // 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"; static const String attributes = "attributes"; + static const String qualifiedSegments = "qualifiedSegments"; + static const String segment = "segment"; static const String decisionInfo = "decisionInfo"; static const String variables = "variables"; static const String reasons = "reasons"; @@ -69,9 +84,15 @@ class Constants { static const String ruleKey = "ruleKey"; static const String enabled = "enabled"; static const String optimizelyDecideOption = "optimizelyDecideOption"; + static const String optimizelySegmentOption = "optimizelySegmentOption"; + static const String optimizelySdkSettings = "optimizelySdkSettings"; + static const String defaultLogLevel = "defaultLogLevel"; static const String payload = "payload"; static const String value = "value"; static const String type = "type"; + static const String action = "action"; + static const String identifiers = "identifiers"; + static const String data = "data"; static const String callbackIds = "callbackIds"; static const String eventBatchSize = "eventBatchSize"; static const String eventTimeInterval = "eventTimeInterval"; @@ -105,6 +126,15 @@ class Constants { static const String variationsMap = "variationsMap"; static const String variablesMap = "variablesMap"; + // Odp Request params + static const String segmentsCacheSize = "segmentsCacheSize"; + static const String segmentsCacheTimeoutInSecs = "segmentsCacheTimeoutInSecs"; + static const String timeoutForSegmentFetchInSecs = + "timeoutForSegmentFetchInSecs"; + static const String timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs"; + static const String disableOdp = "disableOdp"; + static const String enableVuid = "enableVuid"; + // Response keys static const String responseSuccess = "success"; static const String responseResult = "result"; diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index 58344ae..8b18b13 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -15,9 +15,9 @@ ///**************************************************************************/ 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'; class Utils { static Map<OptimizelyDecideOption, String> decideOptions = { @@ -28,6 +28,11 @@ class Utils { OptimizelyDecideOption.excludeVariables: "excludeVariables", }; + static Map<OptimizelySegmentOption, String> segmentOptions = { + OptimizelySegmentOption.ignoreCache: "ignoreCache", + OptimizelySegmentOption.resetCache: "resetCache", + }; + static Map<String, dynamic> convertToTypedMap(Map<String, dynamic> map) { if (map.isEmpty) { return map; @@ -84,4 +89,16 @@ class Utils { Set<OptimizelyDecideOption> options) { return options.map((option) => Utils.decideOptions[option]!).toList(); } + + static List<String> convertSegmentOptions( + Set<OptimizelySegmentOption> options) { + return options.map((option) => Utils.segmentOptions[option]!).toList(); + } + + static String convertLogLevel(OptimizelyLogLevel logLevel) { + // OptimizelyLogLevel.error -> "error" + // OptimizelyLogLevel.debug -> "debug" + return logLevel.toString().split('.').last; + } + } diff --git a/pubspec.yaml b/pubspec.yaml index ac0b6f6..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 Full Stack and Optimizely Rollouts. -version: 1.0.0-beta +description: This repository houses the Flutter SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts. +version: 3.0.1 homepage: https://github.com/optimizely/optimizely-flutter-sdk environment: - sdk: ">=2.16.2 <3.0.0" + sdk: '>=2.16.2 <4.0.0' flutter: ">=2.5.0" dependencies: @@ -16,7 +16,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - flutter_lints: ^1.0.4 + flutter_lints: ^2.0.1 collection: ^1.16.0 flutter: diff --git a/test/optimizely_flutter_sdk_test.dart b/test/optimizely_flutter_sdk_test.dart index 104355a..862c4b0 100644 --- a/test/optimizely_flutter_sdk_test.dart +++ b/test/optimizely_flutter_sdk_test.dart @@ -1,5 +1,5 @@ /// ************************************************************************** -/// Copyright 2022, Optimizely, Inc. and contributors * +/// Copyright 2022-2023, Optimizely, Inc. and contributors * /// * /// Licensed under the Apache License, Version 2.0 (the "License"); * /// you may not use this file except in compliance with the License. * @@ -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() { @@ -35,17 +34,28 @@ void main() { const String ruleKey = "rule_1"; const String variationKey = "var_1"; const String eventKey = "event-key"; + const String segment = "segment"; + const String action = "action1"; + const String type = "type1"; + const String vuid = "vuid_123"; + const Map<String, String> identifiers = {"abc": "123"}; + const Map<String, dynamic> data = {"abc": 12345}; const Map<String, dynamic> attributes = {"abc": 123}; const Map<String, dynamic> attributes1 = {"abc": 1234}; const Map<String, dynamic> eventTags = {"abcd": 1234}; + const List<String> qualifiedSegments = ["1", "2", "3"]; + const String userContextId = "123"; // To check if decide options properly reached the native sdk through channel List<String> decideOptions = []; - // To check if event options and datafileOptions reached the native sdk through channel + // To check if event options, datafileOptions and sdkSettings reached the native sdk through channel EventOptions eventOptions = const EventOptions(); + // To check if segment options properly reached the native sdk through channel + List<String> segmentOptions = []; DatafileHostOptions datafileHostOptions = const DatafileHostOptions("", ""); + SDKSettings sdkSettings = const SDKSettings(); int datafilePeriodicDownloadInterval = 0; - + String defaultLogLevel = "error"; const MethodChannel channel = MethodChannel("optimizely_flutter_sdk"); dynamic mockOptimizelyConfig; @@ -72,6 +82,9 @@ void main() { case Constants.initializeMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); expect(methodCall.arguments[Constants.userContextId], isNull); + + defaultLogLevel = methodCall.arguments[Constants.defaultLogLevel]; + // To Check if eventOptions were received eventOptions = EventOptions( batchSize: methodCall.arguments[Constants.eventBatchSize], @@ -80,6 +93,22 @@ void main() { datafilePeriodicDownloadInterval = methodCall.arguments[Constants.datafilePeriodicDownloadInterval]; + // To Check if sdkSettings were received + var settings = methodCall.arguments[Constants.optimizelySdkSettings]; + if (settings is Map) { + sdkSettings = SDKSettings( + segmentsCacheSize: settings[Constants.segmentsCacheSize], + segmentsCacheTimeoutInSecs: + settings[Constants.segmentsCacheTimeoutInSecs], + timeoutForSegmentFetchInSecs: + settings[Constants.timeoutForSegmentFetchInSecs], + timeoutForOdpEventInSecs: + settings[Constants.timeoutForOdpEventInSecs], + disableOdp: settings[Constants.disableOdp], + enableVuid: settings[Constants.enableVuid], + ); + } + // Resetting to default for every test datafileHostOptions = const DatafileHostOptions("", ""); if (methodCall.arguments[Constants.datafileHostPrefix] != null && @@ -146,13 +175,27 @@ void main() { }; case Constants.createUserContextMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); - expect(methodCall.arguments[Constants.userId], equals(userId)); - expect(methodCall.arguments[Constants.attributes]["abc"], - equals(attributes["abc"])); + 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"])); + } 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); @@ -183,6 +226,62 @@ void main() { return { Constants.responseSuccess: true, }; + case Constants.getQualifiedSegmentsMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + return { + Constants.responseSuccess: true, + Constants.responseResult: { + Constants.qualifiedSegments: qualifiedSegments, + }, + }; + case Constants.setQualifiedSegmentsMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + expect(methodCall.arguments[Constants.qualifiedSegments], + equals(qualifiedSegments)); + return { + Constants.responseSuccess: true, + }; + case Constants.fetchQualifiedSegmentsMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + segmentOptions.addAll(List<String>.from( + methodCall.arguments[Constants.optimizelySegmentOption])); + return { + Constants.responseSuccess: true, + }; + case Constants.isQualifiedForMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + expect(methodCall.arguments[Constants.segment], equals(segment)); + return { + Constants.responseSuccess: true, + }; + case Constants.sendOdpEventMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], isNull); + expect(methodCall.arguments[Constants.action], equals(action)); + expect(methodCall.arguments[Constants.type], equals(type)); + expect( + methodCall.arguments[Constants.identifiers], equals(identifiers)); + expect(methodCall.arguments[Constants.data], equals(data)); + return { + Constants.responseSuccess: true, + }; + 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}, + }; case Constants.trackEventMethod: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); expect(methodCall.arguments[Constants.userContextId], @@ -289,6 +388,7 @@ void main() { tearDown(() { tester?.setMockMethodCallHandler(channel, null); + sdkSettings = const SDKSettings(); }); group("Integration: OptimizelyFlutterSdk MethodChannel", () { @@ -311,12 +411,19 @@ void main() { expect(response.success, isTrue); }); - test("with no eventOptions and no datafileOptions", () async { + test("with no eventOptions, datafileOptions and sdkSettings", () async { // default values const expectedEventOptions = EventOptions(batchSize: 10, timeInterval: 60, maxQueueSize: 10000); debugDefaultTargetPlatformOverride = TargetPlatform.iOS; const expectedDatafileHostOptions = DatafileHostOptions("", ""); + const expectedSDKSettings = SDKSettings( + segmentsCacheSize: 100, + segmentsCacheTimeoutInSecs: 600, + timeoutForSegmentFetchInSecs: 10, + timeoutForOdpEventInSecs: 10, + disableOdp: false, + ); const expectedDatafilePeriodicDownloadInterval = 10 * 60; var sdk = OptimizelyFlutterSdk(testSDKKey); var response = await sdk.initializeClient(); @@ -333,22 +440,40 @@ void main() { equals(expectedDatafileHostOptions.datafileHostPrefix)); expect(datafileHostOptions.datafileHostSuffix, equals(expectedDatafileHostOptions.datafileHostSuffix)); + + expect(sdkSettings.segmentsCacheSize, + equals(expectedSDKSettings.segmentsCacheSize)); + expect(sdkSettings.segmentsCacheTimeoutInSecs, + equals(expectedSDKSettings.segmentsCacheTimeoutInSecs)); + expect(sdkSettings.timeoutForSegmentFetchInSecs, + equals(expectedSDKSettings.timeoutForSegmentFetchInSecs)); + expect(sdkSettings.timeoutForOdpEventInSecs, + equals(expectedSDKSettings.timeoutForOdpEventInSecs)); + expect(sdkSettings.disableOdp, equals(expectedSDKSettings.disableOdp)); debugDefaultTargetPlatformOverride = null; }); - test("with eventOptions and datafileOptions", () async { + test("with eventOptions, datafileOptions and sdkSettings", () async { const expectedEventOptions = EventOptions(batchSize: 20, timeInterval: 30, maxQueueSize: 200); debugDefaultTargetPlatformOverride = TargetPlatform.iOS; const expectedDatafileHostOptions = DatafileHostOptions("123", "456"); const expectedDatafilePeriodicDownloadInterval = 40; + const expectedSDKSettings = SDKSettings( + segmentsCacheSize: 111, + segmentsCacheTimeoutInSecs: 222, + timeoutForSegmentFetchInSecs: 333, + timeoutForOdpEventInSecs: 444, + disableOdp: true, + ); var sdk = OptimizelyFlutterSdk(testSDKKey, eventOptions: expectedEventOptions, datafilePeriodicDownloadInterval: expectedDatafilePeriodicDownloadInterval, datafileHostOptions: { ClientPlatform.iOS: expectedDatafileHostOptions - }); + }, + sdkSettings: expectedSDKSettings); var response = await sdk.initializeClient(); expect(response.success, isTrue); @@ -363,6 +488,16 @@ void main() { equals(expectedDatafileHostOptions.datafileHostPrefix)); expect(datafileHostOptions.datafileHostSuffix, equals(expectedDatafileHostOptions.datafileHostSuffix)); + + expect(sdkSettings.segmentsCacheSize, + equals(expectedSDKSettings.segmentsCacheSize)); + expect(sdkSettings.segmentsCacheTimeoutInSecs, + equals(expectedSDKSettings.segmentsCacheTimeoutInSecs)); + expect(sdkSettings.timeoutForSegmentFetchInSecs, + equals(expectedSDKSettings.timeoutForSegmentFetchInSecs)); + expect(sdkSettings.timeoutForOdpEventInSecs, + equals(expectedSDKSettings.timeoutForOdpEventInSecs)); + expect(sdkSettings.disableOdp, equals(expectedSDKSettings.disableOdp)); debugDefaultTargetPlatformOverride = null; }); @@ -423,6 +558,34 @@ void main() { }); }); + group("log level configuration", () { + test("with no defaultLogLevel, log level should be info level", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + + var response = await sdk.initializeClient(); + + expect(response.success, isTrue); + expect(defaultLogLevel, equals("info")); + }); + + test("with a valid defaultLogLevel parameter", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey, + defaultLogLevel: OptimizelyLogLevel.debug); + + var response = await sdk.initializeClient(); + + expect(response.success, isTrue); + expect(defaultLogLevel, equals("debug")); + }); + + test("should convert OptimizelyLogLevel to string", () async { + expect(Utils.convertLogLevel(OptimizelyLogLevel.error), "error"); + expect(Utils.convertLogLevel(OptimizelyLogLevel.warning), "warning"); + expect(Utils.convertLogLevel(OptimizelyLogLevel.info), "info"); + expect(Utils.convertLogLevel(OptimizelyLogLevel.debug), "debug"); + }); + }); + group("close()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); @@ -495,15 +658,45 @@ void main() { group("createUserContext()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); + expect(userContext, isNotNull); + }); + + 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); }); + + test("should succeed null attributes", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + expect(userContext, isNotNull); + }); + + test("should not succeed null userId and attributes", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + sdk.initializeClient(); + var userContext = await sdk.createUserContext(); + expect(userContext, isNull); + }); }); group("getUserId()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.getUserId(); expect(response.success, isTrue); @@ -513,7 +706,8 @@ void main() { group("getAttributes()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.getAttributes(); expect(response.success, isTrue); @@ -524,17 +718,102 @@ void main() { group("setAttributes()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.setAttributes(attributes1); expect(response.success, isTrue); }); }); + group("getQualifiedSegments()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + var response = await userContext!.getQualifiedSegments(); + + expect(response.qualifiedSegments, qualifiedSegments); + }); + }); + + group("setQualifiedSegments()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + var response = + await userContext!.setQualifiedSegments(qualifiedSegments); + + expect(response.success, isTrue); + }); + }); + + group("isQualifiedFor()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + var response = await userContext!.isQualifiedFor(segment); + + expect(response.success, isTrue); + }); + }); + + group("fetchQualifiedSegments()", () { + bool assertSegmentOptions( + Set<OptimizelySegmentOption> options, List<String> convertedOptions) { + for (var option in options) { + if (!convertedOptions.contains(option.name)) { + return false; + } + } + return true; + } + + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var userContext = await sdk.createUserContext(userId: userId); + Set<OptimizelySegmentOption> options = { + OptimizelySegmentOption.ignoreCache, + OptimizelySegmentOption.resetCache, + }; + var response = await userContext!.fetchQualifiedSegments(options); + expect(response.success, isTrue); + expect(segmentOptions.length == 2, isTrue); + expect(assertSegmentOptions(options, segmentOptions), isTrue); + }); + }); + + group("sendOdpEvent()", () { + test("should succeed", () async { + var sdk = OptimizelyFlutterSdk(testSDKKey); + var response = await sdk.sendOdpEvent(action, + type: type, identifiers: identifiers, data: data); + expect(response.success, isTrue); + }); + }); + + group("getVuid()", () { + 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, 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"); + }); + }); + group("trackEvent()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.trackEvent(eventKey, eventTags); expect(response.success, isTrue); @@ -569,7 +848,8 @@ void main() { }; var sdk = OptimizelyFlutterSdk(testSDKKey, defaultDecideOptions: defaultDecideOptions); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var decideKey = "decide-key"; var response = await userContext!.decide(decideKey, options); @@ -588,7 +868,8 @@ void main() { test("decideForKeys should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey, defaultDecideOptions: defaultDecideOptions); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var decideKeys = ["decide-key-1", "decide-key-2"]; var response = await userContext!.decideForKeys(decideKeys, options); @@ -604,7 +885,8 @@ void main() { test("decideAll() should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey, defaultDecideOptions: defaultDecideOptions); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.decideAll(options); @@ -636,7 +918,8 @@ void main() { group("setForcedDecision()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.setForcedDecision( OptimizelyDecisionContext(flagKey, ruleKey), @@ -649,7 +932,8 @@ void main() { group("getForcedDecision()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext! .getForcedDecision(OptimizelyDecisionContext(flagKey, ruleKey)); @@ -662,7 +946,8 @@ void main() { group("removeForcedDecision()", () { test("should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext! .removeForcedDecision(OptimizelyDecisionContext(flagKey, ruleKey)); @@ -674,7 +959,8 @@ void main() { test("removeAllForcedDecisions() should succeed", () async { var sdk = OptimizelyFlutterSdk(testSDKKey); - var userContext = await sdk.createUserContext(userId, attributes); + var userContext = + await sdk.createUserContext(userId: userId, attributes: attributes); var response = await userContext!.removeAllForcedDecisions(); @@ -1299,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: <String, Object>{ + 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;