diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..81a3f58d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "PHP SDK", + + "remoteEnv": { + "SDK_ROOT": "/workspaces/php-sdk", + "XDEBUG_CONFIG": "log_level=0", + }, + + "image": "mcr.microsoft.com/devcontainers/php:0-8.2", + + "postStartCommand": "composer install", + + "forwardPorts": [ + 8080 + ], + "customizations": { + "vscode": { + "extensions": [ + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug", + "DEVSENSE.composer-php-vscode", + "xdebug.php-pack", + "recca0120.vscode-phpunit", + "eamodio.gitlens" + ] + } + } +} diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml new file mode 100644 index 00000000..c6ce8dae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -0,0 +1,88 @@ +name: 🐞 Bug +description: File a bug/issue +title: "[BUG] " +labels: ["bug", "needs-triage"] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: SDK Version + description: Version of the SDK in use? + validations: + required: true +- type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 1. With this config... + 1. Run '...' + 1. See error... + validations: + required: true +- type: textarea + attributes: + label: PHP Version + description: What version of PHP are you using? + validations: + required: false +- type: textarea + attributes: + label: Link + description: Link to code demonstrating the problem. + validations: + required: false +- type: textarea + attributes: + label: Logs + description: Logs/stack traces related to the problem (⚠️do not include sensitive information). + validations: + required: false +- type: dropdown + attributes: + label: Severity + description: What is the severity of the problem? + multiple: true + options: + - Blocking development + - Affecting users + - Minor issue + validations: + required: false +- type: textarea + attributes: + label: Workaround/Solution + description: Do you have any workaround or solution in mind for the problem? + validations: + required: false +- type: textarea + attributes: + label: Recent Change + description: Has this issue started happening after an update or experiment change? + validations: + required: false +- type: textarea + attributes: + label: Conflicts + description: Are there other libraries/dependencies potentially in conflict? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml new file mode 100644 index 00000000..a4a39434 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml @@ -0,0 +1,45 @@ +name: ✨Enhancement +description: Create a new ticket for a Enhancement/Tech-initiative for the benefit of the SDK which would be considered for a minor version update. +title: "[ENHANCEMENT] <title>" +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: Description + description: Briefly describe the enhancement in a few sentences. + placeholder: Short description... + validations: + required: true + - type: textarea + id: benefits + attributes: + label: Benefits + description: How would the enhancement benefit to your product or usage? + placeholder: Benefits... + validations: + required: true + - type: textarea + id: detail + attributes: + label: Detail + description: How would you like the enhancement to work? Please provide as much detail as possible + placeholder: Detailed description... + validations: + required: false + - type: textarea + id: examples + attributes: + label: Examples + description: Are there any examples of this enhancement in other products/services? If so, please provide links or references. + placeholder: Links/References... + validations: + required: false + - type: textarea + id: risks + attributes: + label: Risks/Downsides + description: Do you think this enhancement could have any potential downsides or risks? + placeholder: Risks/Downsides... + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md new file mode 100644 index 00000000..a061f335 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md @@ -0,0 +1,4 @@ +<!-- + Thanks for filing in issue! Are you requesting a new feature? If so, please share your feedback with us on the following link. +--> +## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..17de7159 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 💡Feature Requests + url: https://feedback.optimizely.com/ + about: Feedback requesting a new feature can be shared here. diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 574c3a53..109abb8a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,8 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v3 - name: Set up PHP uses: shivammathur/setup-php@v2 with: @@ -25,28 +26,24 @@ jobs: name: Source Clear Scan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v3 - name: Source clear scan env: SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} run: curl -sSL https://download.sourceclear.com/ci.sh | bash -s – scan - integration_tests: - name: Integration Tests - uses: optimizely/php-sdk/.github/workflows/integration_test.yml@master - secrets: - CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }} - TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} - unit_tests: name: Unit Tests ${{ matrix.php-versions }} + needs: [ linting, source_clear ] runs-on: ubuntu-latest strategy: fail-fast: false matrix: php-versions: [ '8.1', '8.2' ] steps: - - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v3 - name: Set up PHP v${{ matrix.php-versions }} uses: shivammathur/setup-php@v2 with: @@ -77,3 +74,11 @@ jobs: run: | composer global require php-coveralls/php-coveralls php-coveralls --coverage_clover=./build/logs/clover.xml -v + + integration_tests: + name: Integration Tests + needs: [ unit_tests ] + uses: optimizely/php-sdk/.github/workflows/integration_test.yml@master + secrets: + CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }} + TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index cebf6dcd..34678e01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Optimizely PHP SDK Changelog +## 4.0.1 +December 4, 2023 + +* Add Dev Containers and bug bash #269 +* Add GitHub Issues templates #278 +* Fix: Deprecation warning #279 +* Return Latest Experiment When Duplicate Keys in Config #280 +* Fix: Code examples by @localheinz #281 + ## 4.0.0 June 12, 2023 diff --git a/README.md b/README.md index aec472c4..6f9d1610 100644 --- a/README.md +++ b/README.md @@ -37,25 +37,55 @@ php composer.phar require optimizely/optimizely-sdk Create the Optimizely client, for example: ```php +<?php + +use Optimizely\Optimizely; + $optimizely = new Optimizely(<<DATAFILE>>); ``` Or you may also use OptimizelyFactory method to create an optimizely client using your SDK key, an optional fallback datafile and an optional datafile access token. Using this method internally creates an HTTPProjectConfigManager. See [HTTPProjectConfigManager](#use-httpprojectconfigmanager) for further detail. ```php -$optimizelyClient = OptimizelyFactory::createDefaultInstance("your-sdk-key", <<DATAFILE>>, <<DATAFILE_AUTH_TOKEN>>); +<?php + +use Optimizely\OptimizelyFactory; + +$optimizelyClient = OptimizelyFactory::createDefaultInstance( + "your-sdk-key", + <<DATAFILE>>, + <<DATAFILE_AUTH_TOKEN>> +); ``` To access your HTTPProjectConfigManager: ```php +<?php + +use Optimizely\Optimizely; + +/** @var Optimizely $optimizelyClient */ $configManager = $optimizelyClient->configManager; ``` Or you can also provide an implementation of the [`ProjectConfigManagerInterface`](https://github.com/optimizely/php-sdk/blob/master/src/Optimizely/ProjectConfigManager/ProjectConfigManagerInterface.php) in the constructor: ```php +<?php + +use Optimizely\Optimizely; +use Optimizely\ProjectConfigManager\HTTPProjectConfigManager; + $configManager = new HTTPProjectConfigManager(<<SDK_KEY>>); -$optimizely = new Optimizely(<<DATAFILE>>, null, null, null, false, null, $configManager); +$optimizely = new Optimizely( + <<DATAFILE>>, + null, + null, + null, + false, + null, + $configManager +); ``` ### ProjectConfigManagerInterface @@ -74,6 +104,10 @@ Calling `fetch` will update the internal ProjectConfig instance that will be ret ### Use HTTPProjectConfigManager ```php +<?php + +use Optimizely\ProjectConfigManager\HTTPProjectConfigManager; + $configManager = new HTTPProjectConfigManager(<<SDK_KEY>>); ``` diff --git a/bug-bash/Decide.php b/bug-bash/Decide.php new file mode 100644 index 00000000..d68ebe1c --- /dev/null +++ b/bug-bash/Decide.php @@ -0,0 +1,144 @@ +<?php + +namespace Optimizely\BugBash; + +require_once '../vendor/autoload.php'; +require_once '../bug-bash/_bug-bash-autoload.php'; + +use Monolog\Logger; +use Optimizely\Decide\OptimizelyDecideOption; +use Optimizely\Logger\DefaultLogger; +use Optimizely\Notification\NotificationType; +use Optimizely\Optimizely; +use Optimizely\OptimizelyFactory; +use Optimizely\OptimizelyUserContext; + +// 1. Change this SDK key to your project's SDK Key +const SDK_KEY = '<your-sdk-key>'; + +// 2. Change this to your flag key +const FLAG_KEY = '<your-flag-key>'; + +// 3. Uncomment each scenario 1 by 1 modifying the contents of the method +// to test additional scenarios. + +$test = new DecideTests(); +$test->verifyDecisionProperties(); +// $test->testWithVariationsOfDecideOptions(); +// $test->verifyLogsImpressionsEventsDispatched(); +// $test->verifyResultsPageInYourProjectShowsImpressionEvent(); +// $test->verifyDecisionListenerWasCalled(); +// $test->verifyAnInvalidFlagKeyIsHandledCorrectly(); + +// 4. Change the current folder into the bug-bash directory +// cd bug-bash/ + +// 5. Run the following command to execute the uncommented tests above: +// php Decide.php + +// https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-php +class DecideTests +{ + // verify decision return properties with default DecideOptions + public function verifyDecisionProperties(): void + { + $decision = $this->userContext->decide(FLAG_KEY); + + $this->printDecision($decision, "Check that the following decision properties are expected for user $this->userId"); + } + + // test decide w all options: DISABLE_DECISION_EVENT, ENABLED_FLAGS_ONLY, IGNORE_USER_PROFILE_SERVICE, INCLUDE_REASONS, EXCLUDE_VARIABLES (will need to add variables) + public function testWithVariationsOfDecideOptions(): void + { + $options = [ + OptimizelyDecideOption::INCLUDE_REASONS, + // OptimizelyDecideOption::DISABLE_DECISION_EVENT, + // OptimizelyDecideOption::ENABLED_FLAGS_ONLY, // ⬅️ Disable some of your flags + // OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE, + // OptimizelyDecideOption::EXCLUDE_VARIABLES, + ]; + + $decision = $this->userContext->decide(FLAG_KEY, $options); + + $this->printDecision($decision, 'Modify the OptimizelyDecideOptions and check the decision variables expected'); + } + + // verify in logs that impression event of this decision was dispatched + public function verifyLogsImpressionsEventsDispatched(): void + { + // 💡️ Create a new flag with an A/B Test eg "product_version" + $featureFlagKey = 'product_version'; + $logger = new DefaultLogger(Logger::DEBUG); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $localUserContext = $localOptimizelyClient->createUserContext($this->userId); + + // review the DEBUG output, ensuring you see an impression log + // "Dispatching impression event to URL https://logx.optimizely.com/v1/events with params..." + $localUserContext->decide($featureFlagKey); + } + + // verify on Results page that impression even was created + public function verifyResultsPageInYourProjectShowsImpressionEvent(): void + { + print "Go to your project's results page and verify decisions events are showing (5 min delay)"; + } + + // verify that decision listener contains correct information + public function verifyDecisionListenerWasCalled(): void + { + // Check that this was called during the... + $onDecision = function ($type, $userId, $attributes, $decisionInfo) { + print ">>> [$this->outputTag] OnDecision: + type: $type, + userId: $userId, + attributes: " . print_r($attributes, true) . " + decisionInfo: " . print_r($decisionInfo, true) . "\r\n"; + }; + $this->optimizelyClient->notificationCenter->addNotificationListener( + NotificationType::DECISION, + $onDecision + ); + + // ...decide. + $this->userContext->decide(FLAG_KEY); + } + + // verify that invalid flag key is handled correctly + public function verifyAnInvalidFlagKeyIsHandledCorrectly(): void + { + $logger = new DefaultLogger(Logger::ERROR); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $userId = 'user-' . mt_rand(10, 99); + $localUserContext = $localOptimizelyClient->createUserContext($userId); + + // ensure you see an error -- Optimizely.ERROR: FeatureFlag Key "a_key_not_in_the_project" is not in datafile. + $localUserContext->decide("a_key_not_in_the_project"); + } + + private Optimizely $optimizelyClient; + private string $userId; + private ?OptimizelyUserContext $userContext; + private string $outputTag = "Decide"; + + public function __construct() + { + $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY); + + $this->userId = 'user-' . mt_rand(10, 99); + $attributes = ['age' => 25, 'country' => 'canada', 'abandoned_cart' => false]; + $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes); + } + + private function printDecision($decision, $message): void + { + $enabled = $decision->getEnabled() ? "true" : "false"; + + print ">>> [$this->outputTag] $message: + enabled: $enabled, + flagKey: {$decision->getFlagKey()}, + ruleKey: {$decision->getRuleKey()}, + variationKey: {$decision->getVariationKey()}, + variables: " . print_r($decision->getVariables(), true) . ", + reasons: " . print_r($decision->getReasons(), true) . "\r\n"; + } +} diff --git a/bug-bash/DecideAll.php b/bug-bash/DecideAll.php new file mode 100644 index 00000000..92c3cffd --- /dev/null +++ b/bug-bash/DecideAll.php @@ -0,0 +1,134 @@ +<?php + +namespace Optimizely\BugBash; + +require_once '../vendor/autoload.php'; +require_once '../bug-bash/_bug-bash-autoload.php'; + +use Monolog\Logger; +use Optimizely\Decide\OptimizelyDecideOption; +use Optimizely\Logger\DefaultLogger; +use Optimizely\Notification\NotificationType; +use Optimizely\Optimizely; +use Optimizely\OptimizelyFactory; +use Optimizely\OptimizelyUserContext; + +// 1. Change this SDK key to your project's SDK Key +const SDK_KEY = '<your-sdk-key>'; + +// 2. Create additional flag keys in your project (2+) + +// 3. Uncomment each scenario 1 by 1 modifying the contents of the method +// to test additional scenarios. + +$test = new DecideAllTests(); +$test->verifyDecisionProperties(); +// $test->testWithVariousCombinationsOfOptions(); +// $test->verifyLogImpressionEventDispatched(); +// $test->verifyResultsPageShowsImpressionEvents(); +// $test->verifyDecisionListenerContainsCorrectInformation(); + +// 4. Change the current folder into the bug-bash directory if you're not already there: +// cd bug-bash/ + +// 5. Run the following command to execute the uncommented tests above: +// php DecideAll.php + +// https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-php +class DecideAllTests +{ + // verify decide all returns properties without specifying default options + public function verifyDecisionProperties(): void + { + $decision = $this->userContext->decideAll(); + + $this->printDecisions($decision, "Check that all of the decisions' multiple properties are expected for user `$this->userId`"); + } + + // test with all and variations/combinations of options + public function testWithVariousCombinationsOfOptions(): void + { + $options = [ + OptimizelyDecideOption::INCLUDE_REASONS, + // OptimizelyDecideOption::DISABLE_DECISION_EVENT, + // OptimizelyDecideOption::ENABLED_FLAGS_ONLY, // ⬅️ Disable some of your flags + // OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE, + OptimizelyDecideOption::EXCLUDE_VARIABLES, + ]; + + $decisions = $this->userContext->decideAll($options); + + $this->printDecisions($decisions, "Check that all of your flags' decisions respected the options passed."); + } + + // verify in logs that impression event of this decision was dispatched + public function verifyLogImpressionEventDispatched(): void + { + // 💡️ Be sure you have >=1 of your project's flags has an EXPERIMENT type + $logger = new DefaultLogger(Logger::DEBUG); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $localUserContext = $localOptimizelyClient->createUserContext($this->userId); + + // review the DEBUG output, ensuring you see an impression log for each *EXPERIMENT* with a message like + // "Dispatching impression event to URL https://logx.optimizely.com/v1/events with params..." + // ⚠️ Rollout flag types should not dispatch and impression event + $localUserContext->decideAll(); + } + + // verify on Results page that impression events was created + public function verifyResultsPageShowsImpressionEvents(): void + { + print "After about 5-10 minutes, go to your project's results page and verify decisions events are showing."; + } + + // verify that decision listener contains correct information + public function verifyDecisionListenerContainsCorrectInformation(): void + { + // Check that this was called for each of your project flag keys + $onDecision = function ($type, $userId, $attributes, $decisionInfo) { + print ">>> [$this->outputTag] OnDecision: + type: $type, + userId: $userId, + attributes: " . print_r($attributes, true) . " + decisionInfo: " . print_r($decisionInfo, true) . "\r\n"; + }; + $this->optimizelyClient->notificationCenter->addNotificationListener( + NotificationType::DECISION, + $onDecision + ); + + $this->userContext->decideAll(); + } + + private Optimizely $optimizelyClient; + private string $userId; + private ?OptimizelyUserContext $userContext; + private string $outputTag = "Decide All"; + + public function __construct() + { + $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY); + + $this->userId = 'user-' . mt_rand(10, 99); + $attributes = ['country' => 'nederland', 'age' => 43, 'is_return_visitor' => true]; + $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes); + } + + private function printDecisions($decisions, $message): void + { + $count = 0; + foreach ($decisions as $decision) { + $enabled = $decision->getEnabled() ? "true" : "false"; + + print ">>> [$this->outputTag #$count] $message: + enabled: $enabled, + flagKey: {$decision->getFlagKey()}, + ruleKey: {$decision->getRuleKey()}, + variationKey: {$decision->getVariationKey()}, + variables: " . print_r($decision->getVariables(), true) . ", + reasons: " . print_r($decision->getReasons(), true) . "\r\n"; + + $count++; + } + } +} diff --git a/bug-bash/DecideForKeys.php b/bug-bash/DecideForKeys.php new file mode 100644 index 00000000..b059b49a --- /dev/null +++ b/bug-bash/DecideForKeys.php @@ -0,0 +1,150 @@ +<?php + +namespace Optimizely\BugBash; + +require_once '../vendor/autoload.php'; +require_once '../bug-bash/_bug-bash-autoload.php'; + +use Monolog\Logger; +use Optimizely\Decide\OptimizelyDecideOption; +use Optimizely\Logger\DefaultLogger; +use Optimizely\Notification\NotificationType; +use Optimizely\Optimizely; +use Optimizely\OptimizelyFactory; +use Optimizely\OptimizelyUserContext; + +// 1. Change this SDK key to your project's SDK Key +const SDK_KEY = '<your-sdk-key>'; + +// 2. Check that you have 3+ flag keys in your project and add them here +const FLAG_KEYS = ['<your-flag-key-1>', '<your-flag-key-2>', '<your-flag-key-3>']; + +// 3. Uncomment each scenario 1 by 1 modifying the contents of the method +// to test additional scenarios. + +$test = new DecideForKeysTests(); +$test->verifyDecisionProperties(); +// $test->testWithVariationsOfDecideOptions(); +// $test->verifyLogsImpressionsEventsDispatched(); +// $test->verifyResultsPageInYourProjectShowsImpressionEvent(); +// $test->verifyDecisionListenerWasCalled(); +// $test->verifyAnInvalidFlagKeyIsHandledCorrectly(); + +// 4. Change the current folder into the bug-bash directory if you've not already +// cd bug-bash/ + +// 5. Run the following command to execute the uncommented tests above: +// php DecideForKeys.php + +// https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-php +class DecideForKeysTests +{ + + // verify decision return properties with default DecideOptions + public function verifyDecisionProperties(): void + { + $decision = $this->userContext->decideForKeys(FLAG_KEYS); + + $this->printDecisions($decision, "Check that the following decisions' properties are expected"); + } + + // test decide w all options: DISABLE_DECISION_EVENT, ENABLED_FLAGS_ONLY, IGNORE_USER_PROFILE_SERVICE, INCLUDE_REASONS, EXCLUDE_VARIABLES (will need to add variables) + public function testWithVariationsOfDecideOptions(): void + { + $options = [ + OptimizelyDecideOption::INCLUDE_REASONS, + // OptimizelyDecideOption::DISABLE_DECISION_EVENT, + // OptimizelyDecideOption::ENABLED_FLAGS_ONLY, // ⬅️ Disable some of your flags + // OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE, + // OptimizelyDecideOption::EXCLUDE_VARIABLES, + ]; + + $decision = $this->userContext->decideForKeys(FLAG_KEYS, $options); + + $this->printDecisions($decision, "Modify the OptimizelyDecideOptions and check all the decisions' are as expected"); + } + + // verify in logs that impression event of this decision was dispatched + public function verifyLogsImpressionsEventsDispatched(): void + { + // 💡️ Be sure that your FLAG_KEYS array above includes A/B Test eg "product_version" + $logger = new DefaultLogger(Logger::DEBUG); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $localUserContext = $localOptimizelyClient->createUserContext($this->userId); + + // review the DEBUG output, ensuring you see an impression log for each experiment type in your FLAG_KEYS + // "Dispatching impression event to URL https://logx.optimizely.com/v1/events with params..." + // ⚠️ Your Rollout type flags should not have impression events + $localUserContext->decideForKeys(FLAG_KEYS); + } + + // verify on Results page that impression even was created + public function verifyResultsPageInYourProjectShowsImpressionEvent(): void + { + print "Go to your project's results page and verify decisions events are showing (5 min delay)"; + } + + // verify that decision listener contains correct information + public function verifyDecisionListenerWasCalled(): void + { + // Check that this was called during the... + $onDecision = function ($type, $userId, $attributes, $decisionInfo) { + print ">>> [$this->outputTag] OnDecision: + type: $type, + userId: $userId, + attributes: " . print_r($attributes, true) . " + decisionInfo: " . print_r($decisionInfo, true) . "\r\n"; + }; + $this->optimizelyClient->notificationCenter->addNotificationListener( + NotificationType::DECISION, + $onDecision + ); + + // ...decide. + $this->userContext->decide(FLAG_KEY); + } + + // verify that invalid flag key is handled correctly + public function verifyAnInvalidFlagKeyIsHandledCorrectly(): void + { + $logger = new DefaultLogger(Logger::ERROR); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $userId = 'user-' . mt_rand(10, 99); + $localUserContext = $localOptimizelyClient->createUserContext($userId); + + // ensure you see an error -- Optimizely.ERROR: FeatureFlag Key "a_key_not_in_the_project" is not in datafile. + $localUserContext->decide("a_key_not_in_the_project"); + } + + private Optimizely $optimizelyClient; + private string $userId; + private ?OptimizelyUserContext $userContext; + private string $outputTag = "Decide For Keys"; + + public function __construct() + { + $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY); + + $this->userId = 'user-' . mt_rand(10, 99); + $attributes = ['likes_yams' => true, 'cart_value' => 34.13, 'country' => 'sweden']; + $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes); + } + + private function printDecisions($decisions, $message): void + { + $count = 0; + foreach ($decisions as $decision) { + $enabled = $decision->getEnabled() ? "true" : "false"; + + print ">>> [$this->outputTag #$count] $message: + enabled: $enabled, + flagKey: {$decision->getFlagKey()}, + ruleKey: {$decision->getRuleKey()}, + variationKey: {$decision->getVariationKey()}, + variables: " . print_r($decision->getVariables(), true) . ", + reasons: " . print_r($decision->getReasons(), true) . "\r\n"; + + $count++; + } + } +} diff --git a/bug-bash/ForcedDecision.php b/bug-bash/ForcedDecision.php new file mode 100644 index 00000000..13b7251b --- /dev/null +++ b/bug-bash/ForcedDecision.php @@ -0,0 +1,323 @@ +<?php +namespace Optimizely\BugBash; + +require_once '../vendor/autoload.php'; +require_once '../bug-bash/_bug-bash-autoload.php'; +// fetch the datafile from an authenticated endpoint +use Optimizely\Optimizely; +use Optimizely\OptimizelyDecisionContext; +use Optimizely\OptimizelyForcedDecision; +use Optimizely\Decide\OptimizelyDecideOption; + +// To test forced decisions please add the SDK key, flag key and any attributes from your optimizely +// project in the code below. If no errors then forced decision test should pass. + +$optimizelyClient = new Optimizely(null, null, null, null, null, null, null, null, "<you sdk key here>"); + + +$userId = 'user' . strval(rand(0, 1000001)); + +echo '==================================='; +echo 'F-to-D (no rule key specified):'; +echo '==================================='.PHP_EOL; + +echo ' Set user context, userId any, age = 20 (bucketed)'.PHP_EOL; +echo ' Call decide with flag1 ---> expected result is variation a'.PHP_EOL; +echo ' ---------------------'.PHP_EOL; + +$user = $optimizelyClient->createUserContext($userId, array("age"=> 20)); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert('variation_a' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision w flag1 and variation b' . PHP_EOL; +echo ' Call decide -----> expected variation b in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', null); +$decision = new OptimizelyForcedDecision('variation_b'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_b' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision with flag1 and variation c (invalid)' . PHP_EOL; +echo ' Call decide ----> expected variation a' . PHP_EOL; +echo ' ---------------------' . PHP_EOL; +$context = new OptimizelyDecisionContext('flag1', null); +$decision = new OptimizelyForcedDecision('variation_c'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_a' == $decideDecision->getVariationKey()); + +// E-to-D (rule key = “flag1_experiment”): +// ------------------------------------------- +// Set user context, userId any, age = 20 (bucketed) +// Call decide with flag1 ---> expected result is variation a in the decide decision +// Set forced decision w flag1 and rule key “flag1_experiment”, and variation b +// Call decide -----> expected variation b in decide decision +// Set forced decision with flag1 and rule key “flag1_experiment” and invalid variation c +// Call decide ----> expected variation a + +echo PHP_EOL . PHP_EOL . '==================================='; +echo 'E-to-D (rule key = “flag1_experiment”):'; +echo '==================================='.PHP_EOL; + +echo ' Set user context, userId any, age = 20 (bucketed)'.PHP_EOL; +echo ' Call decide with flag1 ---> expected result is variation a'.PHP_EOL; +echo ' ---------------------'.PHP_EOL; + +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_a' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision with flag1 and rule flag1_experiment and variation b' . PHP_EOL; +echo ' Call decide -----> expected variation_b in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', 'flag1_experiment'); +$decision = new OptimizelyForcedDecision('variation_b'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_b' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision with flag1 and rule flag1_experiment and variation c (Invalid)' . PHP_EOL; +echo ' Call decide -----> expected variation a in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', 'flag1_experiment'); +$decision = new OptimizelyForcedDecision('variation_c'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_a' == $decideDecision->getVariationKey()); + +// D-to-D (rule key = “flag1_targeted_delivery”): +// ------------------------------------------- +// Set user context, userId any, country = “US” (bucketed) +// Call decide with flag1 ---> expected result is “on” in the decide decision +// Set forced decision w flag1 and rule key “flag1_targeted_delivery”, and variation b +// Call decide -----> expected variation b in decide decision +// Set forced decision with flag1 and rule key “flag1_targeted_delivery” and invalid variation c +// Call decide ----> expected “on” + +echo PHP_EOL . PHP_EOL . '==================================='; +echo 'D-to-D (rule key = “flag1_targeted_delivery”):'; +echo '==================================='.PHP_EOL; + +echo ' Set user context, userId any, country = US (bucketed)'.PHP_EOL; +echo ' Call decide with flag1 ---> expected result is on on'.PHP_EOL; +echo ' ---------------------'.PHP_EOL; + +$user = $optimizelyClient->createUserContext($userId, array("country"=> "US")); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('on' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision with flag1 and rule “flag1_targeted_delivery” and variation b' . PHP_EOL; +echo ' Call decide -----> expected variation_b in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', 'flag1_targeted_delivery'); +$decision = new OptimizelyForcedDecision('variation_b'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_b' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision with flag1 and rule flag1_targeted_delivery and variation c (Invalid)' . PHP_EOL; +echo ' Call decide -----> expected on in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', 'flag1_targeted_delivery'); +$decision = new OptimizelyForcedDecision('variation_c'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('on' == $decideDecision->getVariationKey()); + + +// ================================ +// PART 2 b) Repeat above three blocks, this time user DOES NOT meet audience conditions +// ================================ + +// F-to-D (no rule key specified): +// ------------------------------------------- +// Set user context, userId any, age = 0 (not bucketed) +// Call decide with flag1 ---> expected result is “off” (everyone else) +// Set forced decision w flag1, variation b +// Call decide -----> expected variation b in decide decision + +echo PHP_EOL . PHP_EOL . '==================================='; +echo 'F-to-D (no rule key specified):'; +echo '==================================='.PHP_EOL; + +echo ' Set user context, userId any, age = 0 (not bucketed)'.PHP_EOL; +echo ' Call decide with flag1 ---> expected result is off'.PHP_EOL; +echo ' ---------------------'.PHP_EOL; + +$user = $optimizelyClient->createUserContext($userId, array("age"=> 0)); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert('off' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision w flag1 and variation b' . PHP_EOL; +echo ' Call decide -----> expected variation b in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', null); +$decision = new OptimizelyForcedDecision('variation_b'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_b' == $decideDecision->getVariationKey()); + + +// E-to-D (rule key = “flag1_experiment”): +// ------------------------------------------- +// Set user context, userId any, age = 0 ( not bucketed) +// Call decide with flag1 ---> expected result is “off” +// Set forced decision w flag1 and rule key “flag1_experiment”, and variation b +// Call decide -----> expected variation b in decide decision + +echo PHP_EOL . PHP_EOL . '==================================='; +echo 'E-to-D (rule key = “flag1_experiment”):'; +echo '==================================='.PHP_EOL; + +echo ' Set user context, userId any, age = 0 (not bucketed)'.PHP_EOL; +echo ' Call decide with flag1 ---> expected result is off'.PHP_EOL; +echo ' ---------------------'.PHP_EOL; + +$user = $optimizelyClient->createUserContext($userId, array("age"=> 0)); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert('off' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision w flag1 and rule flag1_experiment and variation b' . PHP_EOL; +echo ' Call decide -----> expected variation b in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', 'flag1_experiment'); +$decision = new OptimizelyForcedDecision('variation_b'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_b' == $decideDecision->getVariationKey()); + + +// D-to-D (rule key = “flag1_targeted_delivery”): +// ------------------------------------------- +// Set user context, userId any, country = “MX” (not bucketed) +// Call decide with flag1 ---> expected result is “off” +// Set forced decision w flag1 and rule key “flag1_targeted_delivery”, and variation b +// Call decide -----> expected variation b in decide decision + + +echo PHP_EOL . PHP_EOL . '==================================='; +echo 'D-to-D (rule key = flag1_targeted_delivery):'; +echo '==================================='.PHP_EOL; + +echo ' Set user context, userId any, country = MX (not bucketed)'.PHP_EOL; +echo ' Call decide with flag1 and rule flag1_targeted_delivery ---> expected result is off'.PHP_EOL; +echo ' ---------------------'.PHP_EOL; + +$user = $optimizelyClient->createUserContext($userId, array("country"=> "MX")); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert('off' == $decideDecision->getVariationKey()); + +echo PHP_EOL . ' Set forced decision w flag1 and rule flag1_targeted_delivery and variation b' . PHP_EOL; +echo ' Call decide -----> expected variation b in decide decision' . PHP_EOL; +echo ' ---------------------'; + +$context = new OptimizelyDecisionContext('flag1', 'flag1_targeted_delivery'); +$decision = new OptimizelyForcedDecision('variation_b'); +$user->setForcedDecision($context, $decision); +$decideDecision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); +echo ' VARIATION >>> ' . $decideDecision->getVariationKey() . PHP_EOL; +// ASSERT YOU GET CORRECT VARIATION +echo ' REASONS >>> ' . json_encode($decideDecision->getReasons()) . PHP_EOL; +// VERIFY REASONS ARE CORRECT +assert ('variation_b' == $decideDecision->getVariationKey()); + +// ================================ +// Part 3 +// ================================ +$user = $optimizelyClient->createUserContext($userId); + +$user->setForcedDecision(new OptimizelyDecisionContext('F1', null), + new OptimizelyForcedDecision('V1')); +$user->setForcedDecision(new OptimizelyDecisionContext('F1', 'E1'), + new OptimizelyForcedDecision('V3')); + +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', null)) == 'V1'); +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', 'E1')) == 'V3'); +$user->setForcedDecision(new OptimizelyDecisionContext('F1', null), + new OptimizelyForcedDecision('V5')); +$user->setForcedDecision(new OptimizelyDecisionContext('F1', 'E1'), + new OptimizelyForcedDecision('V5')); + +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', null)) == 'V5'); +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', 'E1')) == 'V5'); +$user->removeForcedDecision(new OptimizelyDecisionContext('F1', null)); +echo $user->getForcedDecision(new OptimizelyDecisionContext('F1', null)) == null; +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', null)) == null); +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', 'E1')) == 'V5'); + +$user->removeForcedDecision(new OptimizelyDecisionContext('F1', 'E1')); + +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', null)) == null); +assert($user->getForcedDecision(new OptimizelyDecisionContext('F1', 'E1')) == null); + +$user->removeAllForcedDecisions(); + +assert($user->getForcedDecision(new OptimizelyDecisionContext('F2', null)) == null); +assert($user->getForcedDecision(new OptimizelyDecisionContext('F2', 'E1')) == null); + + + +// ?> diff --git a/bug-bash/OptiConfig.php b/bug-bash/OptiConfig.php new file mode 100644 index 00000000..a2b76439 --- /dev/null +++ b/bug-bash/OptiConfig.php @@ -0,0 +1,116 @@ +<?php + +namespace Optimizely\BugBash; + +require_once '../vendor/autoload.php'; +require_once '../bug-bash/_bug-bash-autoload.php'; + +use Optimizely\Optimizely; +use Optimizely\Decide\OptimizelyDecideOption; + +// To test optimizely config simply add your SDK key and attributes in, then run the file. +// You should be able to see unpacked entities from featureMap and experimentMap +// verify them and make sure some are not missing or error + +// Instantiate an Optimizely client +$sdkKey = "<your sdk key here>"; +// $optimizelyClient = new Optimizely($sdkKey); +$optimizelyClient = new Optimizely(null, null, null, null, false, null, null, null, $sdkKey); +$user = $optimizelyClient->createUserContext('user123', ['attribute1' => 'hello']); +$decision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]); + +$reasons = $decision->getReasons(); +echo "[OptimizelyConfig] reasons: " . json_encode($reasons) . PHP_EOL; +echo "[OptimizelyConfig - flag key]: " . $decision->getFlagKey() . PHP_EOL; +echo "[OptimizelyConfig - rule key]: " . $decision->getFlagKey() . PHP_EOL; +echo "[OptimizelyConfig - enabled]: " . $decision->getEnabled() . PHP_EOL; +echo "[OptimizelyConfig - variation key]: " . $decision->getVariationKey() . PHP_EOL; +$variables = $decision->getVariables(); +echo "[OptimizelyConfig - variables]: " . json_encode($variables) . PHP_EOL; +echo PHP_EOL; + +$user->trackEvent('myevent'); + +echo "===========================" . PHP_EOL; +echo " OPTIMIZELY CONFIG V2 " . PHP_EOL; +echo "===========================" . PHP_EOL . PHP_EOL; + +$config = $optimizelyClient->getOptimizelyConfig(); +// get the revision +echo "[OptimizelyConfig] revision:" . $config->getRevision() . PHP_EOL; + +// get the SDK key +echo "[OptimizelyConfig] SDKKey:" . $config->getSdkKey() . PHP_EOL; + +// get the environment key +echo "[OptimizelyConfig] environmentKey:" . $config->getEnvironmentKey() . PHP_EOL; + +// all attributes +echo "[OptimizelyConfig] attributes:" . PHP_EOL; +$attributes = $config->getAttributes(); +foreach($attributes as $attribute) +{ + echo "[OptimizelyAttribute] -- (id, key) = ((" . $attribute->getId(). "), (". $attribute->getKey() . "))" . PHP_EOL; +} + +// all audiences +echo "[OptimizelyConfig] audiences:" . PHP_EOL; +$audiences = $config->getAudiences(); +foreach($audiences as $audience) +{ + echo "[OptimizelyAudience] -- (id, key, conditions) = ((" . $audience->getId(). "), (". $audience->getName() . "), (". $audience->getConditions() . "))" . PHP_EOL; +} + +// all events +echo "[OptimizelyConfig] events:" . PHP_EOL; +$events = $config->getEvents(); +foreach($events as $event) +{ + echo "[OptimizelyEvent] -- (id, key, experimentIds) = ((" . $event->getId(). "), (". $event->getKey() . "), (". $event->getExperimentIds() . "))" . PHP_EOL; +} + +// all flags +$flags = array_values((array)$config->getFeaturesMap()); +foreach ($flags as $flag) +{ + // Use experimentRules and deliverRules + $experimentRules = $flag->getExperimentRules(); + echo "------ Experiment rules -----" . PHP_EOL; + foreach ($experimentRules as $experimentRule) + { + echo "---" . PHP_EOL; + echo "[OptimizelyExperiment] - experiment rule-key = " . $experimentRule->getKey() . PHP_EOL; + echo "[OptimizelyExperiment] - experiment audiences = " . PHP_EOL;$experimentRule->getExperimentAudiences(); + // all variations + $variations = array_values((array)$experimentRule->getVariationsMap()); + foreach ($variations as $variation) + { + echo "[OptimizelyVariation] -- variation = { key: " . $variation->getKey() . ", . id: " . $variation->getId() . ", featureEnabled: " . $variation->getFeatureEnabled() . " }" . PHP_EOL; + $variables = $variation->getVariablesMap(); + foreach ($variables as $variable) + { + echo "[OptimizelyVariable] --- variable: " . $variable->getKey() . ", " . $variable->getId() . PHP_EOL; + // use variable data here. + } + // use experimentRule data here. + } + } + $deliveryRules = $flag->getDeliveryRules(); + echo "------ Delivery rules -----" . PHP_EOL; + foreach ($deliveryRules as $deliveryRule) + { + echo "---"; + echo "[OptimizelyExperiment] - delivery rule-key = " . $deliveryRule->getKey() . PHP_EOL; + echo "[OptimizelyExperiment] - delivery audiences = " . $deliveryRule->getExperimentAudiences() . PHP_EOL; + + // use delivery rule data here... + } +} +// $optimizelyClient->notificationCenter->addNotificationListener( +// NotificationType::OPTIMIZELY_CONFIG_UPDATE, +// function () { +// $newConfig = $optimizelyClient->getOptimizelyConfig(); +// echo "[OptimizelyConfig] revision = " . $newConfig ? $newConfig->getRevision() : "" . PHP_EOL; + // } +// ); + diff --git a/bug-bash/TrackEvent.php b/bug-bash/TrackEvent.php new file mode 100644 index 00000000..e77bb1e5 --- /dev/null +++ b/bug-bash/TrackEvent.php @@ -0,0 +1,118 @@ +<?php + +namespace Optimizely\BugBash; + +require_once '../vendor/autoload.php'; +require_once '../bug-bash/_bug-bash-autoload.php'; + +use Monolog\Logger; +use Optimizely\Logger\DefaultLogger; +use Optimizely\Notification\NotificationType; +use Optimizely\Optimizely; +use Optimizely\OptimizelyFactory; +use Optimizely\OptimizelyUserContext; + +// 1. Change this SDK key to your project's SDK Key +const SDK_KEY = '<your-sdk-key>'; + +// 2. Add an event to your project, adding it to your Experiment flag as a metric, then set the key here +const EVENT_KEY = '<your-event-key>'; + +// 3. Uncomment each scenario 1 by 1 modifying the contents of the method +// to test additional scenarios. + +$test = new TrackEventTests(); +$test->checkTrackNotificationListenerProducesEvent(); +// $test->checkConversionEventLogDispatchedOnTrackEvent(); +// $test->checkConversionEventLogIsNOTDispatchedOnTrackEventForInvalidEventName(); +// $test->testEventTagsShowInDispatchedEventAndAppOptimizelyCom(); + +// 4. Change the current folder into the bug-bash directory if you've not already +// cd bug-bash/ + +// 5. Run the following command to execute the uncommented tests above: +// php TrackEvent.php + +// https://docs.developers.optimizely.com/feature-experimentation/docs/track-event-php +class TrackEventTests +{ + // check that track notification listener produces event with event key + public function checkTrackNotificationListenerProducesEvent(): void + { + $this->optimizelyClient->notificationCenter->addNotificationListener( + NotificationType::TRACK, + $this->onTrackEvent // ⬅️ This should be called with a valid EVENT_NAME + ); + + // ...send track event. + $this->userContext->trackEvent(EVENT_KEY); + } + + // check that conversion event in the dispatch logs contains event key below + public function checkConversionEventLogDispatchedOnTrackEvent(): void + { + $logger = new DefaultLogger(Logger::DEBUG); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $localUserContext = $localOptimizelyClient->createUserContext($this->userId); + + $localUserContext->trackEvent(EVENT_KEY); + } + + // check that event is NOT dispatched if invalid event key is used + // test changing event key in the UI and in the code + public function checkConversionEventLogIsNOTDispatchedOnTrackEventForInvalidEventName(): void + { + $logger = new DefaultLogger(Logger::DEBUG); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $localUserContext = $localOptimizelyClient->createUserContext($this->userId); + $this->optimizelyClient->notificationCenter->addNotificationListener( + NotificationType::TRACK, + $this->onTrackEvent // ⬅️ There should not be a Notification Listener OnTrackEvent called on invalid event name + ); + + // You should not see any "Optimizely.DEBUG: Dispatching conversion event" but instead see + // "Optimizely.INFO: Not tracking user "{user-id}" for event "an_invalid_event_name_not_in_the_project". + $localUserContext->trackEvent("an_invalid_event_name_not_in_the_project"); + } + + // try adding event tags (in the project and in the line below) and see if they show in the event body + public function testEventTagsShowInDispatchedEventAndAppOptimizelyCom(): void + { + $logger = new DefaultLogger(Logger::DEBUG); + $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY); + $localUserContext = $localOptimizelyClient->createUserContext($this->userId); + $custom_tags = [ + 'shoe_size_paris_points' => 44, + 'shoe_size_us_size' => 11.5, + 'use_us_size' => false, + 'color' => 'blue' + ]; + + // Dispatched event should have the tags added to the payload `params { ... }` and also + // should show on app.optimizely.com Reports tab after 5-10 minutes + $localUserContext->trackEvent(EVENT_KEY, $custom_tags); + } + + private Optimizely $optimizelyClient; + private string $userId; + private ?OptimizelyUserContext $userContext; + private string $outputTag = "Track Event"; + private \Closure $onTrackEvent; + + public function __construct() + { + $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY); + + $this->userId = 'user-' . mt_rand(10, 99); + $attributes = ['age' => 19, 'country' => 'bangledesh', 'has_purchased' => true]; + $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes); + + $this->onTrackEvent = function ($type, $userId, $attributes, $decisionInfo) { + print ">>> [$this->outputTag] OnTrackEvent: + type: $type, + userId: $userId, + attributes: " . print_r($attributes, true) . " + decisionInfo: " . print_r($decisionInfo, true) . "\r\n"; + }; + } +} diff --git a/bug-bash/_bug-bash-autoload.php b/bug-bash/_bug-bash-autoload.php new file mode 100644 index 00000000..2d7d7424 --- /dev/null +++ b/bug-bash/_bug-bash-autoload.php @@ -0,0 +1,15 @@ +<?php + +function bug_bash_autoloader($class): void +{ + $class_path = str_replace('\\', '/', $class); + + $file = dirname(__DIR__) . '/src/' . $class_path . '.php'; + + // if the file exists, require it + if (file_exists($file)) { + require $file; + } +} + +spl_autoload_register('bug_bash_autoloader'); diff --git a/composer.json b/composer.json index 57cc755c..2a28d4cd 100644 --- a/composer.json +++ b/composer.json @@ -16,9 +16,9 @@ }, "require": { "php": ">=8.1", + "guzzlehttp/guzzle": ">=6.2", "justinrainbow/json-schema": "^1.6 || ^2.0 || ^4.0 || ^5.0", "lastguest/murmurhash": "^1.3.0", - "guzzlehttp/guzzle": ">=6.2", "monolog/monolog": ">=1.21" }, "require-dev": { @@ -31,5 +31,8 @@ "psr-4": { "Optimizely\\": "src/Optimizely" } + }, + "config": { + "sort-packages": true } } diff --git a/composer.lock b/composer.lock index 030d3673..50ece972 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2fad7123e2b1cb75c4a03d2332468c70", + "content-hash": "82b1237943c87584a7fd69a882570e14", "packages": [ { "name": "guzzlehttp/guzzle", diff --git a/src/Optimizely/Decide/OptimizelyDecision.php b/src/Optimizely/Decide/OptimizelyDecision.php index 3cd7fe1c..a69cc8a5 100644 --- a/src/Optimizely/Decide/OptimizelyDecision.php +++ b/src/Optimizely/Decide/OptimizelyDecision.php @@ -81,6 +81,7 @@ public function getReasons() return $this->reasons; } + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/Event/Builder/EventBuilder.php b/src/Optimizely/Event/Builder/EventBuilder.php index 21a0524a..cc1e2e4c 100644 --- a/src/Optimizely/Event/Builder/EventBuilder.php +++ b/src/Optimizely/Event/Builder/EventBuilder.php @@ -39,7 +39,7 @@ class EventBuilder /** * @const string Version of the Optimizely PHP SDK. */ - const SDK_VERSION = '4.0.0'; + const SDK_VERSION = '4.0.1'; /** * @var string URL to send event to. diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php b/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php index d3eee9b9..a38182d5 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php @@ -53,6 +53,7 @@ public function getKey() /** * @return string JSON representation of the object. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php b/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php index e331871a..d11e4b32 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php @@ -68,6 +68,7 @@ public function getConditions() /** * @return string JSON representation of the object. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php b/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php index 26093779..c8d6f33e 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php @@ -166,6 +166,7 @@ public function getEvents() /** * @return string JSON representation of the object. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php b/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php index d2dadb7c..2d29698a 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php @@ -1,12 +1,12 @@ <?php /** - * Copyright 2020-2021, Optimizely Inc and Contributors + * Copyright 2020-2021, 2023 Optimizely Inc and Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -16,9 +16,12 @@ */ namespace Optimizely\OptimizelyConfig; +use Monolog\Logger; use Optimizely\Config\ProjectConfigInterface; use Optimizely\Entity\Experiment; use Optimizely\Entity\Variation; +use Optimizely\Logger\DefaultLogger; +use Optimizely\Logger\LoggerInterface; class OptimizelyConfigService { @@ -73,7 +76,14 @@ class OptimizelyConfigService */ private $featKeyOptlyVariableIdVariableMap; - public function __construct(ProjectConfigInterface $projectConfig) + /** + * Provided or default logger for logging. + * + * @var LoggerInterface $logger + */ + private readonly LoggerInterface $logger; + + public function __construct(ProjectConfigInterface $projectConfig, LoggerInterface $logger = null) { $this->experiments = $projectConfig->getAllExperiments(); $this->featureFlags = $projectConfig->getFeatureFlags(); @@ -82,7 +92,8 @@ public function __construct(ProjectConfigInterface $projectConfig) $this->environmentKey = $projectConfig->getEnvironmentKey(); $this->sdkKey = $projectConfig->getSdkKey(); $this->projectConfig = $projectConfig; - + $this->logger = $logger ?: new DefaultLogger(); + $this->createLookupMaps(); } @@ -258,7 +269,7 @@ protected function getVariablesMap(Experiment $experiment, Variation $variation) // Set default variables for variation. $variablesMap = $this->featKeyOptlyVariableKeyVariableMap[$featureKey]; - + // Return default variable values if feature is not enabled. if (!$variation->getFeatureEnabled()) { return $variablesMap; @@ -267,13 +278,13 @@ protected function getVariablesMap(Experiment $experiment, Variation $variation) // Set variation specific value if any. foreach ($variation->getVariables() as $variableUsage) { $id = $variableUsage->getId(); - + $optVariable = $this->featKeyOptlyVariableIdVariableMap[$featureKey][$id]; - + $key = $optVariable->getKey(); $value = $variableUsage->getValue(); $type = $optVariable->getType(); - + $modifiedOptVariable = new OptimizelyVariable( $id, $key, @@ -287,7 +298,7 @@ protected function getVariablesMap(Experiment $experiment, Variation $variation) return $variablesMap; } - + /** * Generates Variations map for the given Experiment. * @@ -301,7 +312,7 @@ protected function getVariationsMap(Experiment $experiment) foreach ($experiment->getVariations() as $variation) { $variablesMap = $this->getVariablesMap($experiment, $variation); - + $variationKey = $variation->getKey(); $optVariation = new OptimizelyVariation( $variation->getId(), @@ -401,11 +412,17 @@ protected function getExperimentsMaps() foreach ($this->experiments as $exp) { $expId = $exp->getId(); $expKey = $exp->getKey(); + $audiences = ''; if ($exp->getAudienceConditions() != null) { $audienceConditions = $exp->getAudienceConditions(); $audiences = $this->getSerializedAudiences($audienceConditions); } + + if (array_key_exists($expKey, $experimentsKeyMap)) { + $this->logger->log(Logger::WARNING, sprintf('Duplicate experiment keys found in datafile: %s', $expKey)); + } + $optExp = new OptimizelyExperiment( $expId, $expKey, diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php b/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php index 8a3a90de..17ff39ed 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php @@ -68,6 +68,7 @@ public function getExperimentIds() /** * @return string JSON representation of the object. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php b/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php index 71bd9959..77d274b3 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php @@ -83,6 +83,7 @@ public function getVariationsMap() /** * @return string JSON representation of the object. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php b/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php index 7b605064..491f4ea2 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php @@ -118,6 +118,7 @@ public function getVariablesMap() /** * @return string JSON representation of the object. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyVariable.php b/src/Optimizely/OptimizelyConfig/OptimizelyVariable.php index 836e054f..fade386b 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyVariable.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyVariable.php @@ -82,6 +82,7 @@ public function getValue() /** * @return string JSON representation of the object. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyVariation.php b/src/Optimizely/OptimizelyConfig/OptimizelyVariation.php index e7335eea..3e735a20 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyVariation.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyVariation.php @@ -84,6 +84,7 @@ public function getVariablesMap() * @return string JSON representation of the object. * Unsets featureEnabled property for variations of ab experiments. */ + #[\ReturnTypeWillChange] public function jsonSerialize() { $props = get_object_vars($this); diff --git a/src/Optimizely/ProjectConfigManager/HTTPProjectConfigManager.php b/src/Optimizely/ProjectConfigManager/HTTPProjectConfigManager.php index 7b962aee..83e5f839 100644 --- a/src/Optimizely/ProjectConfigManager/HTTPProjectConfigManager.php +++ b/src/Optimizely/ProjectConfigManager/HTTPProjectConfigManager.php @@ -1,12 +1,12 @@ <?php /** - * Copyright 2019-2020, 2022 Optimizely Inc and Contributors + * Copyright 2019-2020, 2022-2023 Optimizely Inc and Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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, @@ -75,7 +75,12 @@ class HTTPProjectConfigManager implements ProjectConfigManagerInterface /** * @var String datafile access token. */ - private $datafileAccessToken; + private $_datafileAccessToken; + + /** + * @var boolean Flag indicates that the datafile access token is valid. + */ + private $_isDatafileAccessTokenValid; public function __construct( $sdkKey = null, @@ -93,8 +98,8 @@ public function __construct( $this->_logger = $logger ?: new NoOpLogger(); $this->_errorHandler = $errorHandler ?: new NoOpErrorHandler(); $this->_notificationCenter = $notificationCenter ?: new NotificationCenter($this->_logger, $this->_errorHandler); - $this->datafileAccessToken = $datafileAccessToken; - $this->isDatafileAccessTokenValid = Validator::validateNonEmptyString($this->datafileAccessToken); + $this->_datafileAccessToken = $datafileAccessToken; + $this->_isDatafileAccessTokenValid = Validator::validateNonEmptyString($this->_datafileAccessToken); $this->httpClient = new HttpClient(); $this->_url = $this->getUrl($sdkKey, $url, $urlTemplate); @@ -136,7 +141,7 @@ protected function getUrl($sdkKey, $url, $urlTemplate) } if (!Validator::validateNonEmptyString($urlTemplate)) { - if ($this->isDatafileAccessTokenValid) { + if ($this->_isDatafileAccessTokenValid) { $urlTemplate = ProjectConfigManagerConstants::AUTHENTICATED_DATAFILE_URL_TEMPLATE; } else { $urlTemplate = ProjectConfigManagerConstants::DEFAULT_DATAFILE_URL_TEMPLATE; @@ -179,8 +184,8 @@ protected function fetchDatafile() } // Add Authorization header if access token available. - if ($this->isDatafileAccessTokenValid) { - $headers['Authorization'] = "Bearer {$this->datafileAccessToken}"; + if ($this->_isDatafileAccessTokenValid) { + $headers['Authorization'] = "Bearer {$this->_datafileAccessToken}"; } $options = [ diff --git a/tests/EventTests/EventBuilderTest.php b/tests/EventTests/EventBuilderTest.php index ae1df007..967c5893 100644 --- a/tests/EventTests/EventBuilderTest.php +++ b/tests/EventTests/EventBuilderTest.php @@ -69,7 +69,7 @@ protected function setUp() : void ]], 'revision' => '15', 'client_name' => 'php-sdk', - 'client_version' => '4.0.0', + 'client_version' => '4.0.1', 'anonymize_ip'=> false, 'enrich_decisions' => true, ]; diff --git a/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php b/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php index 730ebff4..2a5cf5cf 100644 --- a/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php +++ b/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php @@ -16,9 +16,10 @@ */ namespace Optimizely\Tests; -use Exception; +use Monolog\Logger; use Optimizely\Config\DatafileProjectConfig; use Optimizely\ErrorHandler\NoOpErrorHandler; +use Optimizely\Logger\DefaultLogger; use Optimizely\Logger\NoOpLogger; use Optimizely\OptimizelyConfig\OptimizelyAttribute; use Optimizely\OptimizelyConfig\OptimizelyAudience; @@ -29,10 +30,12 @@ use Optimizely\OptimizelyConfig\OptimizelyFeature; use Optimizely\OptimizelyConfig\OptimizelyVariable; use Optimizely\OptimizelyConfig\OptimizelyVariation; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class OptimizelyConfigServiceTest extends TestCase { + private MockObject $loggerMock; protected function setUp() : void { @@ -149,6 +152,9 @@ protected function setUp() : void $this->expectedExpIdMap['17301270474'] = $abExperiment; $this->expectedExpIdMap['17258450439'] = $groupExperiment; $this->expectedExpIdMap['17279300791'] = $featExperiment; + + // Mock Logger + $this->loggerMock = $this->getMockBuilder(DefaultLogger::class)->getMock(); } protected static function getMethod($name) @@ -203,28 +209,26 @@ public function testGetVariationsMap() public function testGetOptimizelyConfigWithDuplicateExperimentKeys() { + $duplicatedExperimentKey = 'targeted_delivery'; + $secondDuplicatedExperimentId = '9300000007573'; + $this->loggerMock->expects($this->once()) + ->method('log') + ->with( + Logger::WARNING, + sprintf('Duplicate experiment keys found in datafile: %s', $duplicatedExperimentKey) + ); + $this->datafile = DATAFILE_FOR_DUPLICATE_EXP_KEYS; $this->projectConfig = new DatafileProjectConfig( $this->datafile, new NoOpLogger(), new NoOpErrorHandler() ); - $this->optConfigService = new OptimizelyConfigService($this->projectConfig); + $this->optConfigService = new OptimizelyConfigService($this->projectConfig, $this->loggerMock); $optimizelyConfig = $this->optConfigService->getConfig(); - $this->assertEquals(Count($optimizelyConfig->getExperimentsMap()), 1); - $experimentRulesFlag1 = $optimizelyConfig->getFeaturesMap()['flag1']->getExperimentRules(); // 9300000007569 - $experimentRulesFlag2 = $optimizelyConfig->getFeaturesMap()['flag2']->getExperimentRules(); // 9300000007573 - foreach ($experimentRulesFlag1 as $experimentRule) { - if ($experimentRule->getKey() == 'targeted_delivery') { - $this->assertEquals($experimentRule->getId(), '9300000007569'); - } - } - foreach ($experimentRulesFlag2 as $experimentRule) { - if ($experimentRule->getKey() == 'targeted_delivery') { - $this->assertEquals($experimentRule->getId(), '9300000007573'); - } - } + $this->assertEquals(1, Count($optimizelyConfig->getExperimentsMap())); + $this->assertEquals($optimizelyConfig->getExperimentsMap()[$duplicatedExperimentKey]->getId(), $secondDuplicatedExperimentId); } public function testGetOptimizelyConfigWithDuplicateRuleKeys() <!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'> <html xmlns='http://www.w3.org/1999/xhtml'> <head> <title>pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy