Skip to content

Commit

Permalink
Merge 1b1ca8a into fc77081
Browse files Browse the repository at this point in the history
  • Loading branch information
mfahadahmed committed Aug 24, 2019
2 parents fc77081 + 1b1ca8a commit 9697acc
Show file tree
Hide file tree
Showing 7 changed files with 837 additions and 36 deletions.
41 changes: 41 additions & 0 deletions src/Optimizely/Enums/ProjectConfigManagerConstants.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Copyright 2019, 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Optimizely\Enums;

class ProjectConfigManagerConstants
{
/**
* @const int Time in seconds to wait before timing out.
*/
const TIMEOUT = 10;

/**
* @const String Default URL Template to use if only SDK key is provided.
*/
const DEFAULT_URL_TEMPLATE = "https://cdn.optimizely.com/datafiles/%s.json";

/**
* @const String to use while fetching the datafile.
*/
const IF_MODIFIED_SINCE = "If-Modified-Since";

/**
* @const String to use while handling the response.
*/
const LAST_MODIFIED = "Last-Modified";
}
38 changes: 23 additions & 15 deletions src/Optimizely/Optimizely.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@
use Optimizely\Event\Dispatcher\EventDispatcherInterface;
use Optimizely\Logger\LoggerInterface;
use Optimizely\Logger\NoOpLogger;
use Optimizely\ProjectConfigManager\StaticProjectConfigManager;
use Optimizely\Notification\NotificationCenter;
use Optimizely\Notification\NotificationType;
use Optimizely\ProjectConfigManager\ProjectConfigManagerInterface;
use Optimizely\ProjectConfigManager\StaticProjectConfigManager;
use Optimizely\UserProfile\UserProfileServiceInterface;
use Optimizely\Utils\Errors;
use Optimizely\Utils\Validator;
Expand All @@ -56,6 +57,11 @@ class Optimizely
const VARIABLE_KEY = 'Variable Key';
const VARIATION_KEY = 'Variation Key';

/**
* @var DatafileProjectConfig
*/
private $_config;

/**
* @var DecisionService
*/
Expand Down Expand Up @@ -87,7 +93,7 @@ class Optimizely
private $_logger;

/**
* @var ProjectConfigManager
* @var ProjectConfigManagerInterface
*/
private $_projectConfigManager;

Expand All @@ -112,16 +118,21 @@ public function __construct(
LoggerInterface $logger = null,
ErrorHandlerInterface $errorHandler = null,
$skipJsonValidation = false,
UserProfileServiceInterface $userProfileService = null
UserProfileServiceInterface $userProfileService = null,
ProjectConfigManagerInterface $configManager = null
) {
$this->_isValid = true;
$this->_eventDispatcher = $eventDispatcher ?: new DefaultEventDispatcher();
$this->_logger = $logger ?: new NoOpLogger();
$this->_errorHandler = $errorHandler ?: new NoOpErrorHandler();
$this->_eventBuilder = new EventBuilder($this->_logger);
$this->_projectConfigManager = new StaticProjectConfigManager($datafile, $skipJsonValidation, $this->_logger, $this->_errorHandler);
$this->_decisionService = new DecisionService($this->_logger, $userProfileService);
$this->notificationCenter = new NotificationCenter($this->_logger, $this->_errorHandler);
$this->_projectConfigManager = $configManager;

if ($this->_projectConfigManager === null) {
$this->_projectConfigManager = new StaticProjectConfigManager($datafile, $skipJsonValidation, $this->_logger, $this->_errorHandler);
}
}

/**
Expand Down Expand Up @@ -166,17 +177,14 @@ private function validateUserInputs($attributes, $eventTags = null)
}

/**
* @param string Experiment key
* @param string Variation key
* @param string User ID
* @param array Associative array of user attributes
* @param string Experiment key
* @param string Variation key
* @param string User ID
* @param array Associative array of user attributes
* @param DatafileProjectConfig DatafileProjectConfig instance
*/
protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes)
protected function sendImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes)
{
// TODO: Config should be passed as param when this is called from activate but
// since PHP is single-threaded we can leave this for now.
$config = $this->getConfig();

$impressionEvent = $this->_eventBuilder
->createImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes);
$this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey));
Expand Down Expand Up @@ -254,7 +262,7 @@ public function activate($experimentKey, $userId, $attributes = null)
return null;
}

$this->sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes);
$this->sendImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes);

return $variationKey;
}
Expand Down Expand Up @@ -527,7 +535,7 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null)
'variationKey'=> $variationKey
);

$this->sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes);
$this->sendImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes);
} else {
$this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'.");
}
Expand Down
245 changes: 245 additions & 0 deletions src/Optimizely/ProjectConfigManager/HTTPProjectConfigManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<?php
/**
* Copyright 2019, Optimizely Inc and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Optimizely\ProjectConfigManager;

use Exception;
use GuzzleHttp\Client as HttpClient;
use Monolog\Logger;
use Optimizely\Config\DatafileProjectConfig;
use Optimizely\Enums\ProjectConfigManagerConstants;
use Optimizely\ErrorHandler\NoOpErrorHandler;
use Optimizely\Logger\NoOpLogger;
use Optimizely\Utils\Validator;

class HTTPProjectConfigManager implements ProjectConfigManagerInterface
{
/**
* @var \GuzzleHttp\Client Guzzle HTTP client to send requests.
*/
private $httpClient;

/**
* @var DatafileProjectConfig
*/
private $_config;

/**
* @var String Datafile URL.
*/
private $_url;

/**
* @var boolean Flag indicates that skip JSON validation of datafile.
*/
private $_skipJsonValidation;

/**
* @var String datafile last modified time.
*/
private $_lastModifiedSince;

/**
* @var LoggerInterface Logger instance.
*/
private $_logger;

/**
* @var ErrorHandlerInterface ErrorHandler instance.
*/
private $_errorHandler;

public function __construct(
$sdkKey = null,
$url = null,
$urlTemplate = null,
$fetchOnInit = true,
$datafile = null,
$skipJsonValidation = false,
$logger = null,
$errorHandler = null
) {
$this->_skipJsonValidation = $skipJsonValidation;
$this->_logger = $logger;
$this->_errorHandler = $errorHandler;
$this->httpClient = new HttpClient();

if ($this->_logger === null) {
$this->_logger = new NoOpLogger();
}

if ($this->_errorHandler === null) {
$this->_errorHandler = new NoOpErrorHandler();
}

$this->_url = $this->getUrl($sdkKey, $url, $urlTemplate);

if ($datafile !== null) {
$this->_config = DatafileProjectConfig::createProjectConfigFromDatafile(
$datafile,
$skipJsonValidation,
$this->_logger,
$this->_errorHandler
);
}

// Update config on initialization.
if ($fetchOnInit === true) {
$this->fetch();
}
}

/**
* Helper function to return URL based on params passed.
*
* @param $sdkKey string SDK key.
* @param $url string URL for datafile.
* @param $urlTemplate string Template to be used with SDK key to fetch datafile.
*
* @return string URL for datafile.
*/
protected function getUrl($sdkKey, $url, $urlTemplate)
{
if (Validator::validateNonEmptyString($url)) {
return $url;
}

if (!Validator::validateNonEmptyString($sdkKey)) {
$exception = new Exception("One of the SDK key or URL must be provided.");
$this->_errorHandler->handleError($exception);
throw $exception;
}

if (!Validator::validateNonEmptyString($urlTemplate)) {
$urlTemplate = ProjectConfigManagerConstants::DEFAULT_URL_TEMPLATE;
}

$url = sprintf($urlTemplate, $sdkKey);

return $url;
}

/**
* Function to fetch latest datafile.
*
* @return boolean flag to indicate if datafile is updated.
*/
public function fetch()
{
$datafile = $this->fetchDatafile();

if ($datafile === null) {
return false;
}

return true;
}

/**
* Helper function to fetch datafile and handle response if datafile is modified.
*
* @return null|datafile.
*/
protected function fetchDatafile()
{
$headers = null;

// Add If-Modified-Since header.
if (Validator::validateNonEmptyString($this->_lastModifiedSince)) {
$headers = array(ProjectConfigManagerConstants::IF_MODIFIED_SINCE => $this->_lastModifiedSince);
}

$options = [
'headers' => $headers,
'timeout' => ProjectConfigManagerConstants::TIMEOUT,
'connect_timeout' => ProjectConfigManagerConstants::TIMEOUT
];

try {
$response = $this->httpClient->get($this->_url, $options);
} catch (Exception $exception) {
$this->_logger->log(Logger::ERROR, 'Unexpected response when trying to fetch datafile, status code: ' . $exception->getCode());
return null;
}

$status = $response->getStatusCode();

// Datafile not updated.
if ($status === 304) {
$this->_logger->log(Logger::DEBUG, 'Not updating ProjectConfig as datafile has not updated since ' . $this->_lastModifiedSince);
return null;
}

// Datafile retrieved successfully.
if ($status >= 200 && $status < 300) {
if ($response->hasHeader(ProjectConfigManagerConstants::LAST_MODIFIED)) {
$this->_lastModifiedSince = $response->getHeader(ProjectConfigManagerConstants::LAST_MODIFIED)[0];
}

$datafile = $response->getBody();

if ($this->handleResponse($datafile) === true) {
return $datafile;
}

return null;
}

// Failed to retrieve datafile from Url.
$this->_logger->log(Logger::ERROR, 'Unexpected response when trying to fetch datafile, status code: ' . $status);
return null;
}

/**
* Helper function to create config from datafile.
*
* @param string $datafile
* @return boolean flag to indicate if config is updated.
*/
protected function handleResponse($datafile)
{
if ($datafile === null) {
return false;
}

$config = DatafileProjectConfig::createProjectConfigFromDatafile($datafile, $this->_skipJsonValidation, $this->_logger, $this->_errorHandler);
if ($config === null) {
return false;
}

$previousRevision = null;
if ($this->_config !== null) {
$previousRevision = $this->_config->getRevision();
}

if ($previousRevision === $config->getRevision()) {
return false;
}

$this->_config = $config;
return true;
}

/**
* Returns instance of DatafileProjectConfig.
* @return null|DatafileProjectConfig DatafileProjectConfig instance.
*/
public function getConfig()
{
return $this->_config;
}
}

0 comments on commit 9697acc

Please sign in to comment.