From 50533a9b1142aeb529b554cdaa2b4a5008c7242d Mon Sep 17 00:00:00 2001
From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com>
Date: Tue, 16 Jan 2024 17:20:39 -0500
Subject: [PATCH 1/9] [FSSDK-8581] chore: Prepare for 4.0.0 release (#371)
* chore: Update AssemblyInformationalVersion
removed beta tag for NuGet
* docs: Update changelog
* docs: Test whether I have to manually link PRs
* docs: Add PR links
wwwhyy is this still something I have to do??? *coughs: AI*
* docs: Update version listed on README
This file needs to be reviewed & updates
* docs: Update version listed on README
This file needs to be reviewed & updates
* docs: Update CHANGELOG with 4.0-GA info
bring pre-release beta notes up to the GA version section
* docs: Update LICENSE copyright line
* docs: Reorder CHANGELOG entries
---
CHANGELOG.md | 70 +++++++++++++++++++
LICENSE | 2 +-
.../Properties/AssemblyInfo.cs | 2 +-
.../Properties/AssemblyInfo.cs | 2 +-
.../Properties/AssemblyInfo.cs | 2 +-
.../Properties/AssemblyInfo.cs | 2 +-
.../Properties/AssemblyInfo.cs | 2 +-
.../Properties/AssemblyInfo.cs | 2 +-
OptimizelySDK/Properties/AssemblyInfo.cs | 2 +-
README.md | 4 +-
10 files changed, 80 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b9620be0..0b431aa14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,75 @@
# Optimizely C# SDK Changelog
+## 4.0.0
+January 16th, 2024
+
+### New Features
+
+#### Advanced Audience Targeting
+
+The 4.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) (
+ [#305](https://github.com/optimizely/csharp-sdk/pull/305),
+ [#310](https://github.com/optimizely/csharp-sdk/pull/310),
+ [#311](https://github.com/optimizely/csharp-sdk/pull/311),
+ [#315](https://github.com/optimizely/csharp-sdk/pull/315),
+ [#321](https://github.com/optimizely/csharp-sdk/pull/321),
+ [#322](https://github.com/optimizely/csharp-sdk/pull/322),
+ [#323](https://github.com/optimizely/csharp-sdk/pull/323),
+ [#324](https://github.com/optimizely/csharp-sdk/pull/324)
+ ).
+
+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 Optimizely 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 `OptimizelyClient`:
+ - `SendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP.
+
+For details, refer to our documentation pages:
+- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+- [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-server-side-sdks)
+- [Initialize C# SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-csharp)
+- [OptimizelyUserContext C# SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-csharp)
+- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-csharp)
+- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-csharp)
+
+#### Polling warning
+
+Add warning to polling intervals below 30 seconds ([#365](https://github.com/optimizely/csharp-sdk/pull/365))
+
+### Breaking Changes
+- `OdpManager` in the SDK is enabled by default. Unless an ODP account is integrated into the Optimizely projects, most `OdpManager` functions will be ignored. If needed, `OdpManager` can be disabled when `OptimizelyClient` is instantiated.
+- `ProjectConfigManager` interface additions + implementing class updates
+- `Evaluate()` updates in `BaseCondition`
+
+### Bug Fixes
+- Return Latest Experiment When Duplicate Keys in Config enhancement
+
+### Documentation
+- Corrections to markdown files in docs directory ([#368](https://github.com/optimizely/csharp-sdk/pull/368))
+- GitHub template updates ([#366](https://github.com/optimizely/csharp-sdk/pull/366))
+
+## 3.11.4
+July 26th, 2023
+
+### Bug Fixes
+- Fix Last-Modified date & time format for If-Modified-Since ([#361](https://github.com/optimizely/csharp-sdk/pull/361))
+
+## 3.11.3
+July 18th, 2023
+
+### Bug Fixes
+- Last-Modified in header not found and used to reduce polling payload ([#355](https://github.com/optimizely/csharp-sdk/pull/355)).
+
## 4.0.0-beta
April 28th, 2023
diff --git a/LICENSE b/LICENSE
index 089978b9d..2f8be7813 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2016 Optimizely
+ Copyright 2016-2024, 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.
diff --git a/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs b/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs
index ef42316d9..5fe786119 100644
--- a/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs
@@ -39,4 +39,4 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.0.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0-beta")] // Used by Nuget.
+[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
diff --git a/OptimizelySDK.Net35/Properties/AssemblyInfo.cs b/OptimizelySDK.Net35/Properties/AssemblyInfo.cs
index 69b04e7f4..9f7dfbe4f 100644
--- a/OptimizelySDK.Net35/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.Net35/Properties/AssemblyInfo.cs
@@ -39,4 +39,4 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.0.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0-beta")] // Used by Nuget.
+[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
diff --git a/OptimizelySDK.Net40/Properties/AssemblyInfo.cs b/OptimizelySDK.Net40/Properties/AssemblyInfo.cs
index c3e01c002..815b273a5 100644
--- a/OptimizelySDK.Net40/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.Net40/Properties/AssemblyInfo.cs
@@ -39,4 +39,4 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.0.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0-beta")] // Used by Nuget.
+[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
diff --git a/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs b/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs
index 7ba7db50d..16f95976c 100644
--- a/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs
@@ -39,4 +39,4 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0-beta")] // Used by Nuget.
+[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
diff --git a/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs b/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs
index e493466c5..6e2fbdc3f 100644
--- a/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs
@@ -39,4 +39,4 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.0.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0-beta")] // Used by Nuget.
+[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
diff --git a/OptimizelySDK.Tests/Properties/AssemblyInfo.cs b/OptimizelySDK.Tests/Properties/AssemblyInfo.cs
index 8ceae6071..ea9d640ed 100644
--- a/OptimizelySDK.Tests/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.Tests/Properties/AssemblyInfo.cs
@@ -32,4 +32,4 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.0.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0-beta")] // Used by Nuget.
+[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
diff --git a/OptimizelySDK/Properties/AssemblyInfo.cs b/OptimizelySDK/Properties/AssemblyInfo.cs
index 07e29d0ab..d801dfaa9 100644
--- a/OptimizelySDK/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK/Properties/AssemblyInfo.cs
@@ -43,4 +43,4 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.0.0.0")]
[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0-beta")] // Used by Nuget.
+[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
diff --git a/README.md b/README.md
index eb0da22ce..ff2148fb9 100644
--- a/README.md
+++ b/README.md
@@ -104,7 +104,7 @@ User can provide variables using following procedure:
```
+ type="OptimizelySDK.OptimizelySDKConfigSection, OptimizelySDK, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" />
```
2. Now add **optlySDKConfigSection** below ****. In this section you can add and set following **HttpProjectConfigManager** and **BatchEventProcessor** variables:
@@ -251,4 +251,4 @@ Optimizely SDK uses third party software:
- Ruby - https://github.com/optimizely/ruby-sdk
-- Swift - https://github.com/optimizely/swift-sdk
\ No newline at end of file
+- Swift - https://github.com/optimizely/swift-sdk
From c37a74daa043a8eb8789b0f86d934bd584597185 Mon Sep 17 00:00:00 2001
From: Farhan Anjum
Date: Tue, 24 Sep 2024 15:13:48 +0600
Subject: [PATCH 2/9] [FSSDK-10665] fix: Github Actions YAML files vulnerable
to script injections corrected (#372)
* github actions yaml files vulnerable to script injections corrected
* build: update NUnit.Console to 3.18.2
CI error similar to https://github.com/nunit/nunit3-vs-adapter/issues/1049
* chore: downgrade to NUnit.Console 3.18.1
v3.18.2 introduced an incompatibility
---------
Co-authored-by: Mike Chu
---
.github/workflows/csharp.yml | 4 ++--
.github/workflows/csharp_release.yml | 7 +++----
.github/workflows/integration_test.yml | 12 ++++++++----
3 files changed, 13 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml
index f417b0e87..4303e0e29 100644
--- a/.github/workflows/csharp.yml
+++ b/.github/workflows/csharp.yml
@@ -48,9 +48,9 @@ jobs:
run: msbuild /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk /p:Configuration=Release ./OptimizelySDK.NETFramework.sln
- name: Install & Run NUnit tests
run: |
- nuget install NUnit.Console -Version 3.15.2 -DirectDownload -OutputDirectory .
+ nuget install NUnit.Console -Version 3.18.1 -DirectDownload -OutputDirectory .
# https://docs.nunit.org/articles/nunit/running-tests/Console-Command-Line.html
- ./NUnit.ConsoleRunner.3.15.2\tools\nunit3-console.exe /timeout 10000 /process Separate ./OptimizelySDK.Tests/bin/Release/OptimizelySDK.Tests.dll
+ ./NUnit.ConsoleRunner.3.18.1\tools\nunit3-console.exe /timeout 10000 /process Separate ./OptimizelySDK.Tests/bin/Release/OptimizelySDK.Tests.dll
netStandard16:
name: Build Standard 1.6
diff --git a/.github/workflows/csharp_release.yml b/.github/workflows/csharp_release.yml
index 90e680a76..8f989fc64 100644
--- a/.github/workflows/csharp_release.yml
+++ b/.github/workflows/csharp_release.yml
@@ -15,8 +15,7 @@ jobs:
- name: Set semantic version variable
id: set_version
run: |
- TAG=${{ env.TAG }}
- SEMANTIC_VERSION=$(echo "${TAG}" | grep -Po "(?<=^|[^0-9])([0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z]+[0-9]*)?)")
+ SEMANTIC_VERSION=$(echo "$TAG" | grep -Po "(?<=^|[^0-9])([0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z]+[0-9]*)?)")
if [ -z "${SEMANTIC_VERSION}" ]; then
echo "Tag did not start with a semantic version number (e.g., #.#.#; #.#.#.#; #.#.#.#-beta)"
exit 1
@@ -25,10 +24,10 @@ jobs:
- name: Output tag & semantic version
id: outputs
run: |
- echo ${{ env.TAG }}
+ echo "$TAG"
echo ${{ steps.set_version.outputs.semantic_version }}
outputs:
- tag: ${{ env.TAG }}
+ tag: $TAG
semanticVersion: ${{ steps.set_version.outputs.semantic_version }}
buildFrameworkVersions:
diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml
index 423e2dfed..cf9a96b37 100644
--- a/.github/workflows/integration_test.yml
+++ b/.github/workflows/integration_test.yml
@@ -23,15 +23,19 @@ 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 "TRAVIS_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
+ echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV
+ echo "TRAVIS_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: csharp
From 50624fb468a31465bf875ce1fe2bcb1409af0ec9 Mon Sep 17 00:00:00 2001
From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com>
Date: Wed, 6 Nov 2024 14:28:14 -0500
Subject: [PATCH 3/9] [FSSDK-10265] fix: UPS `Lookup` & `Save` during batched
Decide (#375)
* ci: fail if triggered from Draft PR
* test: cover new cases
* feat: WIP
* feat: WIP implementing solution. Tests are
still in progress too.
* doc: add comment block
* chore: remove commented code
* test: fix via code under test
* fix: match updates to java PR
* fix: code from test results
* test: correct test
* tests: updated
* refactor: rename to match Java ref imp
* refactor: extract UserProfileTracker
* test: fixes
* fix: tests & code under tests
* ci: also kick off CI if a PR turns to ready for review
* ci: try to ready for review trigger
* ci: for now just manually kick it off
* ci: remove Fail If Draft Pull Request
* fix: lints & copyright dates
* ci: bump action versions
* chore: csproj auto-edit
* fix: super-linter version
* fix: add missing refs to UserProfileTracker.cs
* revert: accidental csproj references
* ci: lots of updates to how we're releasing
* fix: lint & dir name
* revert: modifiers on DecisionService & test changes
---
.github/workflows/csharp.yml | 14 +-
.github/workflows/csharp_release.yml | 143 ++++----
.../OptimizelySDK.Net35.csproj | 5 +-
.../OptimizelySDK.Net40.csproj | 5 +-
.../OptimizelySDK.NetStandard16.csproj | 1 +
.../OptimizelySDK.NetStandard20.csproj | 3 +
OptimizelySDK.Tests/DecisionServiceTest.cs | 49 ++-
.../OptimizelyUserContextTest.cs | 169 ++++++++-
OptimizelySDK.sln.DotSettings | 5 +
OptimizelySDK/Bucketing/DecisionService.cs | 308 +++++++++-------
OptimizelySDK/Bucketing/UserProfileTracker.cs | 108 ++++++
OptimizelySDK/Optimizely.cs | 337 ++++++++++--------
OptimizelySDK/OptimizelySDK.csproj | 1 +
13 files changed, 785 insertions(+), 363 deletions(-)
create mode 100644 OptimizelySDK/Bucketing/UserProfileTracker.cs
diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml
index 4303e0e29..b55407af7 100644
--- a/.github/workflows/csharp.yml
+++ b/.github/workflows/csharp.yml
@@ -5,19 +5,21 @@ on:
branches: [ master ]
pull_request:
branches: [ master ]
+ types: [ opened, synchronize, reopened, ready_for_review ]
jobs:
lintCodebase:
+ name: Lint Codebase if Not Draft
+ if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
- name: Lint Codebase
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
# Full git history is needed to get a proper list of changed files
fetch-depth: 0
- name: Run Super-Linter
- uses: github/super-linter@v4
+ uses: github/super-linter@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_ALL_CODEBASE: false
@@ -37,7 +39,7 @@ jobs:
CURRENT_BRANCH: ${{ github.head_ref || github.ref_name }}
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1
- name: Setup NuGet
@@ -65,7 +67,7 @@ jobs:
CURRENT_BRANCH: ${{ github.head_ref || github.ref_name }}
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
@@ -88,7 +90,7 @@ jobs:
CURRENT_BRANCH: ${{ github.head_ref || github.ref_name }}
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
diff --git a/.github/workflows/csharp_release.yml b/.github/workflows/csharp_release.yml
index 8f989fc64..cd80b0b20 100644
--- a/.github/workflows/csharp_release.yml
+++ b/.github/workflows/csharp_release.yml
@@ -9,23 +9,20 @@ jobs:
name: Set Variables
runs-on: ubuntu-latest
env:
- # ⚠️ IMPORTANT: tag should always start with integer & will be used verbatim to string end
TAG: ${{ github.event.release.tag_name }}
steps:
- - name: Set semantic version variable
+ - name: Extract semantic version from tag
id: set_version
run: |
- SEMANTIC_VERSION=$(echo "$TAG" | grep -Po "(?<=^|[^0-9])([0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z]+[0-9]*)?)")
+ # Remove the "v" prefix if it exists and extract the semantic version number
+ SEMANTIC_VERSION=$(echo "${TAG}" | grep -Po "(?<=^|[^0-9])([0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z]+[0-9]*)?)")
+ SEMANTIC_VERSION=${SEMANTIC_VERSION#"v"}
if [ -z "${SEMANTIC_VERSION}" ]; then
- echo "Tag did not start with a semantic version number (e.g., #.#.#; #.#.#.#; #.#.#.#-beta)"
+ echo "Error: Tag '${TAG}' does not start with a valid semantic version number (e.g., #.#.#; #.#.#.#; #.#.#.#-beta)"
exit 1
fi
+ echo "Extracted semantic version: ${SEMANTIC_VERSION}"
echo "semantic_version=${SEMANTIC_VERSION}" >> $GITHUB_OUTPUT
- - name: Output tag & semantic version
- id: outputs
- run: |
- echo "$TAG"
- echo ${{ steps.set_version.outputs.semantic_version }}
outputs:
tag: $TAG
semanticVersion: ${{ steps.set_version.outputs.semantic_version }}
@@ -48,9 +45,9 @@ jobs:
- name: Build and strongly name assemblies
run: msbuild /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk /p:Configuration=Release ./OptimizelySDK.NETFramework.sln
- name: Upload Framework artifacts
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
- name: nuget-files
+ name: unsigned-dlls
if-no-files-found: error
path: ./**/bin/Release/**/Optimizely*.dll
@@ -70,9 +67,9 @@ jobs:
- name: Build and strongly name assemblies
run: dotnet build OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk -c Release
- name: Upload Standard 1.6 artifact
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
- name: nuget-files
+ name: unsigned-dlls
if-no-files-found: error
path: ./**/bin/Release/**/Optimizely*.dll
@@ -92,21 +89,69 @@ jobs:
- name: Build and strongly name Standard 2.0 project
run: dotnet build OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk -c Release
- name: Build and strongly name assemblies
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
- name: nuget-files
+ name: unsigned-dlls
if-no-files-found: error
path: ./**/bin/Release/**/Optimizely*.dll
- pack:
- name: Sign & pack NuGet package
+ sign:
+ name: Send DLLs for signing
needs: [ variables, buildFrameworkVersions, buildStandard16, buildStandard20 ]
runs-on: ubuntu-latest
+ env:
+ # TODO: Replace actual values
+ SIGNING_SERVER_PRIVATE_KEY: ${{ secrets.SIGNING_SERVER_PRIVATE_KEY }}
+ SIGNING_SERVER_HOST: ${{ secrets.SIGNING_SERVER_HOST }}
+ SIGNING_SERVER_UPLOAD_PATH: /path/to/UPLOAD/directory
+ SIGNING_SERVER_DOWNLOAD_PATH: /path/to/DOWNLOAD/directory
+ steps:
+ # TODO: Remove this when we're ready to automate
+ - name: Temporarily halt progress
+ run: exit 1
+ - name: Download the unsigned files
+ uses: actions/download-artifact@v4
+ with:
+ name: unsigned-dlls
+ path: ./unsigned-dlls
+ - name: Setup SSH
+ uses: shimataro/ssh-key-action@v2
+ with:
+ key: $SIGNING_SERVER_PRIVATE_KEY
+ - name: Send files to signing server
+ run: scp -r ./unsigned-dlls $SIGNING_SERVER_HOST:$SIGNING_SERVER_UPLOAD_PATH
+ - name: Wait for artifact to be published
+ run: |
+ for i in {1..60}; do
+ # Replace with actual path
+ if ssh $SIGNING_SERVER_HOST "ls $SIGNING_SERVER_DOWNLOAD_PATH"; then
+ exit 0
+ fi
+ sleep 10
+ done
+ exit 1
+ - name: Download signed files
+ run: |
+ mkdir ./signed-dlls
+ scp -r $SIGNING_SERVER_HOST:$SIGNING_SERVER_DOWNLOAD_PATH ./signed-dlls
+ - name: Delete signed files from server
+ run: ssh $SIGNING_SERVER_HOST "rm -rf $SIGNING_SERVER_DOWNLOAD_PATH/*"
+ - name: Upload signed files
+ uses: actions/upload-artifact@v4
+ with:
+ name: signed-dlls
+ if-no-files-found: error
+ path: ./signed-dlls
+
+ pack:
+ name: Pack NuGet package
+ needs: [ variables, sign ]
+ runs-on: ubuntu-latest
env:
VERSION: ${{ needs.variables.outputs.semanticVersion }}
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: ${{ needs.variables.outputs.tag }}
- name: Install mono
@@ -114,55 +159,25 @@ jobs:
sudo apt update
sudo apt install -y mono-devel
- name: Download NuGet files
- uses: actions/download-artifact@v2
+ uses: actions/download-artifact@v4
with:
- name: nuget-files
- path: ./nuget-files
+ name: signed-dlls
+ path: ./signed-dlls
- name: Organize files
run: |
- pushd ./nuget-files
+ pushd ./signed-dlls
# Move all dlls to the root directory
- find . -type f -name "*.dll" -exec mv {} . \;
+ find . -type f -name "*.dll" -exec mv {} .
popd
# Create directories
mkdir -p nuget/lib/net35/ nuget/lib/net40/ nuget/lib/net45/ nuget/lib/netstandard1.6/ nuget/lib/netstandard2.0/
pushd ./nuget
# Move files to directories
- mv ../nuget-files/OptimizelySDK.Net35.dll lib/net35/
- mv ../nuget-files/OptimizelySDK.Net40.dll lib/net40/
- mv ../nuget-files/OptimizelySDK.dll lib/net45/
- mv ../nuget-files/OptimizelySDK.NetStandard16.dll lib/netstandard1.6/
- mv ../nuget-files/OptimizelySDK.NetStandard20.dll lib/netstandard2.0/
- popd
- - name: Setup signing prerequisites
- env:
- CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }}
- CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
- run: |
- pushd ./nuget
- echo $CERTIFICATE_P12 | base64 --decode > authenticode.pfx
- openssl pkcs12 -in authenticode.pfx -nocerts -nodes -legacy -out key.pem -password env:CERTIFICATE_PASSWORD
- openssl rsa -in key.pem -outform PVK -pvk-none -out authenticode.pvk
- openssl pkcs12 -in authenticode.pfx -nokeys -nodes -legacy -out cert.pem -password env:CERTIFICATE_PASSWORD
- openssl crl2pkcs7 -nocrl -certfile cert.pem -outform DER -out authenticode.spc
- popd
- - name: Sign the DLLs
- run: |
- pushd ./nuget
- find . -type f -name "*.dll" -print0 | while IFS= read -r -d '' file; do
- echo "Signing ${file}"
- signcode \
- -spc ./authenticode.spc \
- -v ./authenticode.pvk \
- -a sha1 -$ commercial \
- -n "Optimizely, Inc" \
- -i "https://www.optimizely.com/" \
- -t "http://timestamp.digicert.com" \
- -tr 10 \
- ${file}
- rm ${file}.bak
- done
- rm *.spc *.pem *.pvk *.pfx
+ mv ../signed-dlls/OptimizelySDK.Net35.dll lib/net35/
+ mv ../signed-dlls/OptimizelySDK.Net40.dll lib/net40/
+ mv ../signed-dlls/OptimizelySDK.dll lib/net45/
+ mv ../signed-dlls/OptimizelySDK.NetStandard16.dll lib/netstandard1.6/
+ mv ../signed-dlls/OptimizelySDK.NetStandard20.dll lib/netstandard2.0/
popd
- name: Create nuspec
# Uses env.VERSION in OptimizelySDK.nuspec.template
@@ -175,27 +190,29 @@ jobs:
nuget pack OptimizelySDK.nuspec
popd
- name: Upload nupkg artifact
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
name: nuget-package
if-no-files-found: error
path: ./nuget/Optimizely.SDK.${{ env.VERSION }}.nupkg
publish:
- name: Publish package to NuGet
+ name: Publish package to NuGet after reviewing the artifact
needs: [ variables, pack ]
runs-on: ubuntu-latest
+ # Review the `nuget-package` artifact ensuring the dlls are
+ # organized and signed before approving.
+ environment: 'i-reviewed-nuget-package-artifact'
env:
VERSION: ${{ needs.variables.outputs.semanticVersion }}
steps:
- name: Download NuGet files
- uses: actions/download-artifact@v2
+ uses: actions/download-artifact@v4
with:
name: nuget-package
path: ./nuget
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
- name: Publish NuGet package
- # Unset secrets.NUGET_API_KEY to simulate dry run
run: |
dotnet nuget push ./nuget/Optimizely.SDK.${{ env.VERSION }}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
index 8daf74ce3..a44954712 100644
--- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
+++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
@@ -209,6 +209,9 @@
Bucketing\UserProfile.cs
+
+ Bucketing\UserProfileTracker.cs
+
Bucketing\ExperimentUtils
@@ -356,4 +359,4 @@
-->
-
\ No newline at end of file
+
diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
index a0f98b775..05785575d 100644
--- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
+++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
@@ -208,6 +208,9 @@
Bucketing\UserProfile.cs
+
+ Bucketing\UserProfileTracker.cs
+
Bucketing\ExperimentUtils
@@ -366,4 +369,4 @@
-
\ No newline at end of file
+
diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
index 329b8b726..b17f79e74 100644
--- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
+++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
@@ -72,6 +72,7 @@
+
diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
index 6d7ea6382..b7114653d 100644
--- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
+++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
@@ -133,6 +133,9 @@
Bucketing\UserProfile.cs
+
+ Bucketing\UserProfileTracker.cs
+
Bucketing\UserProfileService.cs
diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs
index 633847ae1..8fbedf23c 100644
--- a/OptimizelySDK.Tests/DecisionServiceTest.cs
+++ b/OptimizelySDK.Tests/DecisionServiceTest.cs
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2017-2021, Optimizely and contributors
+ * Copyright 2017-2021, 2024 Optimizely and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -65,7 +65,7 @@ public void SetUp()
DecisionService = new DecisionService(new Bucketer(LoggerMock.Object),
ErrorHandlerMock.Object, null, LoggerMock.Object);
DecisionServiceMock = new Mock(BucketerMock.Object,
- ErrorHandlerMock.Object, null, LoggerMock.Object)
+ ErrorHandlerMock.Object, null, LoggerMock.Object)
{ CallBase = true };
DecisionReasons = new DecisionReasons();
@@ -150,6 +150,11 @@ public void TestGetVariationLogsErrorWhenUserProfileMapItsNull()
var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object,
UserProfileServiceMock.Object, LoggerMock.Object);
var options = new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS };
+ var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(),
+ LoggerMock.Object);
+ OptimizelyUserContextMock = new Mock(optlyObject,
+ WhitelistedUserId, new UserAttributes(), ErrorHandlerMock.Object,
+ LoggerMock.Object);
OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(GenericUserId);
var variationResult = decisionService.GetVariation(experiment,
@@ -157,8 +162,10 @@ public void TestGetVariationLogsErrorWhenUserProfileMapItsNull()
Assert.AreEqual(variationResult.DecisionReasons.ToReport(true)[0],
"We were unable to get a user profile map from the UserProfileService.");
Assert.AreEqual(variationResult.DecisionReasons.ToReport(true)[1],
- "Audiences for experiment \"etag3\" collectively evaluated to FALSE");
+ "No previously activated variation of experiment \"etag3\" for user \"genericUserId\" found in user profile.");
Assert.AreEqual(variationResult.DecisionReasons.ToReport(true)[2],
+ "Audiences for experiment \"etag3\" collectively evaluated to FALSE");
+ Assert.AreEqual(variationResult.DecisionReasons.ToReport(true)[3],
"User \"genericUserId\" does not meet conditions to be in experiment \"etag3\".");
}
@@ -291,6 +298,9 @@ public void TestBucketReturnsVariationStoredInUserProfile()
{
var experiment = ProjectConfig.Experiments[6];
var variation = experiment.Variations[0];
+ var variationResult = Result.NewResult(
+ experiment.Variations[0],
+ DecisionReasons);
var decision = new Decision(variation.Id);
var userProfile = new UserProfile(UserProfileId, new Dictionary
@@ -300,8 +310,10 @@ public void TestBucketReturnsVariationStoredInUserProfile()
UserProfileServiceMock.Setup(_ => _.Lookup(UserProfileId)).Returns(userProfile.ToMap());
- var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object,
- UserProfileServiceMock.Object, LoggerMock.Object);
+ BucketerMock.
+ Setup(bm => bm.Bucket(ProjectConfig, experiment, It.IsAny(),
+ It.IsAny())).
+ Returns(variationResult);
var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(),
LoggerMock.Object);
@@ -310,6 +322,8 @@ public void TestBucketReturnsVariationStoredInUserProfile()
LoggerMock.Object);
OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(UserProfileId);
+ var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object,
+ UserProfileServiceMock.Object, LoggerMock.Object);
var actualVariation = decisionService.GetVariation(experiment,
OptimizelyUserContextMock.Object, ProjectConfig);
@@ -736,7 +750,8 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke
DecisionServiceMock.Setup(ds => ds.GetVariation(
ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"),
OptimizelyUserContextMock.Object, ProjectConfig,
- It.IsAny())).
+ It.IsAny(), It.IsAny(),
+ It.IsAny())).
Returns(variation);
var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
@@ -789,13 +804,18 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
[Test]
public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed()
{
- var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1");
+ var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(),
+ LoggerMock.Object);
+ OptimizelyUserContextMock = new Mock(optlyObject,
+ WhitelistedUserId, new UserAttributes(), ErrorHandlerMock.Object,
+ LoggerMock.Object);
OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns("user1");
DecisionServiceMock.
Setup(ds => ds.GetVariation(It.IsAny(),
It.IsAny(), ProjectConfig,
- It.IsAny())).
+ It.IsAny(), It.IsAny(),
+ It.IsAny())).
Returns(Result.NullResult(null));
var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
@@ -856,7 +876,7 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile()
ds.GetVariationForFeatureExperiment(It.IsAny(),
It.IsAny(), It.IsAny(),
ProjectConfig,
- new OptimizelyDecideOption[] { })).
+ new OptimizelyDecideOption[] { }, null)).
Returns(null);
var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(),
LoggerMock.Object);
@@ -1201,7 +1221,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment
DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(
It.IsAny(), It.IsAny(),
It.IsAny(), ProjectConfig,
- It.IsAny())).
+ It.IsAny(), null)).
Returns(expectedDecision);
OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns("user1");
@@ -1228,7 +1248,7 @@ public void
DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(
It.IsAny(), It.IsAny(),
It.IsAny(), ProjectConfig,
- It.IsAny())).
+ It.IsAny(), null)).
Returns(Result.NullResult(null));
DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(
It.IsAny(), It.IsAny(),
@@ -1262,7 +1282,7 @@ public void
ds.GetVariationForFeatureExperiment(It.IsAny(),
It.IsAny(), It.IsAny(),
ProjectConfig,
- new OptimizelyDecideOption[] { })).
+ new OptimizelyDecideOption[] { }, null)).
Returns(Result.NullResult(null));
DecisionServiceMock.
Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(),
@@ -1309,6 +1329,11 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
WhitelistedUserId, userAttributes, ErrorHandlerMock.Object, LoggerMock.Object);
OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(UserProfileId);
+ BucketerMock.
+ Setup(bm => bm.Bucket(ProjectConfig, experiment, It.IsAny(),
+ It.IsAny())).
+ Returns(variation);
+
DecisionServiceMock.Setup(ds => ds.GetVariation(experiment,
OptimizelyUserContextMock.Object, ProjectConfig,
It.IsAny())).
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index 49fd91a43..76d0d8b8b 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2021, 2022-2023 Optimizely and contributors
+ * Copyright 2020-2021, 2022-2024 Optimizely and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
using System;
using System.Collections.Generic;
-using System.Threading;
+using System.Linq;
using Castle.Core.Internal;
using Moq;
using NUnit.Framework;
@@ -62,6 +62,22 @@ public void SetUp()
LoggerMock.Object, ErrorHandlerMock.Object);
}
+ private Mock MakeUserProfileServiceMock()
+ {
+ var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object,
+ ErrorHandlerMock.Object);
+ var experiment = projectConfig.Experiments[8];
+ var variation = experiment.Variations[0];
+ var decision = new Decision(variation.Id);
+ var userProfile = new UserProfile(UserID, new Dictionary
+ {
+ { experiment.Id, decision },
+ });
+ var userProfileServiceMock = new Mock();
+ userProfileServiceMock.Setup(up => up.Lookup(UserID)).Returns(userProfile.ToMap());
+ return userProfileServiceMock;
+ }
+
[Test]
public void OptimizelyUserContextWithAttributes()
{
@@ -193,7 +209,7 @@ public void SetAttributeToOverrideAttribute()
Assert.AreEqual(user.GetAttributes()["k1"], true);
}
- #region decide
+ #region Decide
[Test]
public void TestDecide()
@@ -409,9 +425,112 @@ public void DecideWhenConfigIsNull()
Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected));
}
- #endregion decide
+ [Test]
+ public void SeparateDecideShouldHaveSameNumberOfUpsSaveAndLookup()
+ {
+ var flag1 = "double_single_variable_feature";
+ var flag2 = "integer_single_variable_feature";
+ var userProfileServiceMock = MakeUserProfileServiceMock();
+ var saveArgsCollector = new List>();
+ userProfileServiceMock.Setup(up => up.Save(Capture.In(saveArgsCollector)));
+ var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object,
+ LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object);
+ var user = optimizely.CreateUserContext(UserID);
+ var flag1UserProfile = new UserProfile(UserID, new Dictionary
+ {
+ { "224", new Decision("280") },
+ { "122238", new Decision("122240") },
+ });
+ var flag2UserProfile = new UserProfile(UserID, new Dictionary
+ {
+ { "224", new Decision("280") },
+ { "122241", new Decision("122242") },
+ });
+
+ user.Decide(flag1);
+ user.Decide(flag2);
+
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.INFO,
+ "We were unable to get a user profile map from the UserProfileService."),
+ Times.Never);
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."),
+ Times.Never);
+ userProfileServiceMock.Verify(l => l.Lookup(UserID), Times.Exactly(2));
+ userProfileServiceMock.Verify(l => l.Save(It.IsAny>()),
+ Times.Exactly(2));
+ Assert.AreEqual(saveArgsCollector[0], flag1UserProfile.ToMap());
+ Assert.AreEqual(saveArgsCollector[1], flag2UserProfile.ToMap());
+ }
+
+ [Test]
+ public void DecideWithUpsShouldOnlyLookupSaveOnce()
+ {
+ var flagKeyFromTestDataJson = "double_single_variable_feature";
+ var userProfileServiceMock = MakeUserProfileServiceMock();
+ var saveArgsCollector = new List>();
+ userProfileServiceMock.Setup(up => up.Save(Capture.In(saveArgsCollector)));
+ var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object,
+ LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object);
+ var user = optimizely.CreateUserContext(UserID);
+ var expectedUserProfile = new UserProfile(UserID, new Dictionary
+ {
+ { "224", new Decision("280") },
+ { "122238", new Decision("122240") },
+ });
+
+ user.Decide(flagKeyFromTestDataJson);
+
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.INFO,
+ "We were unable to get a user profile map from the UserProfileService."),
+ Times.Never);
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."),
+ Times.Never);
+ userProfileServiceMock.Verify(l => l.Lookup(UserID), Times.Once);
+ userProfileServiceMock.Verify(l => l.Save(It.IsAny>()),
+ Times.Once);
+ Assert.AreEqual(saveArgsCollector.First(), expectedUserProfile.ToMap());
+ }
+
+ #endregion Decide
+
+ #region DecideForKeys
+
+ [Test]
+ public void DecideForKeysWithUpsShouldOnlyLookupSaveOnceWithMultipleFlags()
+ {
+ var flagKeys = new[] { "double_single_variable_feature", "boolean_feature" };
+ var userProfileServiceMock = MakeUserProfileServiceMock();
+ var saveArgsCollector = new List>();
+ userProfileServiceMock.Setup(up => up.Save(Capture.In(saveArgsCollector)));
+ var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object,
+ LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object);
+ var userContext = optimizely.CreateUserContext(UserID);
+ var expectedUserProfile = new UserProfile(UserID, new Dictionary
+ {
+ { "224", new Decision("280") },
+ { "122238", new Decision("122240") },
+ { "7723330021", new Decision(null) },
+ { "7718750065", new Decision(null) },
+ });
+
+ userContext.DecideForKeys(flagKeys);
- #region decideAll
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.INFO,
+ "We were unable to get a user profile map from the UserProfileService."),
+ Times.Never);
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."),
+ Times.Never);
+ userProfileServiceMock.Verify(l => l.Lookup(UserID), Times.Once);
+ userProfileServiceMock.Verify(l => l.Save(It.IsAny>()),
+ Times.Once);
+ Assert.AreEqual(saveArgsCollector.First(), expectedUserProfile.ToMap());
+ }
[Test]
public void DecideForKeysWithOneFlag()
@@ -443,6 +562,44 @@ public void DecideForKeysWithOneFlag()
Assert.IsTrue(TestData.CompareObjects(decision, expDecision));
}
+ #endregion DecideForKeys
+
+ #region DecideAll
+
+ [Test]
+ public void DecideAllWithUpsShouldOnlyLookupSaveOnce()
+ {
+ var userProfileServiceMock = MakeUserProfileServiceMock();
+ var saveArgsCollector = new List>();
+ userProfileServiceMock.Setup(up => up.Save(Capture.In(saveArgsCollector)));
+ var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object,
+ LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object);
+ var user = optimizely.CreateUserContext(UserID);
+ var expectedUserProfile = new UserProfile(UserID, new Dictionary
+ {
+ { "224", new Decision("280") },
+ { "122238", new Decision("122240") },
+ { "122241", new Decision("122242") },
+ { "122235", new Decision("122236") },
+ { "7723330021", new Decision(null) },
+ { "7718750065", new Decision(null) },
+ });
+
+ user.DecideAll();
+
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.INFO,
+ "We were unable to get a user profile map from the UserProfileService."),
+ Times.Never);
+ LoggerMock.Verify(
+ l => l.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."),
+ Times.Never);
+ userProfileServiceMock.Verify(l => l.Lookup(UserID), Times.Once);
+ userProfileServiceMock.Verify(l => l.Save(It.IsAny>()),
+ Times.Once);
+ Assert.AreEqual(saveArgsCollector.First(), expectedUserProfile.ToMap());
+ }
+
[Test]
public void DecideAllTwoFlag()
{
@@ -650,7 +807,7 @@ public void DecideAllAllFlags()
null,
flagKey10,
user,
- new string[0]);
+ new[] { "Variable value for key \"any_key\" is invalid or wrong type." });
Assert.IsTrue(TestData.CompareObjects(decisions[flagKey10], expDecision10));
}
diff --git a/OptimizelySDK.sln.DotSettings b/OptimizelySDK.sln.DotSettings
index 3ccf7ffc1..8ee6e5a47 100644
--- a/OptimizelySDK.sln.DotSettings
+++ b/OptimizelySDK.sln.DotSettings
@@ -43,10 +43,15 @@
<Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"><ElementKinds><Kind Name="INTERFACE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /></Policy>TrueTrueTrueTrue
+ TrueTrueTrueTrue
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index e6088d7e0..1e364b292 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -1,5 +1,5 @@
/*
-* Copyright 2017-2022, Optimizely
+* Copyright 2017-2022, 2024 Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using OptimizelySDK.Entity;
using OptimizelySDK.ErrorHandler;
using OptimizelySDK.Logger;
@@ -84,9 +85,9 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler,
///
/// Get a Variation of an Experiment for a user to be allocated into.
///
- /// The Experiment the user will be bucketed into.
- /// Optimizely user context.
- /// Project config.
+ /// The Experiment the user will be bucketed into.
+ /// Optimizely user context.
+ /// Project config.
/// The Variation the user is allocated into.
public virtual Result GetVariation(Experiment experiment,
OptimizelyUserContext user,
@@ -99,11 +100,11 @@ ProjectConfig config
///
/// Get a Variation of an Experiment for a user to be allocated into.
///
- /// The Experiment the user will be bucketed into.
- /// optimizely user context.
- /// Project Config.
- /// An array of decision options.
- /// The Variation the user is allocated into.
+ /// The Experiment the user will be bucketed into.
+ /// Optimizely user context.
+ /// Project Config.
+ /// An array of decision options.
+ ///
public virtual Result GetVariation(Experiment experiment,
OptimizelyUserContext user,
ProjectConfig config,
@@ -111,97 +112,107 @@ OptimizelyDecideOption[] options
)
{
var reasons = new DecisionReasons();
- var userId = user.GetUserId();
+
+ var ignoreUps = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
+ UserProfileTracker userProfileTracker = null;
+
+ if (UserProfileService != null && !ignoreUps)
+ {
+ userProfileTracker = new UserProfileTracker(UserProfileService, user.GetUserId(),
+ Logger, ErrorHandler);
+ userProfileTracker.LoadUserProfile(reasons);
+ }
+
+ var response = GetVariation(experiment, user, config, options, userProfileTracker,
+ reasons);
+
+ if (UserProfileService != null && !ignoreUps &&
+ userProfileTracker?.ProfileUpdated == true)
+ {
+ userProfileTracker.SaveUserProfile();
+ }
+
+ return response;
+ }
+
+ ///
+ /// Get a Variation of an Experiment for a user to be allocated into.
+ ///
+ /// The Experiment the user will be bucketed into.
+ /// Optimizely user context.
+ /// Project Config.
+ /// An array of decision options.
+ /// A UserProfileTracker object.
+ /// Set of reasons for the decision.
+ /// The Variation the user is allocated into.
+ public virtual Result GetVariation(Experiment experiment,
+ OptimizelyUserContext user,
+ ProjectConfig config,
+ OptimizelyDecideOption[] options,
+ UserProfileTracker userProfileTracker,
+ DecisionReasons reasons = null
+ )
+ {
+ if (reasons == null)
+ {
+ reasons = new DecisionReasons();
+ }
+
if (!ExperimentUtils.IsExperimentActive(experiment, Logger))
{
+ var message = reasons.AddInfo($"Experiment {experiment.Key} is not running.");
+ Logger.Log(LogLevel.INFO, message);
return Result.NullResult(reasons);
}
- // check if a forced variation is set
- var decisionVariationResult = GetForcedVariation(experiment.Key, userId, config);
- reasons += decisionVariationResult.DecisionReasons;
- var variation = decisionVariationResult.ResultObject;
+ var userId = user.GetUserId();
+
+ var decisionVariation = GetForcedVariation(experiment.Key, userId, config);
+ reasons += decisionVariation.DecisionReasons;
+ var variation = decisionVariation.ResultObject;
if (variation == null)
{
- decisionVariationResult = GetWhitelistedVariation(experiment, user.GetUserId());
- reasons += decisionVariationResult.DecisionReasons;
-
- variation = decisionVariationResult.ResultObject;
+ decisionVariation = GetWhitelistedVariation(experiment, user.GetUserId());
+ reasons += decisionVariation.DecisionReasons;
+ variation = decisionVariation.ResultObject;
}
if (variation != null)
{
- decisionVariationResult.SetReasons(reasons);
- return decisionVariationResult;
+ decisionVariation.SetReasons(reasons);
+ return decisionVariation;
}
- // fetch the user profile map from the user profile service
- var ignoreUPS = Array.Exists(options,
- option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
-
- UserProfile userProfile = null;
- if (!ignoreUPS && UserProfileService != null)
+ if (userProfileTracker != null)
{
- try
- {
- var userProfileMap = UserProfileService.Lookup(user.GetUserId());
- if (userProfileMap != null &&
- UserProfileUtil.IsValidUserProfileMap(userProfileMap))
- {
- userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
- decisionVariationResult =
- GetStoredVariation(experiment, userProfile, config);
- reasons += decisionVariationResult.DecisionReasons;
- if (decisionVariationResult.ResultObject != null)
- {
- return decisionVariationResult.SetReasons(reasons);
- }
- }
- else if (userProfileMap == null)
- {
- Logger.Log(LogLevel.INFO,
- reasons.AddInfo(
- "We were unable to get a user profile map from the UserProfileService."));
- }
- else
- {
- Logger.Log(LogLevel.ERROR,
- reasons.AddInfo("The UserProfileService returned an invalid map."));
- }
- }
- catch (Exception exception)
+ decisionVariation =
+ GetStoredVariation(experiment, userProfileTracker.UserProfile, config);
+ reasons += decisionVariation.DecisionReasons;
+ variation = decisionVariation.ResultObject;
+ if (variation != null)
{
- Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
- ErrorHandler.HandleError(
- new Exceptions.OptimizelyRuntimeException(exception.Message));
+ return decisionVariation;
}
}
- var filteredAttributes = user.GetAttributes();
- var doesUserMeetAudienceConditionsResult =
- ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, user,
- LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger);
- reasons += doesUserMeetAudienceConditionsResult.DecisionReasons;
- if (doesUserMeetAudienceConditionsResult.ResultObject)
+ var decisionMeetAudience = ExperimentUtils.DoesUserMeetAudienceConditions(config,
+ experiment, user,
+ LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger);
+ reasons += decisionMeetAudience.DecisionReasons;
+ if (decisionMeetAudience.ResultObject)
{
- // Get Bucketing ID from user attributes.
- var bucketingIdResult = GetBucketingId(userId, filteredAttributes);
- reasons += bucketingIdResult.DecisionReasons;
+ var bucketingId = GetBucketingId(userId, user.GetAttributes()).ResultObject;
- decisionVariationResult = Bucketer.Bucket(config, experiment,
- bucketingIdResult.ResultObject, userId);
- reasons += decisionVariationResult.DecisionReasons;
+ decisionVariation = Bucketer.Bucket(config, experiment, bucketingId, userId);
+ reasons += decisionVariation.DecisionReasons;
+ variation = decisionVariation.ResultObject;
- if (decisionVariationResult.ResultObject?.Key != null)
+ if (variation != null)
{
- if (UserProfileService != null && !ignoreUPS)
+ if (userProfileTracker != null)
{
- var bucketerUserProfile = userProfile ??
- new UserProfile(userId,
- new Dictionary());
- SaveVariation(experiment, decisionVariationResult.ResultObject,
- bucketerUserProfile);
+ userProfileTracker.UpdateUserProfile(experiment, variation);
}
else
{
@@ -210,7 +221,7 @@ OptimizelyDecideOption[] options
}
}
- return decisionVariationResult.SetReasons(reasons);
+ return decisionVariation.SetReasons(reasons);
}
Logger.Log(LogLevel.INFO,
@@ -253,8 +264,7 @@ ProjectConfig config
if (experimentToVariationMap.ContainsKey(experimentId) == false)
{
Logger.Log(LogLevel.DEBUG,
- $@"No experiment ""{experimentKey}"" mapped to user ""{userId
- }"" in the forced variation map.");
+ $@"No experiment ""{experimentKey}"" mapped to user ""{userId}"" in the forced variation map.");
return Result.NullResult(reasons);
}
@@ -263,8 +273,7 @@ ProjectConfig config
if (string.IsNullOrEmpty(variationId))
{
Logger.Log(LogLevel.DEBUG,
- $@"No variation mapped to experiment ""{experimentKey
- }"" in the forced variation map.");
+ $@"No variation mapped to experiment ""{experimentKey}"" in the forced variation map.");
return Result.NullResult(reasons);
}
@@ -277,8 +286,7 @@ ProjectConfig config
}
Logger.Log(LogLevel.DEBUG,
- reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{
- experimentKey}"" and user ""{userId}"" in the forced variation map"));
+ reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
var variation = config.GetVariationFromKey(experimentKey, variationKey);
@@ -322,8 +330,7 @@ ProjectConfig config
}
Logger.Log(LogLevel.DEBUG,
- $@"Variation mapped to experiment ""{experimentKey
- }"" has been removed for user ""{userId}"".");
+ $@"Variation mapped to experiment ""{experimentKey}"" has been removed for user ""{userId}"".");
return true;
}
@@ -345,8 +352,7 @@ ProjectConfig config
ForcedVariationMap[userId][experimentId] = variationId;
Logger.Log(LogLevel.DEBUG,
- $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{
- userId}"" in the forced variation map.");
+ $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map.");
return true;
}
@@ -638,7 +644,8 @@ public virtual Result GetVariationForFeatureExperiment(
OptimizelyUserContext user,
UserAttributes filteredAttributes,
ProjectConfig config,
- OptimizelyDecideOption[] options
+ OptimizelyDecideOption[] options,
+ UserProfileTracker userProfileTracker = null
)
{
var reasons = new DecisionReasons();
@@ -679,7 +686,8 @@ OptimizelyDecideOption[] options
}
else
{
- var decisionResponse = GetVariation(experiment, user, config, options);
+ var decisionResponse = GetVariation(experiment, user, config, options,
+ userProfileTracker);
reasons += decisionResponse?.DecisionReasons;
decisionVariation = decisionResponse.ResultObject;
@@ -706,9 +714,9 @@ OptimizelyDecideOption[] options
///
/// Get the variation the user is bucketed into for the FeatureFlag
///
- /// The feature flag the user wants to access.
- /// User Identifier
- /// The user's attributes. This should be filtered to just attributes in the Datafile.
+ /// The feature flag the user wants to access.
+ /// The user context.
+ /// The project config.
/// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is
/// successfully bucketed.
public virtual Result GetVariationForFeature(FeatureFlag featureFlag,
@@ -719,53 +727,101 @@ public virtual Result GetVariationForFeature(FeatureFlag featur
new OptimizelyDecideOption[] { });
}
- ///
- /// Get the variation the user is bucketed into for the FeatureFlag
- ///
- /// The feature flag the user wants to access.
- /// User Identifier
- /// The user's attributes. This should be filtered to just attributes in the Datafile.
- /// The user's attributes. This should be filtered to just attributes in the Datafile.
- /// An array of decision options.
- /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is
- /// successfully bucketed.
- public virtual Result GetVariationForFeature(FeatureFlag featureFlag,
+ public virtual List> GetVariationsForFeatureList(
+ List featureFlags,
OptimizelyUserContext user,
- ProjectConfig config,
+ ProjectConfig projectConfig,
UserAttributes filteredAttributes,
OptimizelyDecideOption[] options
)
{
- var reasons = new DecisionReasons();
- var userId = user.GetUserId();
- // Check if the feature flag has an experiment and the user is bucketed into that experiment.
- var decisionResult = GetVariationForFeatureExperiment(featureFlag, user,
- filteredAttributes, config, options);
- reasons += decisionResult.DecisionReasons;
+ var upsReasons = new DecisionReasons();
+
+ var ignoreUps = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
+ UserProfileTracker userProfileTracker = null;
- if (decisionResult.ResultObject != null)
+ if (UserProfileService != null && !ignoreUps)
{
- return Result.NewResult(decisionResult.ResultObject, reasons);
+ userProfileTracker = new UserProfileTracker(UserProfileService, user.GetUserId(),
+ Logger, ErrorHandler);
+ userProfileTracker.LoadUserProfile(upsReasons);
}
- // Check if the feature flag has rollout and the the user is bucketed into one of its rules.
- decisionResult = GetVariationForFeatureRollout(featureFlag, user, config);
- reasons += decisionResult.DecisionReasons;
+ var userId = user.GetUserId();
+ var decisions = new List>();
- if (decisionResult.ResultObject != null)
+ foreach (var featureFlag in featureFlags)
{
- Logger.Log(LogLevel.INFO,
- reasons.AddInfo(
- $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
- return Result.NewResult(decisionResult.ResultObject, reasons);
+ var reasons = new DecisionReasons();
+ reasons += upsReasons;
+
+ // Check if the feature flag has an experiment and the user is bucketed into that experiment.
+ var decisionResult = GetVariationForFeatureExperiment(featureFlag, user,
+ filteredAttributes, projectConfig, options, userProfileTracker);
+ reasons += decisionResult.DecisionReasons;
+
+ if (decisionResult.ResultObject != null)
+ {
+ decisions.Add(
+ Result.NewResult(decisionResult.ResultObject, reasons));
+ continue;
+ }
+
+ // Check if the feature flag has rollout and the the user is bucketed into one of its rules.
+ decisionResult = GetVariationForFeatureRollout(featureFlag, user, projectConfig);
+ reasons += decisionResult.DecisionReasons;
+
+ if (decisionResult.ResultObject == null)
+ {
+ Logger.Log(LogLevel.INFO,
+ reasons.AddInfo(
+ $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+ decisions.Add(Result.NewResult(
+ new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT),
+ reasons));
+ }
+ else
+ {
+ Logger.Log(LogLevel.INFO,
+ reasons.AddInfo(
+ $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+ decisions.Add(
+ Result.NewResult(decisionResult.ResultObject, reasons));
+ }
}
- Logger.Log(LogLevel.INFO,
- reasons.AddInfo(
- $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
- return Result.NewResult(
- new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons);
- ;
+ if (UserProfileService != null && !ignoreUps &&
+ userProfileTracker?.ProfileUpdated == true)
+ {
+ userProfileTracker.SaveUserProfile();
+ }
+
+ return decisions;
+ }
+
+ ///
+ /// Get the variation the user is bucketed into for the FeatureFlag
+ ///
+ /// The feature flag the user wants to access.
+ /// The user context.
+ /// The project config.
+ /// The user's attributes. This should be filtered to just attributes in the Datafile.
+ /// An array of decision options.
+ /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is
+ /// successfully bucketed.
+ public virtual Result GetVariationForFeature(FeatureFlag featureFlag,
+ OptimizelyUserContext user,
+ ProjectConfig config,
+ UserAttributes filteredAttributes,
+ OptimizelyDecideOption[] options
+ )
+ {
+ return GetVariationsForFeatureList(new List { featureFlag },
+ user,
+ config,
+ filteredAttributes,
+ options).
+ First();
}
///
diff --git a/OptimizelySDK/Bucketing/UserProfileTracker.cs b/OptimizelySDK/Bucketing/UserProfileTracker.cs
new file mode 100644
index 000000000..226cca482
--- /dev/null
+++ b/OptimizelySDK/Bucketing/UserProfileTracker.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using OptimizelySDK.Entity;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
+
+namespace OptimizelySDK.Bucketing
+{
+ public class UserProfileTracker
+ {
+ public UserProfile UserProfile { get; private set; }
+ public bool ProfileUpdated { get; private set; }
+
+ private readonly UserProfileService _userProfileService;
+ private readonly string _userId;
+ private readonly ILogger _logger;
+ private readonly IErrorHandler _errorHandler;
+
+ public UserProfileTracker(UserProfileService userProfileService, string userId, ILogger logger, IErrorHandler errorHandler)
+ {
+ _userProfileService = userProfileService;
+ _userId = userId;
+ _logger = logger;
+ _errorHandler = errorHandler;
+ ProfileUpdated = false;
+ UserProfile = null;
+ }
+
+ public void LoadUserProfile(DecisionReasons reasons)
+ {
+ try
+ {
+ var userProfileMap = _userProfileService.Lookup(_userId);
+ if (userProfileMap == null)
+ {
+ _logger.Log(LogLevel.INFO,
+ reasons.AddInfo(
+ "We were unable to get a user profile map from the UserProfileService."));
+ }
+ else if (UserProfileUtil.IsValidUserProfileMap(userProfileMap))
+ {
+ UserProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
+ }
+ else
+ {
+ _logger.Log(LogLevel.WARN,
+ reasons.AddInfo("The UserProfileService returned an invalid map."));
+ }
+ }
+ catch (Exception exception)
+ {
+ _logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
+ _errorHandler.HandleError(
+ new Exceptions.OptimizelyRuntimeException(exception.Message));
+ }
+
+ if (UserProfile == null)
+ {
+ UserProfile = new UserProfile(_userId, new Dictionary());
+ }
+ }
+
+ public void UpdateUserProfile(Experiment experiment, Variation variation)
+ {
+ var experimentId = experiment.Id;
+ var variationId = variation.Id;
+ Decision decision;
+ if (UserProfile.ExperimentBucketMap.ContainsKey(experimentId))
+ {
+ decision = UserProfile.ExperimentBucketMap[experimentId];
+ decision.VariationId = variationId;
+ }
+ else
+ {
+ decision = new Decision(variationId);
+ }
+
+ UserProfile.ExperimentBucketMap[experimentId] = decision;
+ ProfileUpdated = true;
+
+ _logger.Log(LogLevel.INFO,
+ $"Saved variation \"{variationId}\" of experiment \"{experimentId}\" for user \"{UserProfile.UserId}\".");
+ }
+
+ public void SaveUserProfile()
+ {
+ if (!ProfileUpdated)
+ {
+ return;
+ }
+
+ try
+ {
+ _userProfileService.Save(UserProfile.ToMap());
+ _logger.Log(LogLevel.INFO,
+ $"Saved user profile of user \"{UserProfile.UserId}\".");
+ }
+ catch (Exception exception)
+ {
+ _logger.Log(LogLevel.WARN,
+ $"Failed to save user profile of user \"{UserProfile.UserId}\".");
+ _errorHandler.HandleError(
+ new Exceptions.OptimizelyRuntimeException(exception.Message));
+ }
+ }
+ }
+}
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index 9da300f85..b1766985c 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2023, Optimizely
+ * Copyright 2017-2024, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use file except in compliance with the License.
@@ -573,8 +573,7 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId,
else
{
Logger.Log(LogLevel.INFO,
- $@"The user ""{userId}"" is not being experimented on feature ""{featureKey
- }"".");
+ $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
}
}
@@ -624,8 +623,7 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var
if (config == null)
{
Logger.Log(LogLevel.ERROR,
- $@"Datafile has invalid format. Failing '{
- FeatureVariable.GetFeatureVariableTypeName(variableType)}'.");
+ $@"Datafile has invalid format. Failing '{FeatureVariable.GetFeatureVariableTypeName(variableType)}'.");
return default;
}
@@ -649,15 +647,13 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var
if (featureVariable == null)
{
Logger.Log(LogLevel.ERROR,
- $@"No feature variable was found for key ""{variableKey}"" in feature flag ""{
- featureKey}"".");
+ $@"No feature variable was found for key ""{variableKey}"" in feature flag ""{featureKey}"".");
return default;
}
else if (featureVariable.Type != variableType)
{
Logger.Log(LogLevel.ERROR,
- $@"Variable is of type ""{featureVariable.Type
- }"", but you requested it as type ""{variableType}"".");
+ $@"Variable is of type ""{featureVariable.Type}"", but you requested it as type ""{variableType}"".");
return default;
}
@@ -681,28 +677,24 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var
{
variableValue = featureVariableUsageInstance.Value;
Logger.Log(LogLevel.INFO,
- $@"Got variable value ""{variableValue}"" for variable ""{variableKey
- }"" of feature flag ""{featureKey}"".");
+ $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".");
}
else
{
Logger.Log(LogLevel.INFO,
- $@"Feature ""{featureKey}"" is not enabled for user {userId
- }. Returning the default variable value ""{variableValue}"".");
+ $@"Feature ""{featureKey}"" is not enabled for user {userId}. Returning the default variable value ""{variableValue}"".");
}
}
else
{
Logger.Log(LogLevel.INFO,
- $@"Variable ""{variableKey}"" is not used in variation ""{variation.Key
- }"", returning default value ""{variableValue}"".");
+ $@"Variable ""{variableKey}"" is not used in variation ""{variation.Key}"", returning default value ""{variableValue}"".");
}
}
else
{
Logger.Log(LogLevel.INFO,
- $@"User ""{userId}"" is not in any variation for feature flag ""{featureKey
- }"", returning default value ""{variableValue}"".");
+ $@"User ""{userId}"" is not in any variation for feature flag ""{featureKey}"", returning default value ""{variableValue}"".");
}
var sourceInfo = new Dictionary();
@@ -861,6 +853,7 @@ private OptimizelyUserContext CreateUserContextCopy(string userId,
///
If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons.
///
///
+ /// User context to be used to make decision.
/// A flag key for which a decision will be made.
/// A list of options for decision-making.
/// A decision result.
@@ -877,122 +870,193 @@ OptimizelyDecideOption[] options
ErrorHandler, Logger);
}
- if (key == null)
- {
- return OptimizelyDecision.NewErrorDecision(key,
- user,
- DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key),
- ErrorHandler, Logger);
- }
+ var allOptions = GetAllOptions(options).
+ Where(opt => opt != OptimizelyDecideOption.ENABLED_FLAGS_ONLY).
+ ToArray();
+
+ return DecideForKeys(user, new[] { key }, allOptions, true)[key];
+ }
- var flag = config.GetFeatureFlagFromKey(key);
- if (flag.Key == null)
+ internal Dictionary DecideAll(OptimizelyUserContext user,
+ OptimizelyDecideOption[] options
+ )
+ {
+ var decisionMap = new Dictionary();
+
+ var projectConfig = ProjectConfigManager?.GetConfig();
+ if (projectConfig == null)
{
- return OptimizelyDecision.NewErrorDecision(key,
- user,
- DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key),
- ErrorHandler, Logger);
+ Logger.Log(LogLevel.ERROR,
+ "Optimizely instance is not valid, failing DecideAll call.");
+ return decisionMap;
}
- var userId = user?.GetUserId();
- var userAttributes = user?.GetAttributes();
- var decisionEventDispatched = false;
- var allOptions = GetAllOptions(options);
- var decisionReasons = new DecisionReasons();
- FeatureDecision decision = null;
+ var allFlags = projectConfig.FeatureFlags;
+ var allFlagKeys = allFlags.Select(v => v.Key).ToArray();
- var decisionContext = new OptimizelyDecisionContext(flag.Key);
- var forcedDecisionVariation =
- DecisionService.ValidatedForcedDecision(decisionContext, config, user);
- decisionReasons += forcedDecisionVariation.DecisionReasons;
+ return DecideForKeys(user, allFlagKeys, options);
+ }
+
+ internal Dictionary DecideForKeys(OptimizelyUserContext user,
+ string[] keys,
+ OptimizelyDecideOption[] options,
+ bool ignoreDefaultOptions = false
+ )
+ {
+ var decisionDictionary = new Dictionary();
- if (forcedDecisionVariation.ResultObject != null)
+ var projectConfig = ProjectConfigManager?.GetConfig();
+ if (projectConfig == null)
{
- decision = new FeatureDecision(null, forcedDecisionVariation.ResultObject,
- FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
+ Logger.Log(LogLevel.ERROR,
+ "Optimizely instance is not valid, failing DecideForKeys call.");
+ return decisionDictionary;
}
- else
+
+ if (keys.Length == 0)
{
- var flagDecisionResult = DecisionService.GetVariationForFeature(
- flag,
- user,
- config,
- userAttributes,
- allOptions
- );
- decisionReasons += flagDecisionResult.DecisionReasons;
- decision = flagDecisionResult.ResultObject;
+ return decisionDictionary;
}
- var featureEnabled = false;
+ var allOptions = ignoreDefaultOptions ? options : GetAllOptions(options);
- if (decision?.Variation != null)
- {
- featureEnabled = decision.Variation.FeatureEnabled.GetValueOrDefault();
- }
+ var flagDecisions = new Dictionary();
+ var decisionReasonsMap = new Dictionary();
- if (featureEnabled)
+ var flagsWithoutForcedDecisions = new List();
+
+ var validKeys = new List();
+
+ foreach (var key in keys)
{
- Logger.Log(LogLevel.INFO,
- "Feature \"" + key + "\" is enabled for user \"" + userId + "\"");
+ var flag = projectConfig.GetFeatureFlagFromKey(key);
+ if (flag.Key == null)
+ {
+ decisionDictionary.Add(key,
+ OptimizelyDecision.NewErrorDecision(key, user,
+ DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key),
+ ErrorHandler, Logger));
+ continue;
+ }
+
+ validKeys.Add(key);
+
+ var decisionReasons = new DecisionReasons();
+ decisionReasonsMap.Add(key, decisionReasons);
+
+ var optimizelyDecisionContext = new OptimizelyDecisionContext(key);
+ var forcedDecisionVariation =
+ DecisionService.ValidatedForcedDecision(optimizelyDecisionContext, projectConfig, user);
+ decisionReasons += forcedDecisionVariation.DecisionReasons;
+
+ if (forcedDecisionVariation.ResultObject != null)
+ {
+ flagDecisions.Add(key, new FeatureDecision(null,
+ forcedDecisionVariation.ResultObject,
+ FeatureDecision.DECISION_SOURCE_FEATURE_TEST));
+ }
+ else
+ {
+ flagsWithoutForcedDecisions.Add(flag);
+ }
}
- else
+
+ var decisionsList = DecisionService.GetVariationsForFeatureList(
+ flagsWithoutForcedDecisions, user, projectConfig, user.GetAttributes(),
+ allOptions);
+
+ for (var i = 0; i < decisionsList.Count; i += 1)
{
- Logger.Log(LogLevel.INFO,
- "Feature \"" + key + "\" is not enabled for user \"" + userId + "\"");
+ var decision = decisionsList[i];
+ var flagKey = flagsWithoutForcedDecisions[i].Key;
+ flagDecisions.Add(flagKey, decision.ResultObject);
+ decisionReasonsMap[flagKey] += decision.DecisionReasons;
}
- var variableMap = new Dictionary();
- if (flag?.Variables != null &&
- !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES))
+ foreach (var key in validKeys)
{
- foreach (var featureVariable in flag?.Variables)
+ var flagDecision = flagDecisions[key];
+ var decisionReasons = decisionReasonsMap[key];
+
+ var optimizelyDecision = CreateOptimizelyDecision(user, key, flagDecision,
+ decisionReasons, allOptions.ToList(), projectConfig);
+ if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) ||
+ optimizelyDecision.Enabled)
{
- var variableValue = featureVariable.DefaultValue;
- if (featureEnabled)
- {
- var featureVariableUsageInstance =
- decision?.Variation.GetFeatureVariableUsageFromId(featureVariable.Id);
- if (featureVariableUsageInstance != null)
- {
- variableValue = featureVariableUsageInstance.Value;
- }
- }
+ decisionDictionary.Add(key, optimizelyDecision);
+ }
+ }
- var typeCastedValue =
- GetTypeCastedVariableValue(variableValue, featureVariable.Type);
+ return decisionDictionary;
+ }
- if (typeCastedValue is OptimizelyJSON)
- {
- typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary();
- }
+ private OptimizelyDecision CreateOptimizelyDecision(
+ OptimizelyUserContext user,
+ string flagKey,
+ FeatureDecision flagDecision,
+ DecisionReasons decisionReasons,
+ List allOptions,
+ ProjectConfig projectConfig
+ )
+ {
+ var userId = user.GetUserId();
- variableMap.Add(featureVariable.Key, typeCastedValue);
+ var flagEnabled = false;
+ if (flagDecision.Variation != null)
+ {
+ if (flagDecision.Variation.IsFeatureEnabled)
+ {
+ flagEnabled = true;
}
}
- var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger);
+ Logger.Log(LogLevel.INFO,
+ $"Feature \"{flagKey}\" is enabled for user \"{userId}\"? {flagEnabled}");
- var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
- if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT))
+ var variableMap = new Dictionary();
+ if (!allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES))
{
- decisionEventDispatched = SendImpressionEvent(decision?.Experiment,
- decision?.Variation, userId, userAttributes, config, key, decisionSource,
- featureEnabled);
+ var decisionVariables = GetDecisionVariableMap(
+ projectConfig.GetFeatureFlagFromKey(flagKey),
+ flagDecision.Variation,
+ flagEnabled);
+ variableMap = decisionVariables.ResultObject;
+ decisionReasons += decisionVariables.DecisionReasons;
}
- var reasonsToReport = decisionReasons
- .ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS))
- .ToArray();
- var variationKey = decision?.Variation?.Key;
+ var optimizelyJson = new OptimizelyJSON(variableMap, ErrorHandler, Logger);
+
+ var decisionSource = FeatureDecision.DECISION_SOURCE_ROLLOUT;
+ if (flagDecision.Source != null)
+ {
+ decisionSource = flagDecision.Source;
+ }
+ var includeReasons = allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS);
+ var reasonsToReport = decisionReasons.ToReport(includeReasons).ToArray();
+ var variationKey = flagDecision.Variation?.Key;
// TODO: add ruleKey values when available later. use a copy of experimentKey until then.
- var ruleKey = decision?.Experiment?.Key;
+ // add to event metadata as well (currently set to experimentKey)
+ var ruleKey = flagDecision.Experiment?.Key;
+
+ var decisionEventDispatched = false;
+ if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT))
+ {
+ decisionEventDispatched = SendImpressionEvent(
+ flagDecision.Experiment,
+ flagDecision.Variation,
+ userId,
+ user.GetAttributes(),
+ projectConfig,
+ flagKey,
+ decisionSource,
+ flagEnabled);
+ }
var decisionInfo = new Dictionary
{
- { "flagKey", key },
- { "enabled", featureEnabled },
+ { "flagKey", flagKey },
+ { "enabled", flagEnabled },
{ "variables", variableMap },
{ "variationKey", variationKey },
{ "ruleKey", ruleKey },
@@ -1001,72 +1065,49 @@ OptimizelyDecideOption[] options
};
NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision,
- DecisionNotificationTypes.FLAG, userId,
- userAttributes ?? new UserAttributes(), decisionInfo);
+ DecisionNotificationTypes.FLAG, userId, user.GetAttributes(), decisionInfo);
return new OptimizelyDecision(
variationKey,
- featureEnabled,
- optimizelyJSON,
+ flagEnabled,
+ optimizelyJson,
ruleKey,
- key,
+ flagKey,
user,
reasonsToReport);
}
- internal Dictionary DecideAll(OptimizelyUserContext user,
- OptimizelyDecideOption[] options
- )
+ private Result> GetDecisionVariableMap(FeatureFlag flag, Variation variation, bool featureEnabled)
{
- var decisionMap = new Dictionary();
-
- var projectConfig = ProjectConfigManager?.GetConfig();
- if (projectConfig == null)
- {
- Logger.Log(LogLevel.ERROR,
- "Optimizely instance is not valid, failing isFeatureEnabled call.");
- return decisionMap;
- }
-
- var allFlags = projectConfig.FeatureFlags;
- var allFlagKeys = allFlags.Select(v => v.Key).ToArray();
-
- return DecideForKeys(user, allFlagKeys, options);
- }
-
- internal Dictionary DecideForKeys(OptimizelyUserContext user,
- string[] keys,
- OptimizelyDecideOption[] options
- )
- {
- var decisionDictionary = new Dictionary();
+ var reasons = new DecisionReasons();
+ var valuesMap = new Dictionary();
- var projectConfig = ProjectConfigManager?.GetConfig();
- if (projectConfig == null)
+ foreach (var variable in flag.Variables)
{
- Logger.Log(LogLevel.ERROR,
- "Optimizely instance is not valid, failing isFeatureEnabled call.");
- return decisionDictionary;
- }
-
- if (keys.Length == 0)
- {
- return decisionDictionary;
- }
-
- var allOptions = GetAllOptions(options);
+ var value = variable.DefaultValue;
+ if (featureEnabled)
+ {
+ var instance = variation.GetFeatureVariableUsageFromId(variable.Id);
+ if (instance != null)
+ {
+ value = instance.Value;
+ }
+ }
- foreach (var key in keys)
- {
- var decision = Decide(user, key, options);
- if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) ||
- decision.Enabled)
+ var convertedValue = GetTypeCastedVariableValue(value, variable.Type);
+ if (convertedValue == null)
{
- decisionDictionary.Add(key, decision);
+ reasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, variable.Key));
}
+ else if (convertedValue is OptimizelyJSON optimizelyJson)
+ {
+ convertedValue = optimizelyJson.ToDictionary();
+ }
+
+ valuesMap[variable.Key] = convertedValue;
}
- return decisionDictionary;
+ return Result>.NewResult(valuesMap, reasons);
}
private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options)
diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj
index 1812a2adc..5f041ac18 100644
--- a/OptimizelySDK/OptimizelySDK.csproj
+++ b/OptimizelySDK/OptimizelySDK.csproj
@@ -75,6 +75,7 @@
+
From 846ed2d932ccfef955ec0280f3588d98c7473651 Mon Sep 17 00:00:00 2001
From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com>
Date: Thu, 7 Nov 2024 14:21:15 -0500
Subject: [PATCH 4/9] [FSSDK-10848] Prepare release 4.1.0 (#376)
* chore: bump semver 1 minor release
* doc: update for new minor release
---
CHANGELOG.md | 10 ++++++++++
OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs | 6 +++---
OptimizelySDK.Net35/Properties/AssemblyInfo.cs | 6 +++---
OptimizelySDK.Net40/Properties/AssemblyInfo.cs | 6 +++---
OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs | 6 +++---
OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs | 6 +++---
OptimizelySDK.Tests/Properties/AssemblyInfo.cs | 6 +++---
OptimizelySDK/Properties/AssemblyInfo.cs | 6 +++---
README.md | 2 +-
9 files changed, 32 insertions(+), 22 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b431aa14..dca45cda2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Optimizely C# SDK Changelog
+## 4.1.0
+November 7th, 2024
+
+### Enhancement
+
+- Added support for batch processing in `DecideAll` and `DecideForKeys`, enabling more efficient handling of multiple decisions in the User Profile Service. ([#375](https://github.com/optimizely/csharp-sdk/pull/375))
+
+### Bug Fixes
+- GitHub Actions YAML files vulnerable to script injections ([#372](https://github.com/optimizely/csharp-sdk/pull/372))
+
## 4.0.0
January 16th, 2024
diff --git a/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs b/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs
index 5fe786119..147aa1a83 100644
--- a/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.DemoApp/Properties/AssemblyInfo.cs
@@ -37,6 +37,6 @@
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("4.0.0.0")]
-[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
+[assembly: AssemblyVersion("4.1.0.0")]
+[assembly: AssemblyFileVersion("4.1.0.0")]
+[assembly: AssemblyInformationalVersion("4.1.0")] // Used by NuGet.
diff --git a/OptimizelySDK.Net35/Properties/AssemblyInfo.cs b/OptimizelySDK.Net35/Properties/AssemblyInfo.cs
index 9f7dfbe4f..dd5b9ab24 100644
--- a/OptimizelySDK.Net35/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.Net35/Properties/AssemblyInfo.cs
@@ -37,6 +37,6 @@
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("4.0.0.0")]
-[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
+[assembly: AssemblyVersion("4.1.0.0")]
+[assembly: AssemblyFileVersion("4.1.0.0")]
+[assembly: AssemblyInformationalVersion("4.1.0")] // Used by NuGet.
diff --git a/OptimizelySDK.Net40/Properties/AssemblyInfo.cs b/OptimizelySDK.Net40/Properties/AssemblyInfo.cs
index 815b273a5..48b0cb1d0 100644
--- a/OptimizelySDK.Net40/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.Net40/Properties/AssemblyInfo.cs
@@ -37,6 +37,6 @@
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("4.0.0.0")]
-[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
+[assembly: AssemblyVersion("4.1.0.0")]
+[assembly: AssemblyFileVersion("4.1.0.0")]
+[assembly: AssemblyInformationalVersion("4.1.0")] // Used by NuGet.
diff --git a/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs b/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs
index 16f95976c..f82a11ad0 100644
--- a/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.NetStandard16/Properties/AssemblyInfo.cs
@@ -37,6 +37,6 @@
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("4.0.0")]
-[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
+[assembly: AssemblyVersion("4.1.0")]
+[assembly: AssemblyFileVersion("4.1.0.0")]
+[assembly: AssemblyInformationalVersion("4.1.0")] // Used by NuGet.
diff --git a/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs b/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs
index 6e2fbdc3f..72640cfa1 100644
--- a/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.NetStandard20/Properties/AssemblyInfo.cs
@@ -37,6 +37,6 @@
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("4.0.0.0")]
-[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
+[assembly: AssemblyVersion("4.1.0.0")]
+[assembly: AssemblyFileVersion("4.1.0.0")]
+[assembly: AssemblyInformationalVersion("4.1.0")] // Used by NuGet.
diff --git a/OptimizelySDK.Tests/Properties/AssemblyInfo.cs b/OptimizelySDK.Tests/Properties/AssemblyInfo.cs
index ea9d640ed..f122893fe 100644
--- a/OptimizelySDK.Tests/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK.Tests/Properties/AssemblyInfo.cs
@@ -30,6 +30,6 @@
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("4.0.0.0")]
-[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
+[assembly: AssemblyVersion("4.1.0.0")]
+[assembly: AssemblyFileVersion("4.1.0.0")]
+[assembly: AssemblyInformationalVersion("4.1.0")] // Used by NuGet.
diff --git a/OptimizelySDK/Properties/AssemblyInfo.cs b/OptimizelySDK/Properties/AssemblyInfo.cs
index d801dfaa9..96d048676 100644
--- a/OptimizelySDK/Properties/AssemblyInfo.cs
+++ b/OptimizelySDK/Properties/AssemblyInfo.cs
@@ -41,6 +41,6 @@
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("4.0.0.0")]
-[assembly: AssemblyFileVersion("4.0.0.0")]
-[assembly: AssemblyInformationalVersion("4.0.0")] // Used by NuGet.
+[assembly: AssemblyVersion("4.1.0.0")]
+[assembly: AssemblyFileVersion("4.1.0.0")]
+[assembly: AssemblyInformationalVersion("4.1.0")] // Used by NuGet.
diff --git a/README.md b/README.md
index ff2148fb9..f6b04d5c9 100644
--- a/README.md
+++ b/README.md
@@ -104,7 +104,7 @@ User can provide variables using following procedure:
```
+ type="OptimizelySDK.OptimizelySDKConfigSection, OptimizelySDK, Version=4.1.0.0, Culture=neutral, PublicKeyToken=null" />
```
2. Now add **optlySDKConfigSection** below ****. In this section you can add and set following **HttpProjectConfigManager** and **BatchEventProcessor** variables:
From 3abcf563075fd5ba18989b97a7d4d1737e02135a Mon Sep 17 00:00:00 2001
From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com>
Date: Thu, 7 Nov 2024 15:10:08 -0500
Subject: [PATCH 5/9] [FSSDK-10848] Update Actions tool versions & add workflow
dispatch (#377)
* ci: bump Actions workflow step versions
* test: add workflow dispatch temporarily for testing
---
.github/workflows/csharp_release.yml | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/csharp_release.yml b/.github/workflows/csharp_release.yml
index cd80b0b20..804611619 100644
--- a/.github/workflows/csharp_release.yml
+++ b/.github/workflows/csharp_release.yml
@@ -3,6 +3,7 @@
on:
release:
types: [ published ] # Trigger on published pre-releases and releases
+ workflow_dispatch:
jobs:
variables:
@@ -33,13 +34,13 @@ jobs:
runs-on: windows-2019 # required version for Framework 4.0
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: ${{ needs.variables.outputs.tag }}
- name: Add msbuild to PATH
- uses: microsoft/setup-msbuild@v1
+ uses: microsoft/setup-msbuild@v2
- name: Setup NuGet
- uses: NuGet/setup-nuget@v1
+ uses: nuget/setup-nuget@v2
- name: Restore NuGet packages
run: nuget restore ./OptimizelySDK.NETFramework.sln
- name: Build and strongly name assemblies
@@ -57,11 +58,11 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: ${{ needs.variables.outputs.tag }}
- name: Setup .NET
- uses: actions/setup-dotnet@v2
+ uses: actions/setup-dotnet@v4
- name: Restore dependencies
run: dotnet restore OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
- name: Build and strongly name assemblies
@@ -79,11 +80,11 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: ${{ needs.variables.outputs.tag }}
- name: Setup .NET
- uses: actions/setup-dotnet@v2
+ uses: actions/setup-dotnet@v4
- name: Restore dependencies
run: dotnet restore OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
- name: Build and strongly name Standard 2.0 project
From 10b8af39c9235338608fa077e9a6c8cbc5cac5cc Mon Sep 17 00:00:00 2001
From: alexjoeyyong <96444887+alexjoeyyong@users.noreply.github.com>
Date: Wed, 22 Jan 2025 12:05:02 -0500
Subject: [PATCH 6/9] EC3-1687 Update sonarqube.yml (#378)
---
.github/workflows/sonarqube.yml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index b60e311dd..54ba81652 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -9,21 +9,21 @@ jobs:
runs-on: windows-latest
steps:
- name: Set up JDK 11
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v4
with:
java-version: 1.11
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Cache SonarCloud packages
- uses: actions/cache@v1
+ uses: actions/cache@v4
with:
path: ~\sonar\cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
- uses: actions/cache@v1
+ uses: actions/cache@v4
with:
path: .\.sonar\scanner
key: ${{ runner.os }}-sonar-scanner
From 666abad0dd6c3df350ed6613f3cc8d59aca224d3 Mon Sep 17 00:00:00 2001
From: Jae Kim <45045038+jaeopt@users.noreply.github.com>
Date: Wed, 5 Mar 2025 09:45:10 -0800
Subject: [PATCH 7/9] [FSSDK-11077] clean up travis (#379)
---
.github/workflows/csharp.yml | 6 ++----
.github/workflows/integration_test.yml | 11 +++--------
OptimizelySDK.DemoApp/Scripts/README.md | 3 +--
README.md | 2 +-
4 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml
index b55407af7..3e4e0b693 100644
--- a/.github/workflows/csharp.yml
+++ b/.github/workflows/csharp.yml
@@ -103,17 +103,15 @@ jobs:
integration_tests:
name: Run Integration Tests
needs: [ netFrameworksAndUnitTest, netStandard16, netStandard20 ]
- uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@master
+ uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@jae/FSSDK-11077
secrets:
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
- TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
fullstack_production_suite:
name: Run Performance Tests
needs: [ netFrameworksAndUnitTest, netStandard16, netStandard20 ]
- uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@master
+ uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@jae/FSSDK-11077
with:
FULLSTACK_TEST_REPO: ProdTesting
secrets:
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
- TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml
index cf9a96b37..b56cc8817 100644
--- a/.github/workflows/integration_test.yml
+++ b/.github/workflows/integration_test.yml
@@ -9,8 +9,6 @@ on:
secrets:
CI_USER_TOKEN:
required: true
- TRAVIS_COM_TOKEN:
- required: true
jobs:
test:
runs-on: ubuntu-latest
@@ -19,8 +17,8 @@ jobs:
with:
# You should create a personal access token and store it in your repository
token: ${{ secrets.CI_USER_TOKEN }}
- repository: 'optimizely/travisci-tools'
- path: 'home/runner/travisci-tools'
+ repository: 'optimizely/ci-helper-tools'
+ path: 'home/runner/ci-helper-tools'
ref: 'master'
- name: set SDK Branch if PR
env:
@@ -28,14 +26,12 @@ jobs:
if: ${{ github.event_name == 'pull_request' }}
run: |
echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV
- echo "TRAVIS_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=$REF_NAME" >> $GITHUB_ENV
- echo "TRAVIS_BRANCH=$REF_NAME" >> $GITHUB_ENV
- name: Trigger build
env:
SDK: csharp
@@ -51,9 +47,8 @@ jobs:
PULL_REQUEST_SHA: ${{ github.event.pull_request.head.sha }}
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
UPSTREAM_SHA: ${{ github.sha }}
- TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
EVENT_MESSAGE: ${{ github.event.message }}
HOME: 'home/runner'
run: |
echo "$GITHUB_CONTEXT"
- home/runner/travisci-tools/trigger-script-with-status-update.sh
+ home/runner/ci-helper-tools/trigger-script-with-status-update.sh
diff --git a/OptimizelySDK.DemoApp/Scripts/README.md b/OptimizelySDK.DemoApp/Scripts/README.md
index ff1e9551c..12510e26f 100644
--- a/OptimizelySDK.DemoApp/Scripts/README.md
+++ b/OptimizelySDK.DemoApp/Scripts/README.md
@@ -7,7 +7,6 @@
-
@@ -34,7 +33,7 @@ to make it possible to position it near a given reference element.
The engine is completely modular and most of its features are implemented as **modifiers**
(similar to middlewares or plugins).
-The whole code base is written in ES2015 and its features are automatically tested on real browsers thanks to [SauceLabs](https://saucelabs.com/) and [TravisCI](https://travis-ci.org/).
+The whole code base is written in ES2015 and its features are automatically tested on real browsers thanks to [SauceLabs](https://saucelabs.com/).
Popper.js has zero dependencies. No jQuery, no LoDash, nothing.
It's used by big companies like [Twitter in Bootstrap v4](https://getbootstrap.com/), [Microsoft in WebClipper](https://github.com/OneNoteDev/WebClipper) and [Atlassian in AtlasKit](https://aui-cdn.atlassian.com/atlaskit/registry/).
diff --git a/README.md b/README.md
index f6b04d5c9..9de4ddc76 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Optimizely C# SDK

-[](https://travis-ci.org/optimizely/csharp-sdk)
+
[](https://www.nuget.org/packages/Optimizely.SDK/)
[](http://www.apache.org/licenses/LICENSE-2.0)
From 282b7cf7577d1adf2fedaad33625dab34aea5fa6 Mon Sep 17 00:00:00 2001
From: Jae Kim <45045038+jaeopt@users.noreply.github.com>
Date: Wed, 5 Mar 2025 13:02:26 -0800
Subject: [PATCH 8/9] restore master workflow (#380)
---
.github/workflows/csharp.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml
index 3e4e0b693..0205baa81 100644
--- a/.github/workflows/csharp.yml
+++ b/.github/workflows/csharp.yml
@@ -103,14 +103,14 @@ jobs:
integration_tests:
name: Run Integration Tests
needs: [ netFrameworksAndUnitTest, netStandard16, netStandard20 ]
- uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@jae/FSSDK-11077
+ uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@master
secrets:
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
fullstack_production_suite:
name: Run Performance Tests
needs: [ netFrameworksAndUnitTest, netStandard16, netStandard20 ]
- uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@jae/FSSDK-11077
+ uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@master
with:
FULLSTACK_TEST_REPO: ProdTesting
secrets:
From c9d06cdb0b21f455508f4e74f548758aa44b8625 Mon Sep 17 00:00:00 2001
From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com>
Date: Wed, 18 Jun 2025 23:08:18 +0600
Subject: [PATCH 9/9] [FSSDK-11450] experimentId + variationId to decision
notification listener (#381)
---
.github/workflows/csharp.yml | 4 +-
OptimizelySDK.Tests/OptimizelyTest.cs | 91 ++++++++++---------
.../OptimizelyUserContextTest.cs | 6 ++
OptimizelySDK/Optimizely.cs | 11 ++-
4 files changed, 63 insertions(+), 49 deletions(-)
diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml
index 0205baa81..1650a6850 100644
--- a/.github/workflows/csharp.yml
+++ b/.github/workflows/csharp.yml
@@ -75,7 +75,7 @@ jobs:
- name: Restore dependencies
run: dotnet restore OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
- name: Build & strongly name assemblies
- run: dotnet build OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=D:\a\csharp-sdk\csharp-sdk\keypair.snk -c Release
+ run: dotnet build OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk -c Release
netStandard20:
name: Build Standard 2.0
@@ -98,7 +98,7 @@ jobs:
- name: Restore dependencies
run: dotnet restore OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
- name: Build & strongly name assemblies
- run: dotnet build OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=D:\a\csharp-sdk\csharp-sdk\keypair.snk -c Release
+ run: dotnet build OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk -c Release
integration_tests:
name: Run Integration Tests
diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs
index 5dab3aec6..0adb57ec5 100644
--- a/OptimizelySDK.Tests/OptimizelyTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyTest.cs
@@ -339,6 +339,12 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsFalseAndFeature()
{
"decisionEventDispatched", true
},
+ {
+ "experimentId", "7718750065"
+ },
+ {
+ "variationId", "7713030086"
+ }
}))), Times.Once);
EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()),
Times.Once);
@@ -405,6 +411,12 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsTrueAndFeature()
{
"decisionEventDispatched", true
},
+ {
+ "experimentId", "7718750065"
+ },
+ {
+ "variationId", "7713030086"
+ }
}))), Times.Once);
EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()),
Times.Once);
@@ -476,6 +488,12 @@ public void TestDecisionNotificationNotSentWhenSendFlagDecisionsFalseAndRollout(
{
"decisionEventDispatched", false
},
+ {
+ "experimentId", experiment.Id
+ },
+ {
+ "variationId", variation.Id
+ }
}))), Times.Once);
EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()),
Times.Never);
@@ -547,6 +565,12 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsTrueAndRollout()
{
"decisionEventDispatched", true
},
+ {
+ "experimentId", experiment.Id
+ },
+ {
+ "variationId", variation.Id
+ }
}))), Times.Once);
EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()),
Times.Once);
@@ -2361,8 +2385,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Got variable value ""{variableValue}"" for variable ""{variableKey
- }"" of feature flag ""{featureKey}""."));
+ $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."));
}
[Test]
@@ -2406,8 +2429,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Got variable value ""{variableValue}"" for variable ""{variableKey
- }"" of feature flag ""{featureKey}""."));
+ $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."));
}
[Test]
@@ -2439,8 +2461,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Feature ""{featureKey}"" is not enabled for user {TestUserId
- }. Returning the default variable value ""{variableValue}""."));
+ $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}""."));
}
[Test]
@@ -2484,8 +2505,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Feature ""{featureKey}"" is not enabled for user {TestUserId
- }. Returning the default variable value ""{variableValue}""."));
+ $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}""."));
}
[Test]
@@ -2515,8 +2535,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Got variable value ""true"" for variable ""{variableKey}"" of feature flag ""{
- featureKey}""."));
+ $@"Got variable value ""true"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."));
}
[Test]
@@ -2562,8 +2581,7 @@ public void
Assert.AreEqual(expectedStringValue, variableValue.GetValue("string_var"));
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Got variable value ""{variableValue}"" for variable ""{variableKey
- }"" of feature flag ""{featureKey}""."));
+ $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."));
}
[Test]
@@ -2609,8 +2627,7 @@ public void
Assert.AreEqual(expectedStringValue, variableValue.GetValue("string_var"));
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Got variable value ""{variableValue}"" for variable ""{variableKey
- }"" of feature flag ""{featureKey}""."));
+ $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."));
}
[Test]
@@ -2654,8 +2671,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Got variable value ""{variableValue}"" for variable ""{variableKey
- }"" of feature flag ""{featureKey}""."));
+ $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."));
}
[Test]
@@ -2684,8 +2700,7 @@ public void
variableKey, TestUserId, null);
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Feature ""{featureKey}"" is not enabled for user {TestUserId
- }. Returning the default variable value ""true""."));
+ $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""true""."));
}
[Test]
@@ -2728,8 +2743,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Feature ""{featureKey}"" is not enabled for user {TestUserId
- }. Returning the default variable value ""{variableValue}""."));
+ $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}""."));
}
[Test]
@@ -2758,8 +2772,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"User ""{TestUserId}"" is not in any variation for feature flag ""{featureKey
- }"", returning default value ""{variableValue}""."));
+ $@"User ""{TestUserId}"" is not in any variation for feature flag ""{featureKey}"", returning default value ""{variableValue}""."));
}
#endregion Feature Toggle Tests
@@ -2822,8 +2835,7 @@ public void TestGetFeatureVariableValueForTypeGivenFeatureKeyOrVariableKeyNotFou
LoggerMock.Verify(l =>
l.Log(LogLevel.ERROR, $@"Feature key ""{featureKey}"" is not in datafile."));
LoggerMock.Verify(l => l.Log(LogLevel.ERROR,
- $@"No feature variable was found for key ""{variableKey
- }"" in feature flag ""double_single_variable_feature""."));
+ $@"No feature variable was found for key ""{variableKey}"" in feature flag ""double_single_variable_feature""."));
}
// Should return null and log error message when variable type is invalid.
@@ -2851,17 +2863,13 @@ public void TestGetFeatureVariableValueForTypeGivenInvalidVariableType()
"string_single_variable_feature", "json_var", TestUserId, null, variableTypeInt));
LoggerMock.Verify(l => l.Log(LogLevel.ERROR,
- $@"Variable is of type ""double"", but you requested it as type ""{variableTypeBool
- }""."));
+ $@"Variable is of type ""double"", but you requested it as type ""{variableTypeBool}""."));
LoggerMock.Verify(l => l.Log(LogLevel.ERROR,
- $@"Variable is of type ""boolean"", but you requested it as type ""{
- variableTypeDouble}""."));
+ $@"Variable is of type ""boolean"", but you requested it as type ""{variableTypeDouble}""."));
LoggerMock.Verify(l => l.Log(LogLevel.ERROR,
- $@"Variable is of type ""integer"", but you requested it as type ""{
- variableTypeString}""."));
+ $@"Variable is of type ""integer"", but you requested it as type ""{variableTypeString}""."));
LoggerMock.Verify(l => l.Log(LogLevel.ERROR,
- $@"Variable is of type ""string"", but you requested it as type ""{variableTypeInt
- }""."));
+ $@"Variable is of type ""string"", but you requested it as type ""{variableTypeInt}""."));
}
[Test]
@@ -2913,8 +2921,7 @@ public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUse
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Feature ""{featureKey}"" is not enabled for user {TestUserId
- }. Returning the default variable value ""{variableValue}""."));
+ $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}""."));
}
// Should return default value and log message when feature is enabled for the user
@@ -2954,9 +2961,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Variable ""{variableKey
- }"" is not used in variation ""control"", returning default value ""{expectedValue
- }""."));
+ $@"Variable ""{variableKey}"" is not used in variation ""control"", returning default value ""{expectedValue}""."));
}
// Should return variable value from variation and log message when feature is enabled for the user
@@ -2994,8 +2999,7 @@ public void
Assert.AreEqual(expectedValue, variableValue);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"Got variable value ""{variableValue}"" for variable ""{variableKey
- }"" of feature flag ""{featureKey}""."));
+ $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."));
}
// Verify that GetFeatureVariableValueForType returns correct variable value for rollout rule.
@@ -3149,8 +3153,7 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperi
// SendImpressionEvent() does not get called.
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey
- }""."), Times.Once);
+ $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey}""."), Times.Once);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
$@"Feature flag ""{featureKey}"" is enabled for user ""{TestUserId}""."));
@@ -3183,8 +3186,7 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimen
// SendImpressionEvent() gets called.
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey
- }""."), Times.Never);
+ $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey}""."), Times.Never);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
$@"Feature flag ""{featureKey}"" is enabled for user ""{TestUserId}""."));
@@ -3218,8 +3220,7 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsNotEnabledAndUserIsBeingExperi
// SendImpressionEvent() gets called.
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
- $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey
- }""."), Times.Never);
+ $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey}""."), Times.Never);
LoggerMock.Verify(l => l.Log(LogLevel.INFO,
$@"Feature flag ""{featureKey}"" is not enabled for user ""{TestUserId}""."));
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index 76d0d8b8b..21ae10db8 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -1081,6 +1081,12 @@ public void TestDecisionNotification()
{
"decisionEventDispatched", true
},
+ {
+ "experimentId", "122235"
+ },
+ {
+ "variationId", "122236"
+ },
};
var userAttributes = new UserAttributes
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index b1766985c..99cbdaafa 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -1000,7 +1000,8 @@ ProjectConfig projectConfig
)
{
var userId = user.GetUserId();
-
+ string experimentId = null;
+ string variationId = null;
var flagEnabled = false;
if (flagDecision.Variation != null)
{
@@ -1008,8 +1009,12 @@ ProjectConfig projectConfig
{
flagEnabled = true;
}
+ variationId = flagDecision.Variation.Id;
+ }
+ if (flagDecision.Experiment != null)
+ {
+ experimentId = flagDecision.Experiment.Id;
}
-
Logger.Log(LogLevel.INFO,
$"Feature \"{flagKey}\" is enabled for user \"{userId}\"? {flagEnabled}");
@@ -1062,6 +1067,8 @@ ProjectConfig projectConfig
{ "ruleKey", ruleKey },
{ "reasons", reasonsToReport },
{ "decisionEventDispatched", decisionEventDispatched },
+ { "experimentId", experimentId },
+ { "variationId", variationId },
};
NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision,