Skip to content

Commit

Permalink
feat(track): unique visitors and conversion tracking; track goals glo…
Browse files Browse the repository at this point in the history
…bally
  • Loading branch information
softvar committed Mar 3, 2021
1 parent 065b57c commit 2b04dd5
Show file tree
Hide file tree
Showing 8 changed files with 454 additions and 137 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
php-versions: ['5.6', '7.0', '7.1', '7.2', '7.3']
php-versions: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4']
name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
steps:
- name: Checkout
Expand Down
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,67 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.13.0] - 2021-03-02

### Changed

- Update track API to support tracking a goal globally across campaigns with the same `goalIdentififer` and corresponding changes in `VWO` class constructor.

```php
// it will track goal having `goalIdentifier` of campaign having `campaignKey` for the user having `userId` as id.
$vwoClientInstance->track("campaignKey", $goalIdentifier, $userId, $options);
// it will track goal having `goalIdentifier` of campaigns having `campaignKey1` and `campaignKey2` for the user having `userId` as id.
$vwoClientInstance->track(["campaignKey1", "campaignKey2"], $goalIdentifier, $userId, $options);
// it will track goal having `goalIdentifier` of all the campaigns
$vwoClientInstance->track(null, $goalIdentifier, $userId, $options);
//Read more about configuration and usage - https://developers.vwo.com/reference#server-side-sdk-track
```

- If User Storage Service is provided, do not track same visitor multiple times.

You can pass `shouldTrackReturningUser` as `true` in case you prefer to track duplicate visitors.

```php
$options = [
"shouldTrackReturningUser" => true
];

$vwoClientInstance->activate($campaignKey, $userId, $options);
```

Or, you can also pass `shouldTrackReturningUser` at the time of instantiating VWO SDK client. This will avoid passing the flag in different API calls.

```php
$config=[
'settingsFile' => $settingsFile,
'shouldTrackReturningUser' => true
];

$vwoClientInstance = new VWO($config);
```

If `shouldTrackReturningUser` param is passed at the time of instantiating the SDK as well as in the API options as mentioned above, then the API options value will be considered.

- If User Storage Service is provided, campaign activation is mandatory before tracking any goal, getting variation of a campaign, and getting value of the feature's variable.

**Correct Usage**

```php
$vwoClientInstance->activate($campaignKey, $userId, $options);
$vwoClientInstance->track($campaignKey, $userId, $goalIdentifier, $options);
```

**Wrong Usage**

```php
// Calling track API before activate API
// This will not track goal as campaign has not been activated yet.
$vwoClientInstance->track($campaignKey, $userId, $goalIdentifier, $options);

// After calling track APi
$vwoClientInstance->activate($campaignKey, $userId, $options);
```

## [1.12.0] - 2020-02-19

### Changed
Expand Down
13 changes: 10 additions & 3 deletions src/Constants/LogMessages.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class LogMessages
'UUID_FOR_USER' => '({file}): Uuid generated for userId:{userid} and accountId:{accountId} is {desiredUuid}',
'FEATURE_FLAG_NOT_LINKED' => '({file}): Feature:{featureKey} is not linked to any running campaigns',
'USER_HASH_BUCKET_VALUE' => '({file}): User ID:{userId} having hash:{hashValue} got bucketValue:{bucketValue}',
'VARIATION_HASH_BUCKET_VALUE' => '({file}): User ID:{userId} for campaign:{campaignKey} having percent traffic:{percentTraffic} got bucket value:{bucketValue}'
'VARIATION_HASH_BUCKET_VALUE' => '({file}): User ID:{userId} for campaign:{campaignKey} having percent traffic:{percentTraffic} got bucket value:{bucketValue}',
'CAMPAIGN_NOT_ACTIVATED' => '({file}): Campaign:{campaignKey} for User ID:{userId} is not yet activated for API:{api}. Use activate API to activate A/B test or isFeatureEnabled API to activate Feature Test.'
];

/**
Expand Down Expand Up @@ -82,6 +83,8 @@ class LogMessages
'WHITELISTING_SKIPPED' => '({file}): For userId:{userId} of campaign:{campaignKey},{reason} whitelisting was skipped {variation}',
'SEGMENTATION_SKIPPED' => '({file}): For userId:{userId} of campaign:{campaignKey}, segment was missing, hence skipping segmentation{variation}',
'SEGMENTATION_STATUS' => '({file}): For userId:{userId} of campaign:{campaignKey} with variables:{customVariables} {status} {segmentationType} {variation}',
'GOAL_ALREADY_TRACKED' => '({file}): "Goal:{goalIdentifer} of Campaign:{campaignKey} for User ID:{userId} has already been tracked earlier. Skipping now',
'CAMPAIGN_NOT_ACTIVATED' => '({file}): Activate the campaign:{campaignKey} for User ID:{userId} to {reason}.'

];
/**
Expand All @@ -97,7 +100,7 @@ class LogMessages
'ACTIVATE_API_CONFIG_CORRUPTED' => '({file}): "activate" API has corrupted configuration',
'GET_VARIATION_API_MISSING_PARAMS' => '({file}): "getVariation" API got bad parameters. It expects campaignKey(String) as first and userId(String/Number) as second argument',
'GET_VARIATION_API_CONFIG_CORRUPTED' => '({file}): "getVariation" API has corrupted configuration',
'TRACK_API_MISSING_PARAMS' => '({file}): "track" API got bad parameters. It expects campaignKey(String) as first, userId(String/Number) as second and goalIdentifier (string) as third argument. options is revenueValue(Float/Number/String) and is required for revenue goal only.',
'TRACK_API_MISSING_PARAMS' => '({file}): "track" API got bad parameters. It expects campaignKey(null/String/array) as first, userId(String/Number) as second and goalIdentifier (string) as third argument. options is revenueValue(Float/Number/String) and is required for revenue goal only.',
'TRACK_API_CONFIG_CORRUPTED' => '({file}): "track" API has corrupted configuration',
'TRACK_API_GOAL_NOT_FOUND' => '({file}): Goal not found for campaign:{campaignKey} and userId:{userId}',
'TRACK_API_VARIATION_NOT_FOUND' => '({file}): Variation not found for campaign:{campaignKey} and userId:{userId}',
Expand All @@ -117,6 +120,10 @@ class LogMessages
'TAG_VALUE_CORRUPTED' => '({file}): Invalid tagValue:{tagValue} passed to {method} of this file',
'INVALID_API_CALL' => '({file}): {api} API is not valid for user ID: {userId} in Campaign Key: {campaignKey} having campaign type: {campaignType}',
'ACTIVATE_API_MISSING_PARAMS' => '({file}): "activate" API got bad parameters. It expects campaignKey(String) as first, userId(String) as second and options(optional Object) as third argument',
'SEGMENTATION_ERROR' => '({file}): Error while segmenting the userId:{userId} of campaignKey:{campaignKey}{variation} with customVariables:{customVariables}. Error message: {err}'
'SEGMENTATION_ERROR' => '({file}): Error while segmenting the userId:{userId} of campaignKey:{campaignKey}{variation} with customVariables:{customVariables}. Error message: {err}',
'NO_CAMPAIGN_FOUND' => '({file}): No campaign found for goalIdentifier:{goalIdentifier}. Please verify from VWO app.',
'INVALID_TRACK_RETURNING_USER_VALUE' => '{{file}): shouldTrackReturningUser should be boolean',
'INVALID_GOAL_TYPE' => '({file}): goalTypeToTrack should be certain strings',
'USER_ALREADY_TRACKED' => '({file}): "User ID:{userId} for Campaign:{campaignKey} has already been tracked earlier for "{api}" API. Skipping now'
];
}
39 changes: 32 additions & 7 deletions src/Core/VariationDecider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

use Exception as Exception;
use Monolog\Logger as Logger;
use vwo\Constants\CampaignTypes;
use vwo\Utils\Common as CommonUtil;
use vwo\Utils\Campaign as CampaignUtil;
use vwo\Services\LoggerService;
Expand All @@ -29,13 +30,15 @@

class VariationDecider
{
public $hasStoredVariation;

/**
* @param $campaign
* @param $usesrId
* @param array $options
* @return array|mixed|null
*/
public function fetchVariationData($userStorageObj, $campaign, $userId, $options = [], $apiName = '')
public function fetchVariationData($userStorageObj, $campaign, $userId, $options = [], $apiName = '', $goalIdentifier = '')
{
LoggerService::setApiName($apiName);
$bucketInfo = null;
Expand All @@ -57,6 +60,23 @@ public function fetchVariationData($userStorageObj, $campaign, $userId, $options
LogMessages::DEBUG_MESSAGES['NO_STORED_VARIATION'],
['{userId}' => $userId, '{campaignKey}' => $campaignKey]
);
if (
in_array($apiName, ['track', 'getVariationName', 'getFeatureVariableValue']) &&
!empty($userStorageObj) &&
$campaign['type'] != CampaignTypes::FEATURE_ROLLOUT
) {
LoggerService::log(
Logger::DEBUG,
LogMessages::DEBUG_MESSAGES['CAMPAIGN_NOT_ACTIVATED'],
['{userId}' => $userId, '{campaignKey}' => $campaignKey, '{api}' => $apiName]
);
LoggerService::log(
Logger::INFO,
LogMessages::INFO_MESSAGES['CAMPAIGN_NOT_ACTIVATED'],
['{userId}' => $userId, '{campaignKey}' => $campaignKey, '{reason}' => $apiName === 'track' ? 'track it' : 'get the decision/value']
);
return $bucketInfo;
}

//check for pre-segmentation if applied
$result = ValidationsUtil::checkPreSegmentation($campaign, $userId, $options);
Expand All @@ -79,8 +99,9 @@ public function fetchVariationData($userStorageObj, $campaign, $userId, $options
return $bucketInfo;
}

$this->userStorageSet($userStorageObj, $userId, $campaign['key'], $bucketInfo);
$this->userStorageSet($userStorageObj, $userId, $campaign['key'], $bucketInfo, $goalIdentifier);
} else {
$this->hasStoredVariation = true;
LoggerService::log(
Logger::DEBUG,
LogMessages::DEBUG_MESSAGES['GETTING_STORED_VARIATION'],
Expand Down Expand Up @@ -118,10 +139,14 @@ private function userStorageGet($userStorageObj, $userId, $campaign)
['{userId}' => $userId]
);
if ($campaign !== null) {
return $bucketInfo = Bucketer::getBucketVariationId(
$bucketInfo = Bucketer::getBucketVariationId(
$campaign,
$variationInfo['variationName']
);
if (isset($variationInfo['goalIdentifier'])) {
$bucketInfo['goalIdentifier'] = $variationInfo['goalIdentifier'];
}
return $bucketInfo;
}
} else {
LoggerService::log(Logger::ERROR, LogMessages::ERROR_MESSAGES['GET_USER_STORAGE_SERVICE_FAILED'], ['{userId}' => $userId]);
Expand All @@ -134,16 +159,16 @@ private function userStorageGet($userStorageObj, $userId, $campaign)
}

/**
* this function will fetch the data from user-storage
* this function will save the data to user-storage
* @param string $userId
* @param string $campaignKey
* @param array $variation
* @param string $goalIdentifier
*/
private function userStorageSet($userStorageObj, $userId, $campaignKey, $variation)
public function userStorageSet($userStorageObj, $userId, $campaignKey, $variation, $goalIdentifier = '')
{

if (!empty($userStorageObj)) {
$campaignInfo = CommonUtil::getUserCampaignVariationMapping($campaignKey, $variation, $userId);
$campaignInfo = CommonUtil::getUserCampaignVariationMapping($campaignKey, $variation, $userId, $goalIdentifier);
$userStorageObj->set($campaignInfo);
LoggerService::log(
Logger::INFO,
Expand Down
15 changes: 10 additions & 5 deletions src/Utils/Common.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,24 @@ public static function buildLogMessage($message, $params, $className = '', $apiN
/**
* method to create the input array for user-storage set function
*
* @param $campaignKey
* @param $bucketInfo
* @param $userId
* @param string $campaignKey
* @param array $variation
* @param string $userId
* @param string $goalIdentifier
* @return array
*/

public static function getUserCampaignVariationMapping($campaignKey, $variation, $userId)
public static function getUserCampaignVariationMapping($campaignKey, $variation, $userId, $goalIdentifier)
{
return [
$data = [
'userId' => $userId,
'variationName' => $variation['name'],
'campaignKey' => $campaignKey,
];
if ($goalIdentifier) {
$data['goalIdentifier'] = $goalIdentifier;
}
return $data;
}


Expand Down
2 changes: 1 addition & 1 deletion src/Utils/ImpressionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ImpressionBuilder
/**
* sdk version for api hit
*/
const SDK_VERSION = '1.12.0';
const SDK_VERSION = '1.13.0';
/**
* sdk langauge for api hit
*/
Expand Down
77 changes: 76 additions & 1 deletion src/Utils/Validations.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public static function validateUserId($userId)
}

/**
* function to check if the campaignkey exists in campign array from settings
* function to check if the campaignkey exists in campaign array from settings
*
* @param string $campaignKey
* @param array $settings
Expand All @@ -225,4 +225,79 @@ public static function getCampaignFromCampaignKey($campaignKey, $settings)
LoggerService::log(Logger::ERROR, LogMessages::ERROR_MESSAGES['CAMPAIGN_NOT_RUNNING'], ['{campaignKey}' => $campaignKey], self::$CLASSNAME);
return null;
}

/**
* fetch all running campaigns (with campaignKey in $campaignKeys array) from settings
*
* @param array $campaignKeys
* @param array $settings
* @return array
*/
private static function getCampaignsFromCampaignKeys($campaignKeys, $settings)
{
$campaigns = [];
foreach ($campaignKeys as $campaignKey) {
$campaign = self::getCampaignFromCampaignKey($campaignKey, $settings);
if ($campaign) {
$campaigns[] = $campaign;
} else {
$campaigns[] = ['key' => $campaignKey];
}
}
return $campaigns;
}

/**
* fetch all running campaigns (having goal identifier $goalIdentifier and goal type CUSTOM|REVENUE|ALL) from settings
*
* @param array $settings
* @param string $goalIdentifier
* @param string $goalTypeToTrack
* @return array
*/
private static function getCampaignsForGoal($settings, $goalIdentifier, $goalTypeToTrack = 'ALL')
{
$campaigns = [];
if (isset($settings) && isset($settings['campaigns'])) {
foreach ($settings['campaigns'] as $campaign) {
if (isset($campaign['status']) && $campaign['status'] !== 'RUNNING') {
continue;
}
$goal = CommonUtil::getGoalFromGoals($campaign['goals'], $goalIdentifier);
if ($goal && ($goalTypeToTrack === 'ALL' || $goal['type'] === $goalTypeToTrack)) {
$campaigns[] = $campaign;
}
}
}
if (count($campaigns)) {
LoggerService::log(Logger::ERROR, LogMessages::ERROR_MESSAGES['NO_CAMPAIGN_FOUND'], ['{goalIdentifier}' => $goalIdentifier], self::$CLASSNAME);
}
return $campaigns;
}

/**
* fetch campaigns from settings
*
* @param string|array|null $campaignKey
* @param array $settings
* @param string $goalIdentifier
* @param string $goalTypeToTrack
* @return array
*/
public static function getCampaigns($campaignKey, $settings, $goalIdentifier, $goalTypeToTrack = 'ALL')
{
$campaigns = [];
if (!$campaignKey) {
$campaigns = self::getCampaignsForGoal($settings, $goalIdentifier, $goalTypeToTrack);
} elseif (is_array($campaignKey)) {
$campaigns = self::getCampaignsFromCampaignKeys($campaignKey, $settings);
} elseif (is_string($campaignKey)) {
$campaign = self::getCampaignFromCampaignKey($campaignKey, $settings);
if (is_null($campaign)) {
$campaign['key'] = $campaignKey;
}
$campaigns[] = $campaign;
}
return $campaigns;
}
}
Loading

0 comments on commit 2b04dd5

Please sign in to comment.