From 46a509a00b97f597a2cbd7c49fcd87f1f289c0be Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 27 Jun 2023 08:41:18 -0400 Subject: [PATCH 1/6] [FSSDK-9472] fix: Last-Modified header not respected (#355) * Fix where Last-Modified is pulled * Corrected small problems in test class * WIP Adding Last Modified test * Add test coverage WIP: new tests succeed in isolation (time-based/brittle) * Fix failing test by deferring * Lint fixes * Lint fix whitespace (cherry picked from commit 471ca4bab4dd5d3fda30b4fcd3f6965d2cb1c417) --- .../HttpProjectConfigManagerTest.cs | 73 +++++++++++++++++-- .../Utils/TestHttpProjectConfigManagerUtil.cs | 48 +++++++++--- .../Config/HttpProjectConfigManager.cs | 49 +++++++++---- 3 files changed, 135 insertions(+), 35 deletions(-) diff --git a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs index 9a3bbb87..f6c3d5a3 100644 --- a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs +++ b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021, Optimizely + * Copyright 2019-2021, 2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ public void Setup() } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigByURL() + public void TestHttpConfigManagerRetrieveProjectConfigByURL() { var t = MockSendAsync(TestData.Datafile); HttpProjectConfigManager httpManager = new HttpProjectConfigManager.Builder() @@ -89,6 +89,60 @@ public void TestHttpConfigManagerWithInvalidStatus() httpManager.Dispose(); } + [Test] + public void TestSettingIfModifiedSinceInRequestHeader() + { + var t = MockSendAsync( + datafile: string.Empty, + statusCode: HttpStatusCode.NotModified, + responseContentHeaders: new Dictionary + { + { "Last-Modified", new DateTime(2050, 10, 10).ToString("R") }, + } + ); + + var httpManager = new HttpProjectConfigManager.Builder() + .WithDatafile(string.Empty) + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(2000)) + .WithStartByDefault() + .Build(defer: true); + httpManager.LastModifiedSince = new DateTime(2020, 4, 4).ToString("R"); + t.Wait(3000); + + LoggerMock.Verify( + _ => _.Log(LogLevel.DEBUG, "Set If-Modified-Since in request header."), + Times.AtLeastOnce); + + httpManager.Dispose(); + } + + [Test] + public void TestSettingLastModifiedFromResponseHeader() + { + MockSendAsync( + statusCode: HttpStatusCode.OK, + responseContentHeaders: new Dictionary + { + { "Last-Modified", new DateTime(2050, 10, 10).ToString("R") }, + } + ); + var httpManager = new HttpProjectConfigManager.Builder() + .WithUrl("https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); + + LoggerMock.Verify( + _ => _.Log(LogLevel.DEBUG, "Set LastModifiedSince from response header."), + Times.AtLeastOnce); + + httpManager.Dispose(); + } + [Test] public void TestHttpClientHandler() { @@ -97,7 +151,7 @@ public void TestHttpClientHandler() } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigGivenEmptyFormatUseDefaultFormat() + public void TestHttpConfigManagerRetrieveProjectConfigGivenEmptyFormatUseDefaultFormat() { var t = MockSendAsync(TestData.Datafile); @@ -121,7 +175,7 @@ public void TestHttpConfigManagerRetreiveProjectConfigGivenEmptyFormatUseDefault } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigBySDKKey() + public void TestHttpConfigManagerRetrieveProjectConfigBySDKKey() { var t = MockSendAsync(TestData.Datafile); @@ -143,7 +197,7 @@ public void TestHttpConfigManagerRetreiveProjectConfigBySDKKey() } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigByFormat() + public void TestHttpConfigManagerRetrieveProjectConfigByFormat() { var t = MockSendAsync(TestData.Datafile); @@ -509,10 +563,15 @@ public void TestFormatUrlHigherPriorityThanDefaultUrl() } - public Task MockSendAsync(string datafile = null, TimeSpan? delay = null, HttpStatusCode statusCode = HttpStatusCode.OK) + private Task MockSendAsync(string datafile = null, TimeSpan? delay = null, + HttpStatusCode statusCode = HttpStatusCode.OK, + Dictionary responseContentHeaders = null + ) { - return TestHttpProjectConfigManagerUtil.MockSendAsync(HttpClientMock, datafile, delay, statusCode); + return TestHttpProjectConfigManagerUtil.MockSendAsync(HttpClientMock, datafile, delay, + statusCode, responseContentHeaders); } + #endregion } } diff --git a/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs b/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs index 19e854c8..4db77c84 100644 --- a/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs +++ b/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs @@ -1,6 +1,5 @@ - /* - * Copyright 2019-2020, Optimizely + * Copyright 2019-2020, 2023 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 +15,7 @@ */ using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -29,21 +29,42 @@ namespace OptimizelySDK.Tests.Utils /// public static class TestHttpProjectConfigManagerUtil { - public static Task MockSendAsync(Mock HttpClientMock, string datafile = null, TimeSpan? delay=null, HttpStatusCode statusCode = HttpStatusCode.OK) + public static Task MockSendAsync(Mock httpClientMock, + string datafile = null, TimeSpan? delay = null, + HttpStatusCode statusCode = HttpStatusCode.OK, + Dictionary responseContentHeaders = null + ) { - var t = new System.Threading.Tasks.TaskCompletionSource(); + var t = new TaskCompletionSource(); - HttpClientMock.Setup(_ => _.SendAsync(It.IsAny())) - .Returns(() => { - if (delay != null) { + httpClientMock.Setup(_ => _.SendAsync(It.IsAny())) + .Returns(() => + { + if (delay != null) + { // This delay mocks the networking delay. And help to see the behavior when get a datafile with some delay. Task.Delay(delay.Value).Wait(); } - - return System.Threading.Tasks.Task.FromResult(new HttpResponseMessage { StatusCode = statusCode, Content = new StringContent(datafile ?? string.Empty) }); + + var responseMessage = new HttpResponseMessage + { + StatusCode = statusCode, + Content = new StringContent(datafile ?? string.Empty), + }; + + if (responseContentHeaders != null) + { + foreach (var header in responseContentHeaders) + { + responseMessage.Content.Headers.Add(header.Key, header.Value); + } + } + + return Task.FromResult(responseMessage); }) .Callback(() - => { + => + { t.SetResult(true); }); @@ -60,7 +81,10 @@ public static void SetClientFieldValue(object value) var field = type.GetField("Client", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); - field.SetValue(new object(), value); - } + if (field != null) + { + field.SetValue(new object(), value); + } + } } } diff --git a/OptimizelySDK/Config/HttpProjectConfigManager.cs b/OptimizelySDK/Config/HttpProjectConfigManager.cs index 8b909755..22e9d593 100644 --- a/OptimizelySDK/Config/HttpProjectConfigManager.cs +++ b/OptimizelySDK/Config/HttpProjectConfigManager.cs @@ -1,11 +1,11 @@ /* - * Copyright 2019-2021, Optimizely + * Copyright 2019-2021, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,16 +18,19 @@ using OptimizelySDK.Logger; using OptimizelySDK.Notifications; using System; -using System.Collections.Generic; +using System.Net; using System.Linq; using System.Threading.Tasks; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Logger; +using OptimizelySDK.Notifications; namespace OptimizelySDK.Config { public class HttpProjectConfigManager : PollingProjectConfigManager { private string Url; - private string LastModifiedSince = string.Empty; + internal string LastModifiedSince = string.Empty; private string DatafileAccessToken = string.Empty; private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler) : base(period, blockingTimeout, autoUpdate, logger, errorHandler) @@ -84,45 +87,59 @@ public static System.Net.Http.HttpClientHandler GetHttpClientHandler() static HttpProjectConfigManager() { Client = new HttpClient(); - } + } private string GetRemoteDatafileResponse() { Logger.Log(LogLevel.DEBUG, $"Making datafile request to url \"{Url}\""); - var request = new System.Net.Http.HttpRequestMessage { + var request = new System.Net.Http.HttpRequestMessage + { RequestUri = new Uri(Url), Method = System.Net.Http.HttpMethod.Get, }; // Send If-Modified-Since header if Last-Modified-Since header contains any value. if (!string.IsNullOrEmpty(LastModifiedSince)) + { request.Headers.Add("If-Modified-Since", LastModifiedSince); + Logger.Log(LogLevel.DEBUG, $"Set If-Modified-Since in request header."); + } - if (!string.IsNullOrEmpty(DatafileAccessToken)) { - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DatafileAccessToken); + if (!string.IsNullOrEmpty(DatafileAccessToken)) + { + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", + DatafileAccessToken); } - var httpResponse = Client.SendAsync(request); + var httpResponse = Client.SendAsync(request); httpResponse.Wait(); // Return from here if datafile is not modified. var result = httpResponse.Result; - if (!result.IsSuccessStatusCode) { + + if (result.StatusCode == HttpStatusCode.NotModified) + { + return null; + } + + if (!result.IsSuccessStatusCode) + { Logger.Log(LogLevel.ERROR, $"Error fetching datafile \"{result.StatusCode}\""); return null; } // Update Last-Modified header if provided. - if (result.Headers.TryGetValues("Last-Modified", out IEnumerable values)) - LastModifiedSince = values.First(); - - if (result.StatusCode == System.Net.HttpStatusCode.NotModified) - return null; + if (result.Content.Headers.LastModified.HasValue) + { + LastModifiedSince = result.Content.Headers.LastModified.ToString(); + Logger.Log(LogLevel.DEBUG, $"Set LastModifiedSince from response header."); + } var content = result.Content.ReadAsStringAsync(); content.Wait(); - return content.Result; + return content.Result; } #elif NET40 private string GetRemoteDatafileResponse() From 7d4c00efd435bd641f35628e455b96dd8ed39072 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 3 Jul 2023 13:49:22 -0400 Subject: [PATCH 2/6] Fixed follow-on merge issues --- .../ConfigTest/HttpProjectConfigManagerTest.cs | 1 + OptimizelySDK/Properties/AssemblyInfo.cs | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs index f6c3d5a3..28e74b59 100644 --- a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs +++ b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs @@ -21,6 +21,7 @@ using OptimizelySDK.Tests.NotificationTests; using OptimizelySDK.Tests.Utils; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Http; diff --git a/OptimizelySDK/Properties/AssemblyInfo.cs b/OptimizelySDK/Properties/AssemblyInfo.cs index b8b83851..d32e281e 100644 --- a/OptimizelySDK/Properties/AssemblyInfo.cs +++ b/OptimizelySDK/Properties/AssemblyInfo.cs @@ -22,9 +22,14 @@ // Make types and members with internal scope visible to friend // OptimizelySDK.Tests unit tests. #pragma warning disable 1700 -[assembly: InternalsVisibleTo("OptimizelySDK.Tests, PublicKey=ThePublicKey")] +#if DEBUG +[assembly: InternalsVisibleTo("OptimizelySDK.Tests")] +#else +[assembly: InternalsVisibleTo("OptimizelySDK.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006b0705c5f697a2522639be5d5bc02835aaef2e2cd4adf47c3bbf5ed97187298c17448701597b5a610d29eed362f36f056062bbccd424fc830dd5966a9378302c61e3ddd77effcd9dcfaf739f3ca88149e961f55f23d5ce1948703da33e261f6cc0c681a19ce62ccbfdeca8bd286f93395e4f67e4a2ea7782af581062edab8083")] +#endif #pragma warning restore 1700 + // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("4dde7faa-110d-441c-ab3b-3f31b593e8bf")] From bc0000d6b3ac9128f652676ac88d3909b7b255d6 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 10 Jul 2023 10:49:03 -0400 Subject: [PATCH 3/6] [FSSDK-9486] maint: Update CI and publishing (#356) * Add remote dispatch workflow * Update job & step names * Stop uploading to AWS * Reorganized jobs * Change workflow names * Fix on.push.branches for testing * Rename job * Rename steps; remove second strong name signing for .NET Framework assems * Combine two steps * Run tests before release build * NIT changes * Move NUnit tests after build * Remove testing branch push trigger * Renamings; remove test trigger * Rename jobs for consistency * Revert "Rename jobs for consistency" This reverts commit c15953823fcf06f197708fe5403384c1dc880efd. * Update from @jaeopt PR review * Add back CI_USER_TOKEN secret * Add back TRAVIS_COM_TOKEN * Update release workflow for testing * Fix test tag * Testing fix use OptimizelySDK.Travis.sln since I'm testing using previous release * Adjust names * Migrate nuspec template * Fix checkout during pack; output tag & version * Fix output of env.TAG * Shorten & fix during testing * Add back jobs * Update OptimizelySDK.nuspec.template's permission * Iterate on nuspec creation * Fix semantic extraction * Fix dotnet nuget push * Move env to steps where they're needed * Remove testing setups (cherry picked from commit b6583235191a720b7a03ae6826b4c25709e91a38) --- .github/workflows/csharp.yml | 112 ++++------ .github/workflows/csharp_release.yml | 202 ++++++++++++++++++ ...avis.sln => OptimizelySDK.NETFramework.sln | 0 OptimizelySDK.Package/pack.ps1 | 39 ---- OptimizelySDK.Package/strongname.sh | 44 ---- OptimizelySDK.Package/verifysn.ps1 | 37 ---- ...DK.nuspec => OptimizelySDK.nuspec.template | 21 +- 7 files changed, 253 insertions(+), 202 deletions(-) create mode 100644 .github/workflows/csharp_release.yml rename OptimizelySDK.Travis.sln => OptimizelySDK.NETFramework.sln (100%) delete mode 100644 OptimizelySDK.Package/pack.ps1 delete mode 100644 OptimizelySDK.Package/strongname.sh delete mode 100644 OptimizelySDK.Package/verifysn.ps1 rename OptimizelySDK.Package/OptimizelySDK.nuspec => OptimizelySDK.nuspec.template (81%) diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 15c2e269..f096db7f 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -1,18 +1,13 @@ ---- -name: Csharp CI with .NET +name: Continuous Integration on: push: - branches: [3.11.2] + branches: [ release-3.11.3 ] pull_request: - branches: [3.11.2] - -env: - RELEASE_BRANCH: "3.11.2" - WINDOWS_2019_SN_PATH: C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\sn.exe + branches: [ release-3.11.3 ] jobs: - lint_code_base: + lintCodebase: runs-on: ubuntu-latest name: Lint Codebase steps: @@ -21,7 +16,7 @@ jobs: with: # Full git history is needed to get a proper list of changed files fetch-depth: 0 - - name: Lint codebase + - name: Run Super-Linter uses: github/super-linter@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -29,24 +24,9 @@ jobs: DEFAULT_BRANCH: master VALIDATE_CSHARP: true - integration_tests: - name: Run Integration Tests - uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@master - secrets: - CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }} - TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} - - fullstack_production_suite: - name: Run Optimizely Feature Experimentation Compatibility Suite - uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@master - with: - FULLSTACK_TEST_REPO: ProdTesting - secrets: - CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }} - TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} - - unit_test: - name: Build and Run Unit Tests + netFrameworksAndUnitTest: + name: Build Framework & Run Unit Tests + needs: [ lintCodebase ] runs-on: windows-2019 # required version for Framework 4.0 env: REPO_SLUG: ${{ github.repository }} @@ -63,33 +43,18 @@ jobs: - name: Setup NuGet uses: NuGet/setup-nuget@v1 - name: Restore NuGet packages - run: nuget restore ./OptimizelySDK.Travis.sln - - name: Build solution - run: msbuild /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk /p:Configuration=Release ./OptimizelySDK.Travis.sln - - name: Install NUnit Console - run: nuget install NUnit.Console -Version 3.15.2 -DirectDownload -OutputDirectory . - - name: Run NUnit tests - # https://docs.nunit.org/articles/nunit/running-tests/Console-Command-Line.html - run: ./NUnit.ConsoleRunner.3.15.2\tools\nunit3-console.exe /timeout 10000 /process Separate ./OptimizelySDK.Tests/bin/Release/OptimizelySDK.Tests.dll - - name: Find and sign all DLLs - id: unit_tests - run: | - Get-ChildItem -Recurse -Exclude '.*Tests.*' -Include 'OptimizelySDK*.dll' | - Where-Object { $_.DirectoryName -match '\\bin\\Release' } | - Foreach-Object { & $env:WINDOWS_2019_SN_PATH -R $_.FullName ./keypair.snk } - - name: Install AWS CLI, deploy to S3 on successful tests & for release - if: steps.unit_tests.outcome == 'success' && env.CURRENT_BRANCH == env.RELEASE_BRANCH && env.EVENT_TYPE == 'push' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.OFTA_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.OFTA_SECRET }} - AWS_DEFAULT_REGION: ${{ secrets.OFTA_REGION }} + run: nuget restore ./OptimizelySDK.NETFramework.sln + - name: Build & strongly name assemblies + run: msbuild /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=$(pwd)/keypair.snk /p:Configuration=Release ./OptimizelySDK.NETFramework.sln + - name: Install & Run NUnit tests run: | - Install-Module -Name AWS.Tools.Installer -Force; - Install-AWSToolsModule AWS.Tools.S3 -Force -CleanUp; - Get-ChildItem -Recurse -Exclude '.*Tests.*' -include 'OptimizelySDK*.dll' | Where-Object { $_.DirectoryName -match '\\bin\\Release' } | Foreach-Object { aws s3 cp $_.FullName s3://optly-fs-travisci-artifacts/${{ env.REPO_SLUG }}/${{ env.BUILD_NUMBER }}/${{ env.RUN_NUMBER }}/${{ env.ATTEMPT_NUM }}/$($_.Name)-unsigned } + nuget install NUnit.Console -Version 3.15.2 -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 netStandard16: - name: Build For .NET Standard 1.6 + name: Build Standard 1.6 + needs: [ netFrameworksAndUnitTest ] runs-on: windows-2022 env: REPO_SLUG: ${{ github.repository }} @@ -107,20 +72,12 @@ jobs: dotnet-version: 3.1.x - name: Restore dependencies run: dotnet restore OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj - - name: Build and sign Standard 1.6 project - id: netStandard16_build + - 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 - - name: Check on success - if: steps.netStandard16_build.outcome == 'success' && env.CURRENT_BRANCH == env.RELEASE_BRANCH && env.EVENT_TYPE == 'push' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.OFTA_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.OFTA_SECRET }} - AWS_DEFAULT_REGION: ${{ secrets.OFTA_REGION }} - run: | - (aws s3 cp ./OptimizelySDK.NetStandard16/bin/Release/netstandard1.6/OptimizelySDK.NetStandard16.dll s3://optly-fs-travisci-artifacts/${{ env.REPO_SLUG }}/${{ env.BUILD_NUMBER }}/${{ env.RUN_NUMBER }}/${{ env.ATTEMPT_NUM }}/OptimizelySDK.NetStandard16.dll-unsigned) netStandard20: - name: Build For .NET Standard 2.0 + name: Build Standard 2.0 + needs: [ netFrameworksAndUnitTest ] runs-on: windows-2022 env: REPO_SLUG: ${{ github.repository }} @@ -138,14 +95,23 @@ jobs: dotnet-version: 3.1.x - name: Restore dependencies run: dotnet restore OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj - - name: Build and sign Standard 2.0 project - id: netStandard20_build + - 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 - - name: Check on success - if: steps.netStandard20_build.outcome == 'success' && env.CURRENT_BRANCH == env.RELEASE_BRANCH && env.EVENT_TYPE == 'push' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.OFTA_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.OFTA_SECRET }} - AWS_DEFAULT_REGION: ${{ secrets.OFTA_REGION }} - run: | - (aws s3 cp ./OptimizelySDK.NetStandard20/bin/Release/netstandard2.0/OptimizelySDK.NetStandard20.dll s3://optly-fs-travisci-artifacts/${{ env.REPO_SLUG }}/${{ env.BUILD_NUMBER }}/${{ env.RUN_NUMBER }}/${{ env.ATTEMPT_NUM }}/OptimizelySDK.NetStandard20.dll-unsigned) + + integration_tests: + name: Run Integration Tests + needs: [ netFrameworksAndUnitTest, netStandard16, netStandard20 ] + uses: optimizely/csharp-sdk/.github/workflows/integration_test.yml@master + 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 + 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/csharp_release.yml b/.github/workflows/csharp_release.yml new file mode 100644 index 00000000..90e680a7 --- /dev/null +++ b/.github/workflows/csharp_release.yml @@ -0,0 +1,202 @@ +name: Publish Release To NuGet + +on: + release: + types: [ published ] # Trigger on published pre-releases and releases + +jobs: + variables: + 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 + 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]*)?)") + if [ -z "${SEMANTIC_VERSION}" ]; then + echo "Tag did not start with a semantic version number (e.g., #.#.#; #.#.#.#; #.#.#.#-beta)" + exit 1 + fi + echo "semantic_version=${SEMANTIC_VERSION}" >> $GITHUB_OUTPUT + - name: Output tag & semantic version + id: outputs + run: | + echo ${{ env.TAG }} + echo ${{ steps.set_version.outputs.semantic_version }} + outputs: + tag: ${{ env.TAG }} + semanticVersion: ${{ steps.set_version.outputs.semantic_version }} + + buildFrameworkVersions: + name: Build Framework versions + needs: [ variables ] + runs-on: windows-2019 # required version for Framework 4.0 + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ needs.variables.outputs.tag }} + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1 + - name: Setup NuGet + uses: NuGet/setup-nuget@v1 + - name: Restore NuGet packages + run: nuget restore ./OptimizelySDK.NETFramework.sln + - 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 + with: + name: nuget-files + if-no-files-found: error + path: ./**/bin/Release/**/Optimizely*.dll + + buildStandard16: + name: Build Standard 1.6 version + needs: [ variables ] + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ needs.variables.outputs.tag }} + - name: Setup .NET + uses: actions/setup-dotnet@v2 + - name: Restore dependencies + run: dotnet restore OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj + - 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 + with: + name: nuget-files + if-no-files-found: error + path: ./**/bin/Release/**/Optimizely*.dll + + buildStandard20: + name: Build Standard 2.0 version + needs: [ variables ] + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ needs.variables.outputs.tag }} + - name: Setup .NET + uses: actions/setup-dotnet@v2 + - name: Restore dependencies + run: dotnet restore OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj + - 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 + with: + name: nuget-files + if-no-files-found: error + path: ./**/bin/Release/**/Optimizely*.dll + + pack: + name: Sign & pack NuGet package + needs: [ variables, buildFrameworkVersions, buildStandard16, buildStandard20 ] + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.variables.outputs.semanticVersion }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ needs.variables.outputs.tag }} + - name: Install mono + run: | + sudo apt update + sudo apt install -y mono-devel + - name: Download NuGet files + uses: actions/download-artifact@v2 + with: + name: nuget-files + path: ./nuget-files + - name: Organize files + run: | + pushd ./nuget-files + # Move all dlls to the root directory + 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 + popd + - name: Create nuspec + # Uses env.VERSION in OptimizelySDK.nuspec.template + run: | + chmod +x ./OptimizelySDK.nuspec.template + ./OptimizelySDK.nuspec.template + - name: Pack NuGet package + run: | + pushd ./nuget + nuget pack OptimizelySDK.nuspec + popd + - name: Upload nupkg artifact + uses: actions/upload-artifact@v2 + with: + name: nuget-package + if-no-files-found: error + path: ./nuget/Optimizely.SDK.${{ env.VERSION }}.nupkg + + publish: + name: Publish package to NuGet + needs: [ variables, pack ] + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.variables.outputs.semanticVersion }} + steps: + - name: Download NuGet files + uses: actions/download-artifact@v2 + with: + name: nuget-package + path: ./nuget + - name: Setup .NET + uses: actions/setup-dotnet@v3 + - 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.Travis.sln b/OptimizelySDK.NETFramework.sln similarity index 100% rename from OptimizelySDK.Travis.sln rename to OptimizelySDK.NETFramework.sln diff --git a/OptimizelySDK.Package/pack.ps1 b/OptimizelySDK.Package/pack.ps1 deleted file mode 100644 index 099c0375..00000000 --- a/OptimizelySDK.Package/pack.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -Write-Host "Packing Optimizely SDK for NuGet" -Write-Host "-" -Write-Host "This script requires VS 2017 MSBuild & NuGet CLI" -Write-Host "-" - -################################################################ -# NuGet lib -################################################################ -New-Item -Path ".\lib\net45" -ItemType "directory" -force -Copy-Item -Path "..\OptimizelySDK\bin\Release\Optimizely*.dll" -Destination ".\lib\net45" -Recurse -force -Copy-Item -Path "..\OptimizelySDK\bin\Release\Optimizely*.pdb" -Destination ".\lib\net45" -Recurse -force -Copy-Item -Path "..\OptimizelySDK\bin\Release\Optimizely*.xml" -Destination ".\lib\net45" -Recurse -force - -New-Item -Path ".\lib\net40" -ItemType "directory" -force -Copy-Item -Path "..\OptimizelySDK.Net40\bin\Release\Optimizely*.dll" -Destination ".\lib\net40" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.Net40\bin\Release\Optimizely*.pdb" -Destination ".\lib\net40" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.Net40\bin\Release\Optimizely*.xml" -Destination ".\lib\net40" -Recurse -force - -New-Item -Path ".\lib\net35" -ItemType "directory" -force -Copy-Item -Path "..\OptimizelySDK.Net35\bin\Release\Optimizely*.dll" -Destination ".\lib\net35" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.Net35\bin\Release\Optimizely*.pdb" -Destination ".\lib\net35" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.Net35\bin\Release\Optimizely*.xml" -Destination ".\lib\net35" -Recurse -force - -New-Item -Path ".\lib\netstandard1.6" -ItemType "directory" -force -Copy-Item -Path "..\OptimizelySDK.NetStandard16\bin\Release\netstandard1.6\Optimizely*.dll" -Destination ".\lib\netstandard1.6" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.NetStandard16\bin\Release\netstandard1.6\Optimizely*.pdb" -Destination ".\lib\netstandard1.6" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.NetStandard16\bin\Release\netstandard1.6\Optimizely*.xml" -Destination ".\lib\netstandard1.6" -Recurse -force - -New-Item -Path ".\lib\netstandard2.0" -ItemType "directory" -force -Copy-Item -Path "..\OptimizelySDK.NetStandard20\bin\Release\netstandard2.0\Optimizely*.dll" -Destination ".\lib\netstandard2.0" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.NetStandard20\bin\Release\netstandard2.0\Optimizely*.pdb" -Destination ".\lib\netstandard2.0" -Recurse -force -Copy-Item -Path "..\OptimizelySDK.NetStandard20\bin\Release\netstandard2.0\Optimizely*.xml" -Destination ".\lib\netstandard2.0" -Recurse -force - -################################################################ -# Creating NuGet package -################################################################ -Write-Host "-" -Write-Host "Creating NuGet package" -nuget pack OptimizelySDK.nuspec diff --git a/OptimizelySDK.Package/strongname.sh b/OptimizelySDK.Package/strongname.sh deleted file mode 100644 index 6ced74de..00000000 --- a/OptimizelySDK.Package/strongname.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -################################################################ -# strongname.sh (Strong Naming) -################################################################ -# One can use 'find . -name "Optimizely*.dll" -print | grep "bin/Release" | grep -v ".Tests"' -# to find the Optimizely*.dll that we need this PowerShell script to strongname (sn.exe). - -set -e - -cleanup() { - rm -f "${tempfiles[@]}" -} -trap cleanup 0 - -error() { - local lineno="$1" - local message="$2" - local code="${3:-1}" - if [[ -n "${message}" ]] ; then - echo "Error on line ${lineno}: ${message}; status ${code}" - else - echo "Error on line ${lineno}; status ${code}" - fi - exit "${code}" -} -trap 'error ${LINENO}' ERR - -main() { - if [ "$(uname)" != "Darwin" ]; then - echo "${0} MUST be run on a Mac." - exit 1 - fi - sn -R "../OptimizelySDK/bin/Release/OptimizelySDK.dll" "../keypair.snk" - sn -R "../OptimizelySDK.Net35/bin/Release/OptimizelySDK.Net35.dll" "../keypair.snk" - sn -R "../OptimizelySDK.Net40/bin/Release/OptimizelySDK.Net40.dll" "../keypair.snk" - sn -R "../OptimizelySDK.NetStandard16/bin/Release/netstandard1.6/OptimizelySDK.NetStandard16.dll" "../keypair.snk" - sn -v "../OptimizelySDK/bin/Release/OptimizelySDK.dll" - sn -v "../OptimizelySDK.Net35/bin/Release/OptimizelySDK.Net35.dll" - sn -v "../OptimizelySDK.Net40/bin/Release/OptimizelySDK.Net40.dll" - sn -v "../OptimizelySDK.NetStandard16/bin/Release/netstandard1.6/OptimizelySDK.NetStandard16.dll" - cleanup -} - -main diff --git a/OptimizelySDK.Package/verifysn.ps1 b/OptimizelySDK.Package/verifysn.ps1 deleted file mode 100644 index 2bc3db2d..00000000 --- a/OptimizelySDK.Package/verifysn.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -Write-Host "Verify Strong Naming" -Write-Host "This script requires VS 2017" - -################################################################ -# Locate Tools (*.exe) -################################################################ -if ($PSVersionTable["Platform"] -eq "Unix") { - # Including macOS - $sn="/Library/Frameworks/Mono.framework/Versions/Current/Commands/sn" -} elseif (Test-Path "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.2 Tools\x64") { - $sn="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.2 Tools\x64\sn.exe" -} else { - Write-Host "Unable to locate sn.exe" - Exit 1 -} - -################################################################ -# Locate *.nupkg -################################################################ -Write-Host "Locate *.nupkg" -# Good enough for 3.0.0 -$nupkg="./Optimizely.SDK.3.0.0.nupkg" - -################################################################ -# Unzipping *.nupkg -################################################################ -Write-Host "Unzipping *.nupkg" -New-Item -Path "./VerifySn" -ItemType "directory" -force -Expand-Archive -Path $nupkg -DestinationPath "./VerifySn" - -################################################################ -# Verify Strong Names (sn.exe) -################################################################ -& $sn -v "./VerifySn/lib/net35/OptimizelySDK.Net35.dll" -& $sn -v "./VerifySn/lib/net40/OptimizelySDK.Net40.dll" -& $sn -v "./VerifySn/lib/net45/OptimizelySDK.dll" -& $sn -v "./VerifySn/lib/netstandard1.6/OptimizelySDK.NetStandard16.dll" diff --git a/OptimizelySDK.Package/OptimizelySDK.nuspec b/OptimizelySDK.nuspec.template similarity index 81% rename from OptimizelySDK.Package/OptimizelySDK.nuspec rename to OptimizelySDK.nuspec.template index f9e6c8de..22b321fa 100644 --- a/OptimizelySDK.Package/OptimizelySDK.nuspec +++ b/OptimizelySDK.nuspec.template @@ -1,19 +1,23 @@ +#!/bin/bash + +COPYRIGHT_YEAR=$(date +%Y) + +cat > ./nuget/OptimizelySDK.nuspec < Optimizely.SDK - 3.11.2 + ${VERSION} Optimizely C# SDK Optimizely Development Team fullstack.optimizely - Apache-2.0 + http://www.apache.org/licenses/LICENSE-2.0 https://github.com/optimizely/csharp-sdk - OptimizelySDK.png - https://github.com/optimizely/csharp-sdk/blob/master/OptimizelySDK.png?raw=true + https://github.com/optimizely/csharp-sdk/blob/${RELEASE_BRANCH}/OptimizelySDK.png?raw=true false C# SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts - https://github.com/optimizely/csharp-sdk/blob/3.11.2/CHANGELOG.md - Copyright 2017-2023 + https://github.com/optimizely/csharp-sdk/blob/${RELEASE_BRANCH}/CHANGELOG.md + Copyright 2017-${COPYRIGHT_YEAR} Optimizely @@ -45,8 +49,7 @@ - - - + +EOF From 6585abf289de761a5068421a5cc9811544cdc6d6 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 10 Jul 2023 11:22:15 -0400 Subject: [PATCH 4/6] =?UTF-8?q?Lint=20fixes=20=F0=9F=A5=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpProjectConfigManagerTest.cs | 132 ++-- OptimizelySDK/Config/DatafileProjectConfig.cs | 176 ++++-- .../Config/HttpProjectConfigManager.cs | 111 ++-- OptimizelySDK/Optimizely.cs | 574 ++++++++++++------ OptimizelySDK/Utils/Validator.cs | 30 +- 5 files changed, 702 insertions(+), 321 deletions(-) diff --git a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs index 28e74b59..1c558f49 100644 --- a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs +++ b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs @@ -35,7 +35,9 @@ public class HttpProjectConfigManagerTest { private Mock LoggerMock; private Mock HttpClientMock; - private Mock NotificationCallbackMock = new Mock(); + + private Mock NotificationCallbackMock = + new Mock(); [SetUp] public void Setup() @@ -46,7 +48,6 @@ public void Setup() TestHttpProjectConfigManagerUtil.SetClientFieldValue(HttpClientMock.Object); LoggerMock.Setup(l => l.Log(It.IsAny(), It.IsAny())); NotificationCallbackMock.Setup(nc => nc.TestConfigUpdateCallback()); - } [Test] @@ -67,7 +68,8 @@ public void TestHttpConfigManagerRetrieveProjectConfigByURL() HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" + requestMessage.RequestUri.ToString() == + "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" ))); httpManager.Dispose(); } @@ -85,7 +87,9 @@ public void TestHttpConfigManagerWithInvalidStatus() .WithStartByDefault() .Build(); - LoggerMock.Verify(_ => _.Log(LogLevel.ERROR, $"Error fetching datafile \"{HttpStatusCode.Forbidden}\""), Times.AtLeastOnce); + LoggerMock.Verify( + _ => _.Log(LogLevel.ERROR, + $"Error fetching datafile \"{HttpStatusCode.Forbidden}\""), Times.AtLeastOnce); httpManager.Dispose(); } @@ -98,7 +102,9 @@ public void TestSettingIfModifiedSinceInRequestHeader() statusCode: HttpStatusCode.NotModified, responseContentHeaders: new Dictionary { - { "Last-Modified", new DateTime(2050, 10, 10).ToString("R") }, + { + "Last-Modified", new DateTime(2050, 10, 10).ToString("R") + }, } ); @@ -126,7 +132,9 @@ public void TestSettingLastModifiedFromResponseHeader() statusCode: HttpStatusCode.OK, responseContentHeaders: new Dictionary { - { "Last-Modified", new DateTime(2050, 10, 10).ToString("R") }, + { + "Last-Modified", new DateTime(2050, 10, 10).ToString("R") + }, } ); var httpManager = new HttpProjectConfigManager.Builder() @@ -148,7 +156,9 @@ public void TestSettingLastModifiedFromResponseHeader() public void TestHttpClientHandler() { var httpConfigHandler = HttpProjectConfigManager.HttpClient.GetHttpClientHandler(); - Assert.IsTrue(httpConfigHandler.AutomaticDecompression == (System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip)); + Assert.IsTrue(httpConfigHandler.AutomaticDecompression == + (System.Net.DecompressionMethods.Deflate | + System.Net.DecompressionMethods.GZip)); } [Test] @@ -157,20 +167,21 @@ public void TestHttpConfigManagerRetrieveProjectConfigGivenEmptyFormatUseDefault var t = MockSendAsync(TestData.Datafile); HttpProjectConfigManager httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithFormat("") - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .WithStartByDefault() - .Build(); + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithFormat("") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); // This "Wait" notifies When SendAsync is triggered. // Time is given here to avoid hanging-up in any worst case. t.Wait(1000); HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" + requestMessage.RequestUri.ToString() == + "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" ))); httpManager.Dispose(); } @@ -191,7 +202,8 @@ public void TestHttpConfigManagerRetrieveProjectConfigBySDKKey() t.Wait(1000); HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" + requestMessage.RequestUri.ToString() == + "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" ))); Assert.IsNotNull(httpManager.GetConfig()); httpManager.Dispose(); @@ -214,12 +226,14 @@ public void TestHttpConfigManagerRetrieveProjectConfigByFormat() t.Wait(1000); HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/json/10192104166.json" + requestMessage.RequestUri.ToString() == + "https://cdn.optimizely.com/json/10192104166.json" ))); - + Assert.IsNotNull(httpManager.GetConfig()); - LoggerMock.Verify(_ => _.Log(LogLevel.DEBUG, "Making datafile request to url \"https://cdn.optimizely.com/json/10192104166.json\"")); + LoggerMock.Verify(_ => _.Log(LogLevel.DEBUG, + "Making datafile request to url \"https://cdn.optimizely.com/json/10192104166.json\"")); httpManager.Dispose(); } @@ -240,12 +254,14 @@ public void TestHttpProjectConfigManagerDoesntRaiseExceptionForDefaultErrorHandl t.Wait(1000); HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/json/10192104166.json" + requestMessage.RequestUri.ToString() == + "https://cdn.optimizely.com/json/10192104166.json" ))); var datafileConfig = httpManager.GetConfig(); Assert.IsNotNull(datafileConfig); Assert.IsNull(datafileConfig.GetExperimentFromKey("project_config_not_valid").Key); - LoggerMock.Verify(_ => _.Log(LogLevel.DEBUG, "Making datafile request to url \"https://cdn.optimizely.com/json/10192104166.json\"")); + LoggerMock.Verify(_ => _.Log(LogLevel.DEBUG, + "Making datafile request to url \"https://cdn.optimizely.com/json/10192104166.json\"")); httpManager.Dispose(); } @@ -253,7 +269,8 @@ public void TestHttpProjectConfigManagerDoesntRaiseExceptionForDefaultErrorHandl public void TestOnReadyPromiseResolvedImmediatelyWhenDatafileIsProvided() { // Revision - 42 - var t = MockSendAsync(TestData.SimpleABExperimentsDatafile, TimeSpan.FromMilliseconds(100)); + var t = MockSendAsync(TestData.SimpleABExperimentsDatafile, + TimeSpan.FromMilliseconds(100)); HttpProjectConfigManager httpManager = new HttpProjectConfigManager.Builder() // Revision - 15 @@ -282,7 +299,8 @@ public void TestOnReadyPromiseResolvedImmediatelyWhenDatafileIsProvided() public void TestOnReadyPromiseWaitsForProjectConfigRetrievalWhenDatafileIsNotProvided() { // Revision - 42 - var t = MockSendAsync(TestData.SimpleABExperimentsDatafile, TimeSpan.FromMilliseconds(1000)); + var t = MockSendAsync(TestData.SimpleABExperimentsDatafile, + TimeSpan.FromMilliseconds(1000)); HttpProjectConfigManager httpManager = new HttpProjectConfigManager.Builder() .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") @@ -320,7 +338,7 @@ public void TestHttpConfigManagerDoesNotWaitForTheConfigWhenDeferIsTrue() t.Wait(); // in case deadlock, it will release after 3sec. httpManager.OnReady().Wait(8000); - + HttpClientMock.Verify(_ => _.SendAsync(It.IsAny())); Assert.NotNull(httpManager.GetConfig()); @@ -328,9 +346,10 @@ public void TestHttpConfigManagerDoesNotWaitForTheConfigWhenDeferIsTrue() } #region Notification + [Test] public void TestHttpConfigManagerSendConfigUpdateNotificationWhenProjectConfigGetsUpdated() - { + { var t = MockSendAsync(TestData.Datafile); var httpManager = new HttpProjectConfigManager.Builder() @@ -341,7 +360,8 @@ public void TestHttpConfigManagerSendConfigUpdateNotificationWhenProjectConfigGe .WithStartByDefault(false) .Build(true); - httpManager.NotifyOnProjectConfigUpdate += NotificationCallbackMock.Object.TestConfigUpdateCallback; + httpManager.NotifyOnProjectConfigUpdate += + NotificationCallbackMock.Object.TestConfigUpdateCallback; httpManager.Start(); Assert.NotNull(httpManager.GetConfig()); @@ -352,7 +372,7 @@ public void TestHttpConfigManagerSendConfigUpdateNotificationWhenProjectConfigGe [Test] public void TestHttpConfigManagerDoesNotSendConfigUpdateNotificationWhenDatafileIsProvided() - { + { var t = MockSendAsync(TestData.Datafile, TimeSpan.FromMilliseconds(100)); var httpManager = new HttpProjectConfigManager.Builder() @@ -364,10 +384,12 @@ public void TestHttpConfigManagerDoesNotSendConfigUpdateNotificationWhenDatafile .Build(); - httpManager.NotifyOnProjectConfigUpdate += NotificationCallbackMock.Object.TestConfigUpdateCallback; + httpManager.NotifyOnProjectConfigUpdate += + NotificationCallbackMock.Object.TestConfigUpdateCallback; NotificationCallbackMock.Verify(nc => nc.TestConfigUpdateCallback(), Times.Never); - Assert.NotNull(httpManager.GetConfig()); Assert.NotNull(httpManager.GetConfig()); + Assert.NotNull(httpManager.GetConfig()); + Assert.NotNull(httpManager.GetConfig()); httpManager.Dispose(); } @@ -383,11 +405,15 @@ public void TestDefaultBlockingTimeoutWhileProvidingZero() .WithStartByDefault(true) .Build(true); - var fieldInfo = httpManager.GetType().GetField("BlockingTimeout", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var fieldInfo = httpManager.GetType() + .GetField("BlockingTimeout", + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.NonPublic); var expectedBlockingTimeout = (TimeSpan)fieldInfo.GetValue(httpManager); Assert.AreNotEqual(expectedBlockingTimeout.TotalSeconds, TimeSpan.Zero.TotalSeconds); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"Blocking timeout is not valid, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms")); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + $"Blocking timeout is not valid, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms")); httpManager.Dispose(); } @@ -403,11 +429,13 @@ public void TestDefaultPeriodWhileProvidingZero() .WithStartByDefault(true) .Build(true); - var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var expectedPollingInterval = (TimeSpan)fieldInfo.GetValue(httpManager); Assert.AreNotEqual(expectedPollingInterval.TotalSeconds, TimeSpan.Zero.TotalSeconds); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"Polling interval is not valid for periodic calls, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + $"Polling interval is not valid for periodic calls, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); httpManager.Dispose(); } @@ -423,11 +451,13 @@ public void TestDefaultPeriodWhileProvidingNegative() .WithStartByDefault(true) .Build(true); - var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var expectedPollingInterval = (TimeSpan)fieldInfo.GetValue(httpManager); Assert.AreNotEqual(expectedPollingInterval.TotalSeconds, TimeSpan.Zero.TotalSeconds); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"Polling interval is not valid for periodic calls, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + $"Polling interval is not valid for periodic calls, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); httpManager.Dispose(); } @@ -440,11 +470,13 @@ public void TestDefaultPeriodWhileNotProvidingValue() .WithLogger(LoggerMock.Object) .Build(true); - var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var expectedPollingInterval = (TimeSpan)fieldInfo.GetValue(httpManager); Assert.AreNotEqual(expectedPollingInterval.TotalSeconds, TimeSpan.Zero.TotalSeconds); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + $"No polling interval provided, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); httpManager.Dispose(); } @@ -457,11 +489,15 @@ public void TestDefaultBlockingTimeoutWhileNotProvidingValue() .WithLogger(LoggerMock.Object) .Build(true); - var fieldInfo = httpManager.GetType().GetField("BlockingTimeout", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var fieldInfo = httpManager.GetType() + .GetField("BlockingTimeout", + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.NonPublic); var expectedBlockingTimeout = (TimeSpan)fieldInfo.GetValue(httpManager); Assert.AreNotEqual(expectedBlockingTimeout.TotalSeconds, TimeSpan.Zero.TotalSeconds); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No Blocking timeout provided, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms")); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + $"No Blocking timeout provided, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms")); httpManager.Dispose(); } @@ -474,8 +510,10 @@ public void TestDefaultValuesWhenNotProvided() .WithLogger(LoggerMock.Object) .Build(true); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No Blocking timeout provided, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms")); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + $"No polling interval provided, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + $"No Blocking timeout provided, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms")); httpManager.Dispose(); } @@ -496,7 +534,8 @@ public void TestAuthUrlWhenTokenProvided() HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "https://config.optimizely.com/datafiles/auth/QBw9gFM8oTn7ogY9ANCC1z.json" + requestMessage.RequestUri.ToString() == + "https://config.optimizely.com/datafiles/auth/QBw9gFM8oTn7ogY9ANCC1z.json" ))); httpManager.Dispose(); } @@ -516,7 +555,8 @@ public void TestDefaultUrlWhenTokenNotProvided() t.Wait(2000); HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" + requestMessage.RequestUri.ToString() == + "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json" ))); httpManager.Dispose(); } @@ -538,7 +578,7 @@ public void TestAuthenticationHeaderWhenTokenProvided() HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.Headers.Authorization.ToString() == "Bearer datafile1" + requestMessage.Headers.Authorization.ToString() == "Bearer datafile1" ))); httpManager.Dispose(); } @@ -558,10 +598,10 @@ public void TestFormatUrlHigherPriorityThanDefaultUrl() t.Wait(2000); HttpClientMock.Verify(_ => _.SendAsync( It.Is(requestMessage => - requestMessage.RequestUri.ToString() == "http://customformat/QBw9gFM8oTn7ogY9ANCC1z.json" + requestMessage.RequestUri.ToString() == + "http://customformat/QBw9gFM8oTn7ogY9ANCC1z.json" ))); httpManager.Dispose(); - } private Task MockSendAsync(string datafile = null, TimeSpan? delay = null, diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index b214d554..db284cb6 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022, Optimizely + * Copyright 2019-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,7 +101,8 @@ public enum OPTLYSDKVersion /// /// Supported datafile versions list. /// - private static List SupportedVersions = new List { + private static List SupportedVersions = new List + { OPTLYSDKVersion.V2, OPTLYSDKVersion.V3, OPTLYSDKVersion.V4 @@ -137,7 +138,10 @@ private Dictionary _ExperimentIdMap private Dictionary> _VariationKeyMap = new Dictionary>(); - public Dictionary> VariationKeyMap { get { return _VariationKeyMap; } } + public Dictionary> VariationKeyMap + { + get { return _VariationKeyMap; } + } /// /// Associative array of experiment ID to associative array of variation key to variations @@ -145,7 +149,10 @@ private Dictionary> _VariationKeyMap private Dictionary> _VariationKeyMapByExperimentId = new Dictionary>(); - public Dictionary> VariationKeyMapByExperimentId { get { return _VariationKeyMapByExperimentId; } } + public Dictionary> VariationKeyMapByExperimentId + { + get { return _VariationKeyMapByExperimentId; } + } /// /// Associative array of experiment ID to associative array of variation key to variations @@ -153,7 +160,10 @@ private Dictionary> _VariationKeyMapByExpe private Dictionary> _VariationIdMapByExperimentId = new Dictionary>(); - public Dictionary> VariationKeyIdByExperimentId { get { return _VariationIdMapByExperimentId; } } + public Dictionary> VariationKeyIdByExperimentId + { + get { return _VariationIdMapByExperimentId; } + } /// /// Associative array of experiment key to associative array of variation ID to variations @@ -161,7 +171,10 @@ private Dictionary> _VariationIdMapByExper private Dictionary> _VariationIdMap = new Dictionary>(); - public Dictionary> VariationIdMap { get { return _VariationIdMap; } } + public Dictionary> VariationIdMap + { + get { return _VariationIdMap; } + } /// /// Associative array of event key to Event(s) in the datafile @@ -202,13 +215,19 @@ private Dictionary> _VariationIdMap /// Associative array of experiment IDs that exist in any feature /// for checking that experiment is a feature experiment. /// - private Dictionary> ExperimentFeatureMap = new Dictionary>(); + private Dictionary> ExperimentFeatureMap = + new Dictionary>(); /// /// Associated dictionary of flags to variations key value. /// - private Dictionary> _FlagVariationMap = new Dictionary>(); - public Dictionary> FlagVariationMap { get { return _FlagVariationMap; } } + private Dictionary> _FlagVariationMap = + new Dictionary>(); + + public Dictionary> FlagVariationMap + { + get { return _FlagVariationMap; } + } //========================= Interfaces =========================== @@ -282,22 +301,33 @@ private void Initialize() Rollouts = Rollouts ?? new Rollout[0]; _ExperimentKeyMap = new Dictionary(); - _GroupIdMap = ConfigParser.GenerateMap(entities: Groups, getKey: g => g.Id.ToString(), clone: true); - _ExperimentIdMap = ConfigParser.GenerateMap(entities: Experiments, getKey: e => e.Id, clone: true); - _EventKeyMap = ConfigParser.GenerateMap(entities: Events, getKey: e => e.Key, clone: true); - _AttributeKeyMap = ConfigParser.GenerateMap(entities: Attributes, getKey: a => a.Key, clone: true); - _AudienceIdMap = ConfigParser.GenerateMap(entities: Audiences, getKey: a => a.Id.ToString(), clone: true); - _FeatureKeyMap = ConfigParser.GenerateMap(entities: FeatureFlags, getKey: f => f.Key, clone: true); - _RolloutIdMap = ConfigParser.GenerateMap(entities: Rollouts, getKey: r => r.Id.ToString(), clone: true); + _GroupIdMap = ConfigParser.GenerateMap(entities: Groups, + getKey: g => g.Id.ToString(), clone: true); + _ExperimentIdMap = ConfigParser.GenerateMap(entities: Experiments, + getKey: e => e.Id, clone: true); + _EventKeyMap = + ConfigParser.GenerateMap(entities: Events, getKey: e => e.Key, + clone: true); + _AttributeKeyMap = ConfigParser.GenerateMap(entities: Attributes, + getKey: a => a.Key, clone: true); + _AudienceIdMap = ConfigParser.GenerateMap(entities: Audiences, + getKey: a => a.Id.ToString(), clone: true); + _FeatureKeyMap = ConfigParser.GenerateMap(entities: FeatureFlags, + getKey: f => f.Key, clone: true); + _RolloutIdMap = ConfigParser.GenerateMap(entities: Rollouts, + getKey: r => r.Id.ToString(), clone: true); // Overwrite similar items in audience id map with typed audience id map. - var typedAudienceIdMap = ConfigParser.GenerateMap(entities: TypedAudiences, getKey: a => a.Id.ToString(), clone: true); + var typedAudienceIdMap = ConfigParser.GenerateMap(entities: TypedAudiences, + getKey: a => a.Id.ToString(), clone: true); foreach (var item in typedAudienceIdMap) _AudienceIdMap[item.Key] = item.Value; foreach (Group group in Groups) { - var experimentsInGroup = ConfigParser.GenerateMap(group.Experiments, getKey: e => e.Id, clone: true); + var experimentsInGroup = + ConfigParser.GenerateMap(group.Experiments, getKey: e => e.Id, + clone: true); foreach (Experiment experiment in experimentsInGroup.Values) { experiment.GroupId = group.Id; @@ -338,8 +368,10 @@ private void Initialize() { _VariationKeyMap[rolloutRule.Key] = new Dictionary(); _VariationIdMap[rolloutRule.Key] = new Dictionary(); - _VariationIdMapByExperimentId[rolloutRule.Id] = new Dictionary(); - _VariationKeyMapByExperimentId[rolloutRule.Id] = new Dictionary(); + _VariationIdMapByExperimentId[rolloutRule.Id] = + new Dictionary(); + _VariationKeyMapByExperimentId[rolloutRule.Id] = + new Dictionary(); if (rolloutRule.Variations != null) { @@ -347,7 +379,8 @@ private void Initialize() { _VariationKeyMap[rolloutRule.Key][variation.Key] = variation; _VariationIdMap[rolloutRule.Key][variation.Id] = variation; - _VariationKeyMapByExperimentId[rolloutRule.Id][variation.Key] = variation; + _VariationKeyMapByExperimentId[rolloutRule.Id][variation.Key] = + variation; _VariationIdMapByExperimentId[rolloutRule.Id][variation.Id] = variation; } } @@ -361,7 +394,9 @@ private void Initialize() var variationKeyToVariationDict = new Dictionary(); foreach (var experimentId in feature.ExperimentIds ?? new List()) { - foreach (var variationDictKV in ExperimentIdMap[experimentId].VariationKeyToVariationMap) { + foreach (var variationDictKV in ExperimentIdMap[experimentId] + .VariationKeyToVariationMap) + { variationKeyToVariationDict[variationDictKV.Key] = variationDictKV.Value; } @@ -370,19 +405,28 @@ private void Initialize() ExperimentFeatureMap[experimentId].Add(feature.Id); } else - { - ExperimentFeatureMap[experimentId] = new List { feature.Id }; + { + ExperimentFeatureMap[experimentId] = new List + { + feature.Id + }; } } - if (RolloutIdMap.TryGetValue(feature.RolloutId, out var rolloutRules)) { - var rolloutRulesVariations = rolloutRules.Experiments.SelectMany(ex => ex.Variations); - foreach (var rolloutRuleVariation in rolloutRulesVariations) { - variationKeyToVariationDict[rolloutRuleVariation.Key] = rolloutRuleVariation; + + if (RolloutIdMap.TryGetValue(feature.RolloutId, out var rolloutRules)) + { + var rolloutRulesVariations = + rolloutRules.Experiments.SelectMany(ex => ex.Variations); + foreach (var rolloutRuleVariation in rolloutRulesVariations) + { + variationKeyToVariationDict[rolloutRuleVariation.Key] = + rolloutRuleVariation; } } - + flagToVariationsMap[feature.Key] = variationKeyToVariationDict; } + _FlagVariationMap = flagToVariationsMap; } @@ -393,7 +437,9 @@ private void Initialize() /// Logger instance /// ErrorHandler instance /// ProjectConfig instance created from datafile string - public static ProjectConfig Create(string content, ILogger logger, IErrorHandler errorHandler) + public static ProjectConfig Create(string content, ILogger logger, + IErrorHandler errorHandler + ) { DatafileProjectConfig config = GetConfig(content); @@ -416,8 +462,11 @@ private static DatafileProjectConfig GetConfig(string configData) var config = JsonConvert.DeserializeObject(configData); config._datafile = configData; - if (SupportedVersions.TrueForAll((supportedVersion) => !(((int)supportedVersion).ToString() == config.Version))) - throw new ConfigParseException($@"This version of the C# SDK does not support the given datafile version: {config.Version}"); + if (SupportedVersions.TrueForAll((supportedVersion) => + !(((int)supportedVersion).ToString() == config.Version))) + throw new ConfigParseException( + $@"This version of the C# SDK does not support the given datafile version: { + config.Version}"); return config; } @@ -436,7 +485,8 @@ public Group GetGroup(string groupId) string message = $@"Group ID ""{groupId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidGroupException("Provided group is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidGroupException("Provided group is not in datafile.")); return new Group(); } @@ -452,7 +502,9 @@ public Experiment GetExperimentFromKey(string experimentKey) string message = $@"Experiment key ""{experimentKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidExperimentException("Provided experiment is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidExperimentException( + "Provided experiment is not in datafile.")); return new Experiment(); } @@ -468,7 +520,9 @@ public Experiment GetExperimentFromId(string experimentId) string message = $@"Experiment ID ""{experimentId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidExperimentException("Provided experiment is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidExperimentException( + "Provided experiment is not in datafile.")); return new Experiment(); } @@ -484,7 +538,8 @@ public Entity.Event GetEvent(string eventKey) string message = $@"Event key ""{eventKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidEventException("Provided event is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidEventException("Provided event is not in datafile.")); return new Entity.Event(); } @@ -500,7 +555,8 @@ public Audience GetAudience(string audienceId) string message = $@"Audience ID ""{audienceId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidAudienceException("Provided audience is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidAudienceException("Provided audience is not in datafile.")); return new Audience(); } @@ -516,7 +572,8 @@ public Attribute GetAttribute(string attributeKey) string message = $@"Attribute key ""{attributeKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidAttributeException("Provided attribute is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidAttributeException("Provided attribute is not in datafile.")); return new Attribute(); } @@ -533,9 +590,11 @@ public Variation GetVariationFromKey(string experimentKey, string variationKey) _VariationKeyMap[experimentKey].ContainsKey(variationKey)) return _VariationKeyMap[experimentKey][variationKey]; - string message = $@"No variation key ""{variationKey}"" defined in datafile for experiment ""{experimentKey}""."; + string message = $@"No variation key ""{variationKey + }"" defined in datafile for experiment ""{experimentKey}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -552,9 +611,11 @@ public Variation GetVariationFromKeyByExperimentId(string experimentId, string v _VariationKeyMapByExperimentId[experimentId].ContainsKey(variationKey)) return _VariationKeyMapByExperimentId[experimentId][variationKey]; - string message = $@"No variation key ""{variationKey}"" defined in datafile for experiment ""{experimentId}""."; + string message = $@"No variation key ""{variationKey + }"" defined in datafile for experiment ""{experimentId}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -571,9 +632,11 @@ public Variation GetVariationFromId(string experimentKey, string variationId) _VariationIdMap[experimentKey].ContainsKey(variationId)) return _VariationIdMap[experimentKey][variationId]; - string message = $@"No variation ID ""{variationId}"" defined in datafile for experiment ""{experimentKey}""."; + string message = $@"No variation ID ""{variationId + }"" defined in datafile for experiment ""{experimentKey}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -590,9 +653,11 @@ public Variation GetVariationFromIdByExperimentId(string experimentId, string va _VariationIdMapByExperimentId[experimentId].ContainsKey(variationId)) return _VariationIdMapByExperimentId[experimentId][variationId]; - string message = $@"No variation ID ""{variationId}"" defined in datafile for experiment ""{experimentId}""."; + string message = $@"No variation ID ""{variationId + }"" defined in datafile for experiment ""{experimentId}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -608,7 +673,8 @@ public FeatureFlag GetFeatureFlagFromKey(string featureKey) string message = $@"Feature key ""{featureKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidFeatureException("Provided feature is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidFeatureException("Provided feature is not in datafile.")); return new FeatureFlag(); } @@ -620,8 +686,8 @@ public FeatureFlag GetFeatureFlagFromKey(string featureKey) /// public Variation GetFlagVariationByKey(string flagKey, string variationKey) { - if (FlagVariationMap.TryGetValue(flagKey, out var variationsKeyMap)) { - + if (FlagVariationMap.TryGetValue(flagKey, out var variationsKeyMap)) + { variationsKeyMap.TryGetValue(variationKey, out var variation); return variation; } @@ -639,18 +705,19 @@ public Rollout GetRolloutFromId(string rolloutId) #if NET35 || NET40 if (string.IsNullOrEmpty(rolloutId) || string.IsNullOrEmpty(rolloutId.Trim())) #else - if (string.IsNullOrWhiteSpace(rolloutId)) + if (string.IsNullOrWhiteSpace(rolloutId)) #endif { return new Rollout(); } - + if (_RolloutIdMap.ContainsKey(rolloutId)) return _RolloutIdMap[rolloutId]; string message = $@"Rollout ID ""{rolloutId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidRolloutException("Provided rollout is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidRolloutException("Provided rollout is not in datafile.")); return new Rollout(); } @@ -667,7 +734,10 @@ public string GetAttributeId(string attributeKey) { var attribute = _AttributeKeyMap[attributeKey]; if (hasReservedPrefix) - Logger.Log(LogLevel.WARN, $@"Attribute {attributeKey} unexpectedly has reserved prefix {RESERVED_ATTRIBUTE_PREFIX}; using attribute ID instead of reserved attribute name."); + Logger.Log(LogLevel.WARN, + $@"Attribute {attributeKey} unexpectedly has reserved prefix { + RESERVED_ATTRIBUTE_PREFIX + }; using attribute ID instead of reserved attribute name."); return attribute.Id; } diff --git a/OptimizelySDK/Config/HttpProjectConfigManager.cs b/OptimizelySDK/Config/HttpProjectConfigManager.cs index 22e9d593..2f8a3d40 100644 --- a/OptimizelySDK/Config/HttpProjectConfigManager.cs +++ b/OptimizelySDK/Config/HttpProjectConfigManager.cs @@ -32,13 +32,18 @@ public class HttpProjectConfigManager : PollingProjectConfigManager private string Url; internal string LastModifiedSince = string.Empty; private string DatafileAccessToken = string.Empty; - private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler) + + private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, + bool autoUpdate, ILogger logger, IErrorHandler errorHandler + ) : base(period, blockingTimeout, autoUpdate, logger, errorHandler) { Url = url; } - private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string datafileAccessToken) + private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, + bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string datafileAccessToken + ) : this(period, url, blockingTimeout, autoUpdate, logger, errorHandler) { DatafileAccessToken = datafileAccessToken; @@ -62,21 +67,26 @@ public HttpClient() public HttpClient(System.Net.Http.HttpClient httpClient) : this() { - if (httpClient != null) { + if (httpClient != null) + { Client = httpClient; } } public static System.Net.Http.HttpClientHandler GetHttpClientHandler() { - var handler = new System.Net.Http.HttpClientHandler() { - AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate + var handler = new System.Net.Http.HttpClientHandler() + { + AutomaticDecompression = System.Net.DecompressionMethods.GZip | + System.Net.DecompressionMethods.Deflate }; return handler; } - public virtual Task SendAsync(System.Net.Http.HttpRequestMessage httpRequestMessage) + public virtual Task SendAsync( + System.Net.Http.HttpRequestMessage httpRequestMessage + ) { return Client.SendAsync(httpRequestMessage); } @@ -141,7 +151,7 @@ private string GetRemoteDatafileResponse() return content.Result; } -#elif NET40 +#elif NET40 private string GetRemoteDatafileResponse() { var request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(Url); @@ -181,19 +191,24 @@ protected override ProjectConfig Poll() return DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); } - + public class Builder { private const long MAX_MILLISECONDS_LIMIT = 4294967294; private readonly TimeSpan DEFAULT_PERIOD = TimeSpan.FromMinutes(5); private readonly TimeSpan DEFAULT_BLOCKINGOUT_PERIOD = TimeSpan.FromSeconds(15); - private readonly string DEFAULT_FORMAT = "https://cdn.optimizely.com/datafiles/{0}.json"; - private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = "https://config.optimizely.com/datafiles/auth/{0}.json"; + + private readonly string DEFAULT_FORMAT = + "https://cdn.optimizely.com/datafiles/{0}.json"; + + private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = + "https://config.optimizely.com/datafiles/auth/{0}.json"; + private string Datafile; - private string DatafileAccessToken; + private string DatafileAccessToken; private string SdkKey; private string Url; - private string Format; + private string Format; private ILogger Logger; private IErrorHandler ErrorHandler; private TimeSpan Period; @@ -214,6 +229,7 @@ public Builder WithBlockingTimeoutPeriod(TimeSpan blockingTimeoutSpan) return this; } + public Builder WithDatafile(string datafile) { Datafile = datafile; @@ -258,7 +274,7 @@ public Builder WithFormat(string format) return this; } - + public Builder WithLogger(ILogger logger) { Logger = logger; @@ -280,7 +296,7 @@ public Builder WithAutoUpdate(bool autoUpdate) return this; } - public Builder WithStartByDefault(bool startByDefault=true) + public Builder WithStartByDefault(bool startByDefault = true) { StartByDefault = startByDefault; @@ -320,41 +336,62 @@ public HttpProjectConfigManager Build(bool defer) if (ErrorHandler == null) ErrorHandler = new DefaultErrorHandler(Logger, false); - if (string.IsNullOrEmpty(Format)) { - - if (string.IsNullOrEmpty(DatafileAccessToken)) { + if (string.IsNullOrEmpty(Format)) + { + if (string.IsNullOrEmpty(DatafileAccessToken)) + { Format = DEFAULT_FORMAT; - } else { + } + else + { Format = DEFAULT_AUTHENTICATED_DATAFILE_FORMAT; } } - if (string.IsNullOrEmpty(Url)) { - if (string.IsNullOrEmpty(SdkKey)) { + if (string.IsNullOrEmpty(Url)) + { + if (string.IsNullOrEmpty(SdkKey)) + { ErrorHandler.HandleError(new Exception("SdkKey cannot be null")); } + Url = string.Format(Format, SdkKey); } - if (IsPollingIntervalProvided && (Period.TotalMilliseconds <= 0 || Period.TotalMilliseconds > MAX_MILLISECONDS_LIMIT)) { - Logger.Log(LogLevel.DEBUG, $"Polling interval is not valid for periodic calls, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); + if (IsPollingIntervalProvided && (Period.TotalMilliseconds <= 0 || + Period.TotalMilliseconds > + MAX_MILLISECONDS_LIMIT)) + { + Logger.Log(LogLevel.DEBUG, + $"Polling interval is not valid for periodic calls, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); Period = DEFAULT_PERIOD; - } else if(!IsPollingIntervalProvided) { - Logger.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); + } + else if (!IsPollingIntervalProvided) + { + Logger.Log(LogLevel.DEBUG, + $"No polling interval provided, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); Period = DEFAULT_PERIOD; } - - if (IsBlockingTimeoutProvided && (BlockingTimeoutSpan.TotalMilliseconds <= 0 || BlockingTimeoutSpan.TotalMilliseconds > MAX_MILLISECONDS_LIMIT)) { - Logger.Log(LogLevel.DEBUG, $"Blocking timeout is not valid, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); + + if (IsBlockingTimeoutProvided && (BlockingTimeoutSpan.TotalMilliseconds <= 0 || + BlockingTimeoutSpan.TotalMilliseconds > + MAX_MILLISECONDS_LIMIT)) + { + Logger.Log(LogLevel.DEBUG, + $"Blocking timeout is not valid, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); BlockingTimeoutSpan = DEFAULT_BLOCKINGOUT_PERIOD; - } else if(!IsBlockingTimeoutProvided) { - Logger.Log(LogLevel.DEBUG, $"No Blocking timeout provided, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); + } + else if (!IsBlockingTimeoutProvided) + { + Logger.Log(LogLevel.DEBUG, + $"No Blocking timeout provided, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); BlockingTimeoutSpan = DEFAULT_BLOCKINGOUT_PERIOD; } - - configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, AutoUpdate, Logger, ErrorHandler, DatafileAccessToken); + + configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, + AutoUpdate, Logger, ErrorHandler, DatafileAccessToken); if (Datafile != null) { @@ -368,11 +405,13 @@ public HttpProjectConfigManager Build(bool defer) Logger.Log(LogLevel.WARN, "Error parsing fallback datafile." + ex.Message); } } - - configManager.NotifyOnProjectConfigUpdate += () => { - NotificationCenter?.SendNotifications(NotificationCenter.NotificationType.OptimizelyConfigUpdate); + + configManager.NotifyOnProjectConfigUpdate += () => + { + NotificationCenter?.SendNotifications(NotificationCenter.NotificationType + .OptimizelyConfigUpdate); }; - + if (StartByDefault) configManager.Start(); @@ -380,7 +419,7 @@ public HttpProjectConfigManager Build(bool defer) // Optionally block until config is available. if (!defer) configManager.GetConfig(); - + return configManager; } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 30f7d76a..7b77ad04 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021, Optimizely + * Copyright 2017-2021, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use file except in compliance with the License. @@ -69,8 +69,10 @@ public class Optimizely : IOptimizely, IDisposable /// It returns true if the ProjectConfig is valid otherwise false. /// Also, it may block execution if GetConfig() blocks execution to get ProjectConfig. /// - public bool IsValid { - get { + public bool IsValid + { + get + { return ProjectConfigManager?.GetConfig() != null; } } @@ -88,7 +90,8 @@ public static String SDK_VERSION // Microsoft Major.Minor.Build.Revision // Semantic Major.Minor.Patch Version version = assembly.GetName().Version; - String answer = String.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build); + String answer = String.Format("{0}.{1}.{2}", version.Major, version.Minor, + version.Build); return answer; } } @@ -120,25 +123,32 @@ public static String SDK_TYPE /// boolean representing whether JSON schema validation needs to be performed /// EventProcessor public Optimizely(string datafile, - IEventDispatcher eventDispatcher = null, - ILogger logger = null, - IErrorHandler errorHandler = null, - UserProfileService userProfileService = null, - bool skipJsonValidation = false, - EventProcessor eventProcessor = null, - OptimizelyDecideOption[] defaultDecideOptions = null) + IEventDispatcher eventDispatcher = null, + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null, + bool skipJsonValidation = false, + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null + ) { - try { - InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, null, eventProcessor, defaultDecideOptions); + try + { + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + null, eventProcessor, defaultDecideOptions); - if (ValidateInputs(datafile, skipJsonValidation)) { + if (ValidateInputs(datafile, skipJsonValidation)) + { var config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); ProjectConfigManager = new FallbackProjectConfigManager(config); - } else { + } + else + { Logger.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."); } } - catch (Exception ex) { + catch (Exception ex) + { string error = String.Empty; if (ex.GetType() == typeof(ConfigParseException)) error = ex.Message; @@ -160,26 +170,29 @@ public Optimizely(string datafile, /// User profile service. /// EventProcessor public Optimizely(ProjectConfigManager configManager, - NotificationCenter notificationCenter = null, - IEventDispatcher eventDispatcher = null, - ILogger logger = null, - IErrorHandler errorHandler = null, - UserProfileService userProfileService = null, - EventProcessor eventProcessor = null, - OptimizelyDecideOption[] defaultDecideOptions = null) + NotificationCenter notificationCenter = null, + IEventDispatcher eventDispatcher = null, + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null, + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null + ) { ProjectConfigManager = configManager; - InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, notificationCenter, eventProcessor, defaultDecideOptions); + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + notificationCenter, eventProcessor, defaultDecideOptions); } private void InitializeComponents(IEventDispatcher eventDispatcher = null, - ILogger logger = null, - IErrorHandler errorHandler = null, - UserProfileService userProfileService = null, - NotificationCenter notificationCenter = null, - EventProcessor eventProcessor = null, - OptimizelyDecideOption[] defaultDecideOptions = null) + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null, + NotificationCenter notificationCenter = null, + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null + ) { Logger = logger ?? new NoOpLogger(); EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger); @@ -188,9 +201,13 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, EventBuilder = new EventBuilder(Bucketer, Logger); UserProfileService = userProfileService; NotificationCenter = notificationCenter ?? new NotificationCenter(Logger); - DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); - EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, NotificationCenter, Logger); - DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[] { }; + DecisionService = + new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); + EventProcessor = eventProcessor ?? + new ForwardingEventProcessor(EventDispatcher, NotificationCenter, + Logger); + DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[] + { }; } /// @@ -199,11 +216,14 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, /// Experiment Object representing experiment /// string ID for user /// associative array of Attributes for the user - private bool ValidatePreconditions(Experiment experiment, string userId, ProjectConfig config, UserAttributes userAttributes = null) + private bool ValidatePreconditions(Experiment experiment, string userId, + ProjectConfig config, UserAttributes userAttributes = null + ) { if (!experiment.IsExperimentRunning) { - Logger.Log(LogLevel.INFO, string.Format("Experiment {0} is not running.", experiment.Key)); + Logger.Log(LogLevel.INFO, + string.Format("Experiment {0} is not running.", experiment.Key)); return false; } @@ -212,9 +232,14 @@ private bool ValidatePreconditions(Experiment experiment, string userId, Project return true; } - if (!ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, userAttributes, "experiment", experiment.Key, Logger).ResultObject) + if (!ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, userAttributes, + "experiment", experiment.Key, Logger) + .ResultObject) { - Logger.Log(LogLevel.INFO, string.Format("User \"{0}\" does not meet conditions to be in experiment \"{1}\".", userId, experiment.Key)); + Logger.Log(LogLevel.INFO, + string.Format( + "User \"{0}\" does not meet conditions to be in experiment \"{1}\".", + userId, experiment.Key)); return false; } @@ -228,7 +253,9 @@ private bool ValidatePreconditions(Experiment experiment, string userId, Project /// string ID for user /// associative array of Attributes for the user /// null|Variation Representing variation - public Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null) + public Variation Activate(string experimentKey, string userId, + UserAttributes userAttributes = null + ) { var config = ProjectConfigManager?.GetConfig(); @@ -240,8 +267,12 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us var inputValues = new Dictionary { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } + { + USER_ID, userId + }, + { + EXPERIMENT_KEY, experimentKey + } }; if (!ValidateStringInputs(inputValues)) @@ -263,7 +294,8 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - SendImpressionEvent(experiment, variation, userId, userAttributes, config, SOURCE_TYPE_EXPERIMENT, true); + SendImpressionEvent(experiment, variation, userId, userAttributes, config, + SOURCE_TYPE_EXPERIMENT, true); return variation; } @@ -286,7 +318,9 @@ private bool ValidateInputs(string datafile, bool skipJsonValidation) /// ID for user /// Attributes of the user /// eventTags array Hash representing metadata associated with the event. - public void Track(string eventKey, string userId, UserAttributes userAttributes = null, EventTags eventTags = null) + public void Track(string eventKey, string userId, UserAttributes userAttributes = null, + EventTags eventTags = null + ) { var config = ProjectConfigManager?.GetConfig(); @@ -298,8 +332,12 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes var inputValues = new Dictionary { - { USER_ID, userId }, - { EVENT_KEY, eventKey } + { + USER_ID, userId + }, + { + EVENT_KEY, eventKey + } }; if (!ValidateStringInputs(inputValues)) @@ -309,7 +347,8 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes if (eevent.Key == null) { - Logger.Log(LogLevel.INFO, string.Format("Not tracking user {0} for event {1}.", userId, eventKey)); + Logger.Log(LogLevel.INFO, + string.Format("Not tracking user {0} for event {1}.", userId, eventKey)); return; } @@ -318,15 +357,19 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes eventTags = eventTags.FilterNullValues(Logger); } - var userEvent = UserEventFactory.CreateConversionEvent(config, eventKey, userId, userAttributes, eventTags); + var userEvent = UserEventFactory.CreateConversionEvent(config, eventKey, userId, + userAttributes, eventTags); EventProcessor.Process(userEvent); - Logger.Log(LogLevel.INFO, string.Format("Tracking event {0} for user {1}.", eventKey, userId)); + Logger.Log(LogLevel.INFO, + string.Format("Tracking event {0} for user {1}.", eventKey, userId)); - if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Track) > 0) + if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Track) > + 0) { var conversionEvent = EventFactory.CreateLogEvent(userEvent, Logger); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Track, eventKey, userId, - userAttributes, eventTags, conversionEvent); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Track, + eventKey, userId, + userAttributes, eventTags, conversionEvent); } } @@ -337,7 +380,9 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes /// ID for the user /// Attributes for the users /// null|Variation Representing variation - public Variation GetVariation(string experimentKey, string userId, UserAttributes userAttributes = null) + public Variation GetVariation(string experimentKey, string userId, + UserAttributes userAttributes = null + ) { var config = ProjectConfigManager?.GetConfig(); return GetVariation(experimentKey, userId, config, userAttributes); @@ -351,7 +396,9 @@ public Variation GetVariation(string experimentKey, string userId, UserAttribute /// ProjectConfig to be used for variation /// Attributes for the users /// null|Variation Representing variation - private Variation GetVariation(string experimentKey, string userId, ProjectConfig config, UserAttributes userAttributes = null) + private Variation GetVariation(string experimentKey, string userId, ProjectConfig config, + UserAttributes userAttributes = null + ) { if (config == null) { @@ -361,8 +408,12 @@ private Variation GetVariation(string experimentKey, string userId, ProjectConfi var inputValues = new Dictionary { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } + { + USER_ID, userId + }, + { + EXPERIMENT_KEY, experimentKey + } }; if (!ValidateStringInputs(inputValues)) @@ -374,15 +425,23 @@ private Variation GetVariation(string experimentKey, string userId, ProjectConfi userAttributes = userAttributes ?? new UserAttributes(); var userContext = CreateUserContext(userId, userAttributes); - var variation = DecisionService.GetVariation(experiment, userContext, config)?.ResultObject; + var variation = DecisionService.GetVariation(experiment, userContext, config) + ?.ResultObject; var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", variation?.Key }, + { + "experimentKey", experimentKey + }, + { + "variationKey", variation?.Key + }, }; - var decisionNotificationType = config.IsFeatureExperiment(experiment.Id) ? DecisionNotificationTypes.FEATURE_TEST : DecisionNotificationTypes.AB_TEST; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, decisionNotificationType, userId, + var decisionNotificationType = config.IsFeatureExperiment(experiment.Id) ? + DecisionNotificationTypes.FEATURE_TEST : + DecisionNotificationTypes.AB_TEST; + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + decisionNotificationType, userId, userAttributes, decisionInfo); return variation; } @@ -406,10 +465,15 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia var inputValues = new Dictionary { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } + { + USER_ID, userId + }, + { + EXPERIMENT_KEY, experimentKey + } }; - return ValidateStringInputs(inputValues) && DecisionService.SetForcedVariation(experimentKey, userId, variationKey, config); + return ValidateStringInputs(inputValues) && + DecisionService.SetForcedVariation(experimentKey, userId, variationKey, config); } /// @@ -428,8 +492,12 @@ public Variation GetForcedVariation(string experimentKey, string userId) var inputValues = new Dictionary { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } + { + USER_ID, userId + }, + { + EXPERIMENT_KEY, experimentKey + } }; if (!ValidateStringInputs(inputValues)) @@ -456,15 +524,20 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IsFeatureEnabled'."); + Logger.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'IsFeatureEnabled'."); return false; } var inputValues = new Dictionary { - { USER_ID, userId }, - { FEATURE_KEY, featureKey } + { + USER_ID, userId + }, + { + FEATURE_KEY, featureKey + } }; if (!ValidateStringInputs(inputValues)) @@ -479,7 +552,10 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, bool featureEnabled = false; var sourceInfo = new Dictionary(); - var decision = DecisionService.GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), config).ResultObject; + var decision = DecisionService + .GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), + config) + .ResultObject; var variation = decision?.Variation; var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; @@ -497,27 +573,41 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, } else { - Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); + Logger.Log(LogLevel.INFO, + $@"The user ""{userId}"" is not being experimented on feature ""{featureKey + }""."); } } if (featureEnabled == true) - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); + Logger.Log(LogLevel.INFO, + $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); else - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); + Logger.Log(LogLevel.INFO, + $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", featureEnabled }, - { "source", decisionSource }, - { "sourceInfo", sourceInfo }, + { + "featureKey", featureKey + }, + { + "featureEnabled", featureEnabled + }, + { + "source", decisionSource + }, + { + "sourceInfo", sourceInfo + }, }; - SendImpressionEvent(decision?.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource, featureEnabled); + SendImpressionEvent(decision?.Experiment, variation, userId, userAttributes, config, + featureKey, decisionSource, featureEnabled); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.FEATURE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return featureEnabled; } @@ -530,21 +620,31 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, /// The user's attributes /// Variable type /// string | null Feature variable value - public virtual T GetFeatureVariableValueForType(string featureKey, string variableKey, string userId, - UserAttributes userAttributes, string variableType) + public virtual T GetFeatureVariableValueForType(string featureKey, string variableKey, + string userId, + UserAttributes userAttributes, string variableType + ) { var config = ProjectConfigManager?.GetConfig(); if (config == null) { - Logger.Log(LogLevel.ERROR, $@"Datafile has invalid format. Failing '{FeatureVariable.GetFeatureVariableTypeName(variableType)}'."); + Logger.Log(LogLevel.ERROR, + $@"Datafile has invalid format. Failing '{ + FeatureVariable.GetFeatureVariableTypeName(variableType)}'."); return default(T); } var inputValues = new Dictionary { - { USER_ID, userId }, - { FEATURE_KEY, featureKey }, - { VARIABLE_KEY, variableKey } + { + USER_ID, userId + }, + { + FEATURE_KEY, featureKey + }, + { + VARIABLE_KEY, variableKey + } }; if (!ValidateStringInputs(inputValues)) @@ -558,47 +658,60 @@ 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(T); } 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(T); } var featureEnabled = false; var variableValue = featureVariable.DefaultValue; - var decision = DecisionService.GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), config).ResultObject; + var decision = DecisionService + .GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), + config) + .ResultObject; if (decision?.Variation != null) { var variation = decision.Variation; featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); - var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); + var featureVariableUsageInstance = + variation.GetFeatureVariableUsageFromId(featureVariable.Id); if (featureVariableUsageInstance != null) { if (variation.FeatureEnabled == true) { variableValue = featureVariableUsageInstance.Value; - Logger.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."); + Logger.Log(LogLevel.INFO, + $@"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}""."); + Logger.Log(LogLevel.INFO, + $@"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}""."); + Logger.Log(LogLevel.INFO, + $@"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(); @@ -611,17 +724,35 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var var typeCastedValue = GetTypeCastedVariableValue(variableValue, variableType); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", featureEnabled }, - { "variableKey", variableKey }, - { "variableValue", typeCastedValue is OptimizelyJSON? ((OptimizelyJSON)typeCastedValue).ToDictionary() : typeCastedValue }, - { "variableType", variableType.ToString().ToLower() }, - { "source", decision?.Source }, - { "sourceInfo", sourceInfo }, + { + "featureKey", featureKey + }, + { + "featureEnabled", featureEnabled + }, + { + "variableKey", variableKey + }, + { + "variableValue", + typeCastedValue is OptimizelyJSON ? + ((OptimizelyJSON)typeCastedValue).ToDictionary() : + typeCastedValue + }, + { + "variableType", variableType.ToString().ToLower() + }, + { + "source", decision?.Source + }, + { + "sourceInfo", sourceInfo + }, }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE_VARIABLE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.FEATURE_VARIABLE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return (T)typeCastedValue; } @@ -633,9 +764,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// bool | Feature variable value or null - public bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.BOOLEAN_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.BOOLEAN_TYPE); } /// @@ -646,9 +780,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// double | Feature variable value or null - public double? GetFeatureVariableDouble(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public double? GetFeatureVariableDouble(string featureKey, string variableKey, + string userId, UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.DOUBLE_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.DOUBLE_TYPE); } /// @@ -659,9 +796,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// int | Feature variable value or null - public int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.INTEGER_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.INTEGER_TYPE); } /// @@ -672,9 +812,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// string | Feature variable value or null - public string GetFeatureVariableString(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public string GetFeatureVariableString(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.STRING_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.STRING_TYPE); } /// @@ -685,9 +828,12 @@ public string GetFeatureVariableString(string featureKey, string variableKey, st /// The user ID /// The user's attributes /// OptimizelyJson | Feature variable value or null - public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableKey, + string userId, UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.JSON_TYPE); } /// @@ -698,11 +844,14 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK /// The user's attributes /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. public OptimizelyUserContext CreateUserContext(string userId, - UserAttributes userAttributes = null) + UserAttributes userAttributes = null + ) { var inputValues = new Dictionary { - { USER_ID, userId }, + { + USER_ID, userId + }, }; if (!ValidateStringInputs(inputValues)) @@ -721,14 +870,16 @@ public OptimizelyUserContext CreateUserContext(string userId, /// A list of options for decision-making. /// A decision result. internal OptimizelyDecision Decide(OptimizelyUserContext user, - string key, - OptimizelyDecideOption[] options) + string key, + OptimizelyDecideOption[] options + ) { var config = ProjectConfigManager?.GetConfig(); if (config == null) { - return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); + return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, + ErrorHandler, Logger); } if (key == null) @@ -756,12 +907,14 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, FeatureDecision decision = null; var decisionContext = new OptimizelyDecisionContext(flag.Key); - var forcedDecisionVariation = DecisionService.ValidatedForcedDecision(decisionContext, config, user); + var forcedDecisionVariation = + DecisionService.ValidatedForcedDecision(decisionContext, config, user); decisionReasons += forcedDecisionVariation.DecisionReasons; if (forcedDecisionVariation.ResultObject != null) { - decision = new FeatureDecision(null, forcedDecisionVariation.ResultObject, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); + decision = new FeatureDecision(null, forcedDecisionVariation.ResultObject, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } else { @@ -785,29 +938,34 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, if (featureEnabled) { - Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); } else { - Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); } var variableMap = new Dictionary(); - if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) + if (flag?.Variables != null && + !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { foreach (var featureVariable in flag?.Variables) { string variableValue = featureVariable.DefaultValue; if (featureEnabled) { - var featureVariableUsageInstance = decision?.Variation.GetFeatureVariableUsageFromId(featureVariable.Id); + var featureVariableUsageInstance = + decision?.Variation.GetFeatureVariableUsageFromId(featureVariable.Id); if (featureVariableUsageInstance != null) { variableValue = featureVariableUsageInstance.Value; } } - var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); + var typeCastedValue = + GetTypeCastedVariableValue(variableValue, featureVariable.Type); if (typeCastedValue is OptimizelyJSON) typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); @@ -821,9 +979,14 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { - decisionEventDispatched = SendImpressionEvent(decision?.Experiment, decision?.Variation, userId, userAttributes, config, key, decisionSource, featureEnabled); + decisionEventDispatched = SendImpressionEvent(decision?.Experiment, + decision?.Variation, userId, userAttributes, config, key, decisionSource, + featureEnabled); } - var reasonsToReport = decisionReasons.ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS)).ToArray(); + + var reasonsToReport = decisionReasons + .ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) + .ToArray(); var variationKey = decision?.Variation?.Key; // TODO: add ruleKey values when available later. use a copy of experimentKey until then. @@ -831,17 +994,32 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, var decisionInfo = new Dictionary { - { "flagKey", key }, - { "enabled", featureEnabled }, - { "variables", variableMap }, - { "variationKey", variationKey }, - { "ruleKey", ruleKey }, - { "reasons", reasonsToReport }, - { "decisionEventDispatched", decisionEventDispatched } + { + "flagKey", key + }, + { + "enabled", featureEnabled + }, + { + "variables", variableMap + }, + { + "variationKey", variationKey + }, + { + "ruleKey", ruleKey + }, + { + "reasons", reasonsToReport + }, + { + "decisionEventDispatched", decisionEventDispatched + } }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FLAG, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.FLAG, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return new OptimizelyDecision( variationKey, @@ -854,14 +1032,16 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, } internal Dictionary DecideAll(OptimizelyUserContext user, - OptimizelyDecideOption[] options) + OptimizelyDecideOption[] options + ) { var decisionMap = new Dictionary(); var projectConfig = ProjectConfigManager?.GetConfig(); if (projectConfig == null) { - Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + Logger.Log(LogLevel.ERROR, + "Optimizely instance is not valid, failing isFeatureEnabled call."); return decisionMap; } @@ -872,15 +1052,17 @@ internal Dictionary DecideAll(OptimizelyUserContext } internal Dictionary DecideForKeys(OptimizelyUserContext user, - string[] keys, - OptimizelyDecideOption[] options) + string[] keys, + OptimizelyDecideOption[] options + ) { var decisionDictionary = new Dictionary(); var projectConfig = ProjectConfigManager?.GetConfig(); if (projectConfig == null) { - Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + Logger.Log(LogLevel.ERROR, + "Optimizely instance is not valid, failing isFeatureEnabled call."); return decisionDictionary; } @@ -894,7 +1076,8 @@ internal Dictionary DecideForKeys(OptimizelyUserCont foreach (string key in keys) { var decision = Decide(user, key, options); - if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.Enabled) + if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || + decision.Enabled) { decisionDictionary.Add(key, decision); } @@ -910,6 +1093,7 @@ private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) { copiedOptions = options.Union(DefaultDecideOptions).ToArray(); } + return copiedOptions; } @@ -922,10 +1106,12 @@ private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) /// The user's attributes /// It can either be experiment in case impression event is sent from activate or it's feature-test or rollout private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, - UserAttributes userAttributes, ProjectConfig config, - string ruleType, bool enabled) + UserAttributes userAttributes, ProjectConfig config, + string ruleType, bool enabled + ) { - SendImpressionEvent(experiment, variation, userId, userAttributes, config, "", ruleType, enabled); + SendImpressionEvent(experiment, variation, userId, userAttributes, config, "", ruleType, + enabled); } /// @@ -938,34 +1124,43 @@ private void SendImpressionEvent(Experiment experiment, Variation variation, str /// It can either be experiment key in case if ruleType is experiment or it's feature key in case ruleType is feature-test or rollout /// It can either be experiment in case impression event is sent from activate or it's feature-test or rollout private bool SendImpressionEvent(Experiment experiment, Variation variation, string userId, - UserAttributes userAttributes, ProjectConfig config, - string flagKey, string ruleType, bool enabled) + UserAttributes userAttributes, ProjectConfig config, + string flagKey, string ruleType, bool enabled + ) { if (experiment != null && !experiment.IsExperimentRunning) { - Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation."); + Logger.Log(LogLevel.ERROR, + @"Experiment has ""Launched"" status so not dispatching event during activation."); } - var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, userId, userAttributes, flagKey, ruleType, enabled); + var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, + userId, userAttributes, flagKey, ruleType, enabled); if (userEvent == null) { return false; } + EventProcessor.Process(userEvent); if (experiment != null) { - Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); + Logger.Log(LogLevel.INFO, + $"Activating user {userId} in experiment {experiment.Key}."); } + // Kept For backwards compatibility. // This notification is deprecated and the new DecisionNotifications // are sent via their respective method calls. - if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Activate) > 0) + if (NotificationCenter.GetNotificationCount( + NotificationCenter.NotificationType.Activate) > 0) { var impressionEvent = EventFactory.CreateLogEvent(userEvent, Logger); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId, - userAttributes, variation, impressionEvent); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, + experiment, userId, + userAttributes, variation, impressionEvent); } + return true; } @@ -983,11 +1178,17 @@ public List GetEnabledFeatures(string userId, UserAttributes userAttribu if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetEnabledFeatures'."); + Logger.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetEnabledFeatures'."); return enabledFeaturesList; } - if (!ValidateStringInputs(new Dictionary { { USER_ID, userId } })) + if (!ValidateStringInputs(new Dictionary + { + { + USER_ID, userId + } + })) return enabledFeaturesList; foreach (var feature in config.FeatureKeyMap.Values) @@ -1008,12 +1209,14 @@ public List GetEnabledFeatures(string userId, UserAttributes userAttribu /// The user's attributes /// string | null An OptimizelyJSON instance for all variable values. public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, - UserAttributes userAttributes = null) + UserAttributes userAttributes = null + ) { var config = ProjectConfigManager?.GetConfig(); if (config == null) { - Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing getAllFeatureVariableValues call. type"); + Logger.Log(LogLevel.ERROR, + "Optimizely instance is not valid, failing getAllFeatureVariableValues call. type"); return null; } @@ -1031,7 +1234,8 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, var featureFlag = config.GetFeatureFlagFromKey(featureKey); if (string.IsNullOrEmpty(featureFlag.Key)) { - Logger.Log(LogLevel.INFO, "No feature flag was found for key \"" + featureKey + "\"."); + Logger.Log(LogLevel.INFO, + "No feature flag was found for key \"" + featureKey + "\"."); return null; } @@ -1039,7 +1243,8 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, return null; var featureEnabled = false; - var decisionResult = DecisionService.GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), config); + var decisionResult = DecisionService.GetVariationForFeature(featureFlag, + CreateUserContext(userId, userAttributes), config); var variation = decisionResult.ResultObject?.Variation; if (variation != null) @@ -1048,38 +1253,46 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, } else { - Logger.Log(LogLevel.INFO, "User \"" + userId + "\" was not bucketed into any variation for feature flag \"" + featureKey + "\". " + - "The default values are being returned."); + Logger.Log(LogLevel.INFO, "User \"" + userId + + "\" was not bucketed into any variation for feature flag \"" + + featureKey + "\". " + + "The default values are being returned."); } if (featureEnabled) { - Logger.Log(LogLevel.INFO, "Feature \"" + featureKey + "\" is enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + featureKey + "\" is enabled for user \"" + userId + "\""); } else { - Logger.Log(LogLevel.INFO, "Feature \"" + featureKey + "\" is not enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + featureKey + "\" is not enabled for user \"" + userId + "\""); } + var valuesMap = new Dictionary(); foreach (var featureVariable in featureFlag.Variables) { string variableValue = featureVariable.DefaultValue; if (featureEnabled) { - var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); + var featureVariableUsageInstance = + variation.GetFeatureVariableUsageFromId(featureVariable.Id); if (featureVariableUsageInstance != null) { variableValue = featureVariableUsageInstance.Value; } } - var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); + var typeCastedValue = + GetTypeCastedVariableValue(variableValue, featureVariable.Type); if (typeCastedValue is OptimizelyJSON) typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); valuesMap.Add(featureVariable.Key, typeCastedValue); } + var sourceInfo = new Dictionary(); if (decisionResult.ResultObject?.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) { @@ -1089,14 +1302,25 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", featureEnabled }, - { "variableValues", valuesMap }, - { "source", decisionResult.ResultObject?.Source }, - { "sourceInfo", sourceInfo }, + { + "featureKey", featureKey + }, + { + "featureEnabled", featureEnabled + }, + { + "variableValues", valuesMap + }, + { + "source", decisionResult.ResultObject?.Source + }, + { + "sourceInfo", sourceInfo + }, }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.ALL_FEATURE_VARIABLE, userId, + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.ALL_FEATURE_VARIABLE, userId, userAttributes ?? new UserAttributes(), decisionInfo); return new OptimizelyJSON(valuesMap, ErrorHandler, Logger); @@ -1112,7 +1336,8 @@ public OptimizelyConfig GetOptimizelyConfig() if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetOptimizelyConfig'."); + Logger.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetOptimizelyConfig'."); return null; } @@ -1123,7 +1348,8 @@ public OptimizelyConfig GetOptimizelyConfig() return ((IOptimizelyConfigManager)ProjectConfigManager).GetOptimizelyConfig(); } - Logger.Log(LogLevel.DEBUG, "ProjectConfigManager is not instance of IOptimizelyConfigManager, generating new OptimizelyConfigObject as a fallback"); + Logger.Log(LogLevel.DEBUG, + "ProjectConfigManager is not instance of IOptimizelyConfigManager, generating new OptimizelyConfigObject as a fallback"); return new OptimizelyConfigService(config).GetOptimizelyConfig(); } @@ -1174,7 +1400,8 @@ private object GetTypeCastedVariableValue(string value, string type) break; case FeatureVariable.DOUBLE_TYPE: - double.TryParse(value, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out double doubleValue); + double.TryParse(value, System.Globalization.NumberStyles.Number, + System.Globalization.CultureInfo.InvariantCulture, out double doubleValue); result = doubleValue; break; @@ -1193,7 +1420,8 @@ private object GetTypeCastedVariableValue(string value, string type) } if (result == null) - Logger.Log(LogLevel.ERROR, $@"Unable to cast variable value ""{value}"" to type ""{type}""."); + Logger.Log(LogLevel.ERROR, + $@"Unable to cast variable value ""{value}"" to type ""{type}""."); return result; } diff --git a/OptimizelySDK/Utils/Validator.cs b/OptimizelySDK/Utils/Validator.cs index 693cd0cd..52004662 100644 --- a/OptimizelySDK/Utils/Validator.cs +++ b/OptimizelySDK/Utils/Validator.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019, Optimizely + * Copyright 2017-2019, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + using OptimizelySDK.Entity; using System; using System.Collections.Generic; @@ -37,9 +38,9 @@ public static bool ValidateJSONSchema(string configJson, string schemaJson = nul { try { - return !NJsonSchema.JsonSchema.FromJsonAsync(schemaJson ?? Schema.GetSchemaJson()). - Result.Validate(configJson). - Any(); + return !NJsonSchema.JsonSchema.FromJsonAsync(schemaJson ?? Schema.GetSchemaJson()) + .Result.Validate(configJson) + .Any(); } catch (Newtonsoft.Json.JsonReaderException) { @@ -73,10 +74,10 @@ public static bool IsAttributeValid(Attribute attribute) return !int.TryParse(attribute.Key, out key); } - public static bool AreEventTagsValid(Dictionary eventTags) { + public static bool AreEventTagsValid(Dictionary eventTags) + { int key; return eventTags.All(tag => !int.TryParse(tag.Key, out key)); - } /// @@ -112,8 +113,9 @@ public static bool IsFeatureFlagValid(ProjectConfig projectConfig, FeatureFlag f /// true if attribute key is not null and value is one of the supported type, false otherwise public static bool IsUserAttributeValid(KeyValuePair attribute) { - return (attribute.Key != null) && - (attribute.Value is string || attribute.Value is bool || IsValidNumericValue(attribute.Value)); + return (attribute.Key != null) && + (attribute.Value is string || attribute.Value is bool || + IsValidNumericValue(attribute.Value)); } /// @@ -123,9 +125,11 @@ public static bool IsUserAttributeValid(KeyValuePair attribute) /// public static bool IsNumericType(object value) { - return value is byte || value is sbyte || value is char || value is short || value is ushort - || value is int || value is uint || value is long || value is ulong || value is float - || value is double || value is decimal; + return value is byte || value is sbyte || value is char || value is short || + value is ushort + || value is int || value is uint || value is long || value is ulong || + value is float + || value is double || value is decimal; } /// @@ -138,7 +142,8 @@ public static bool IsValidNumericValue(object value) if (IsNumericType(value)) { var doubleValue = Convert.ToDouble(value); - if (double.IsInfinity(doubleValue) || double.IsNaN(doubleValue) || Math.Abs(doubleValue) > OPT_NUMBER_LIMIT) + if (double.IsInfinity(doubleValue) || double.IsNaN(doubleValue) || + Math.Abs(doubleValue) > OPT_NUMBER_LIMIT) return false; return true; @@ -148,4 +153,3 @@ public static bool IsValidNumericValue(object value) } } } - From 08a347e6c01605d43c7dcb15bf476cff3cac5e56 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 10 Jul 2023 11:30:18 -0400 Subject: [PATCH 5/6] Sort imports for linting --- .../Utils/TestHttpProjectConfigManagerUtil.cs | 6 +++--- .../Config/HttpProjectConfigManager.cs | 4 ---- OptimizelySDK/Optimizely.cs | 17 ++++++++--------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs b/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs index 4db77c84..451517be 100644 --- a/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs +++ b/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright 2019-2020, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,13 @@ * limitations under the License. */ +using Moq; +using OptimizelySDK.Config; using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Moq; -using OptimizelySDK.Config; namespace OptimizelySDK.Tests.Utils { diff --git a/OptimizelySDK/Config/HttpProjectConfigManager.cs b/OptimizelySDK/Config/HttpProjectConfigManager.cs index 2f8a3d40..f27559f5 100644 --- a/OptimizelySDK/Config/HttpProjectConfigManager.cs +++ b/OptimizelySDK/Config/HttpProjectConfigManager.cs @@ -19,11 +19,7 @@ using OptimizelySDK.Notifications; using System; using System.Net; -using System.Linq; using System.Threading.Tasks; -using OptimizelySDK.ErrorHandler; -using OptimizelySDK.Logger; -using OptimizelySDK.Notifications; namespace OptimizelySDK.Config { diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 7b77ad04..efffc02f 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -14,24 +14,23 @@ * limitations under the License. */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using OptimizelySDK.Bucketing; +using OptimizelySDK.Config; using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event; using OptimizelySDK.Event.Builder; using OptimizelySDK.Event.Dispatcher; using OptimizelySDK.Exceptions; using OptimizelySDK.Logger; -using OptimizelySDK.Utils; using OptimizelySDK.Notifications; -using System; -using System.Collections.Generic; -using System.Reflection; -using OptimizelySDK.Config; -using OptimizelySDK.Event; -using OptimizelySDK.OptlyConfig; -using System.Net; using OptimizelySDK.OptimizelyDecisions; -using System.Linq; +using OptimizelySDK.OptlyConfig; +using OptimizelySDK.Utils; namespace OptimizelySDK { From 8353be1f5bf05f9be80a2c3259259e11d0f662f2 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 10 Jul 2023 11:31:14 -0400 Subject: [PATCH 6/6] Sort alphabetically --- OptimizelySDK/Optimizely.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index efffc02f..c21f2c65 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -14,10 +14,6 @@ * limitations under the License. */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using OptimizelySDK.Bucketing; using OptimizelySDK.Config; using OptimizelySDK.Entity; @@ -31,6 +27,10 @@ using OptimizelySDK.OptimizelyDecisions; using OptimizelySDK.OptlyConfig; using OptimizelySDK.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; namespace OptimizelySDK {