Trial->Paid conversion tracking with GTM

* Added trial to paid conversion tracking with GTM
* Removed WHMCS roundtrip from Showbuilder
* Moved all Analytics code into common/GoogleAnalytics.php
* Added a new Thank You page after plan changes to capture conversions
* Added a ConversionTracking plugin to facilitate that
* Also backported some minor staticBaseDir compatibility changes
* Fixed a logic error in creating the baseDir
This commit is contained in:
Albert Santoni 2015-03-24 10:11:25 -04:00
parent 7b9efb988f
commit 3d03f837d2
11 changed files with 222 additions and 102 deletions

View file

@ -24,12 +24,14 @@ require_once "FileIO.php";
require_once "OsPath.php"; require_once "OsPath.php";
require_once "Database.php"; require_once "Database.php";
require_once "ProvisioningHelper.php"; require_once "ProvisioningHelper.php";
require_once "GoogleAnalytics.php";
require_once "Timezone.php"; require_once "Timezone.php";
require_once "Auth.php"; require_once "Auth.php";
require_once __DIR__.'/forms/helpers/ValidationTypes.php'; require_once __DIR__.'/forms/helpers/ValidationTypes.php';
require_once __DIR__.'/forms/helpers/CustomDecorators.php'; require_once __DIR__.'/forms/helpers/CustomDecorators.php';
require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php'; require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php';
require_once __DIR__.'/controllers/plugins/Maintenance.php'; require_once __DIR__.'/controllers/plugins/Maintenance.php';
require_once __DIR__.'/controllers/plugins/ConversionTracking.php';
require_once __DIR__.'/modules/rest/controllers/ShowImageController.php'; require_once __DIR__.'/modules/rest/controllers/ShowImageController.php';
require_once __DIR__.'/modules/rest/controllers/MediaController.php'; require_once __DIR__.'/modules/rest/controllers/MediaController.php';
@ -52,6 +54,7 @@ Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance());
$front = Zend_Controller_Front::getInstance(); $front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new RabbitMqPlugin()); $front->registerPlugin(new RabbitMqPlugin());
$front->registerPlugin(new Zend_Controller_Plugin_ConversionTracking());
$front->throwExceptions(false); $front->throwExceptions(false);
//localization configuration //localization configuration

View file

@ -0,0 +1,90 @@
<?php
class Application_Common_GoogleAnalytics
{
/** Returns a string containing the JavaScript code to pass some billing account info
* into Google Tag Manager / Google Analytics, so we can track things like the plan type.
*/
public static function generateGoogleTagManagerDataLayerJavaScript()
{
$code = "";
try {
$clientId = Application_Model_Preference::GetClientId();
$plan = Application_Model_Preference::GetPlanLevel();
$isTrial = ($plan == "trial");
//Figure out how long the customer has been around using a mega hack.
//(I'm avoiding another round trip to WHMCS for now...)
//We calculate it based on the trial end date...
$trialEndDateStr = Application_Model_Preference::GetTrialEndingDate();
if ($trialEndDateStr == '') {
$accountDuration = 0;
} else {
$today = new DateTime();
$trialEndDate = new DateTime($trialEndDateStr);
$trialDuration = new DateInterval("P30D"); //30 day trial duration
$accountCreationDate = $trialEndDate->sub($trialDuration);
$interval = $today->diff($accountCreationDate);
$accountDuration = $interval->days;
}
$code = "$( document ).ready(function() {
dataLayer.push({
'UserID': '" . $clientId . "',
'Customer': 'Customer',
'PlanType': '" . $plan . "',
'Trial': '" . $isTrial . "',
'AccountDuration': '" . strval($accountDuration) . "'
});
});";
//No longer sending these variables because we used to make a query to WHMCS
//to fetch them, which was slow.
// 'ZipCode': '" . $postcode . "',
// 'Country': '" . $country . "',
} catch (Exception $e) {
Logging::error($e);
return "";
}
return $code;
}
/** Generate the JavaScript snippet that logs a trial to paid conversion */
public static function generateConversionTrackingJavaScript()
{
$newPlan = Application_Model_Preference::GetPlanLevel();
$oldPlan = Application_Model_Preference::GetOldPlanLevel();
$code = "dataLayer.push({'event': 'Conversion',
'Conversion': 'Trial to Paid',
'Old Plan' : '$oldPlan',
'New Plan' : '$newPlan'});";
return $code;
}
/** Return true if the user used to be on a trial plan and was just converted to a paid plan. */
public static function didPaidConversionOccur($request)
{
$userInfo = Zend_Auth::getInstance()->getStorage()->read();
if ($userInfo) {
$user = new Application_Model_User($userInfo->id);
} else {
return;
}
$oldPlan = Application_Model_Preference::GetOldPlanLevel();
if ($user->isSuperAdmin() && $request->getControllerKey() !== "thank-you")
{
//Only tracking trial->paid conversions for now.
if ($oldPlan == "trial")
{
return true;
}
}
return false;
}
}

View file

@ -27,7 +27,7 @@ class Application_Common_HTTPHelper
if (empty($baseDir)) { if (empty($baseDir)) {
$baseDir = "/"; $baseDir = "/";
} }
if ($baseDir[0] != "") { if ($baseDir[0] != "/") {
$baseDir = "/" . $baseDir; $baseDir = "/" . $baseDir;
} }

View file

@ -36,6 +36,7 @@ $ccAcl->add(new Zend_Acl_Resource('library'))
->add(new Zend_Acl_Resource('rest:media')) ->add(new Zend_Acl_Resource('rest:media'))
->add(new Zend_Acl_Resource('rest:show-image')) ->add(new Zend_Acl_Resource('rest:show-image'))
->add(new Zend_Acl_Resource('billing')) ->add(new Zend_Acl_Resource('billing'))
->add(new Zend_Acl_Resource('thank-you'))
->add(new Zend_Acl_Resource('provisioning')); ->add(new Zend_Acl_Resource('provisioning'));
/** Creating permissions */ /** Creating permissions */
@ -69,6 +70,7 @@ $ccAcl->allow('G', 'index')
->allow('A', 'user') ->allow('A', 'user')
->allow('A', 'systemstatus') ->allow('A', 'systemstatus')
->allow('A', 'preference') ->allow('A', 'preference')
->allow('S', 'thank-you')
->allow('S', 'billing'); ->allow('S', 'billing');

View file

@ -44,6 +44,13 @@ class Config {
$CC_CONFIG['dev_env'] = 'production'; $CC_CONFIG['dev_env'] = 'production';
} }
//Backported static_base_dir default value into saas for now.
if (array_key_exists('static_base_dir', $values['general'])) {
$CC_CONFIG['staticBaseDir'] = $values['general']['static_base_dir'];
} else {
$CC_CONFIG['staticBaseDir'] = '/';
}
// Parse separate conf file for cloud storage values // Parse separate conf file for cloud storage values
$cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf"; $cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf";
if (!file_exists($cloudStorageConfig)) { if (!file_exists($cloudStorageConfig)) {

View file

@ -35,7 +35,7 @@ class ShowbuilderController extends Zend_Controller_Action
$user = Application_Model_User::GetCurrentUser(); $user = Application_Model_User::GetCurrentUser();
$userType = $user->getType(); $userType = $user->getType();
$this->view->headScript()->appendScript("localStorage.setItem( 'user-type', '$userType' );"); $this->view->headScript()->appendScript("localStorage.setItem( 'user-type', '$userType' );");
$this->view->headScript()->appendScript($this->generateGoogleTagManagerDataLayerJavaScript()); $this->view->headScript()->appendScript(Application_Common_GoogleAnalytics::generateGoogleTagManagerDataLayerJavaScript());
$this->view->headScript()->appendFile($baseUrl.'js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/datatables/js/jquery.dataTables.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/js/jquery.dataTables.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
@ -367,104 +367,5 @@ class ShowbuilderController extends Zend_Controller_Action
throw new Exception("this controller is/was a no-op please fix your throw new Exception("this controller is/was a no-op please fix your
code"); code");
} }
/** Returns a string containing the JavaScript code to pass some billing account info
* into Google Tag Manager / Google Analytics, so we can track things like the plan type.
*/
private static function generateGoogleTagManagerDataLayerJavaScript()
{
$code = "";
try
{
$accessKey = $_SERVER["WHMCS_ACCESS_KEY"];
$username = $_SERVER["WHMCS_USERNAME"];
$password = $_SERVER["WHMCS_PASSWORD"];
$url = "https://account.sourcefabric.com/includes/api.php?accesskey=" . $accessKey; # URL to WHMCS API file goes here
$postfields = array();
$postfields["username"] = $username;
$postfields["password"] = md5($password);
$postfields["action"] = "getclientsdetails";
$postfields["stats"] = true;
$postfields["clientid"] = Application_Model_Preference::GetClientId();
$postfields["responsetype"] = "json";
$query_string = "";
foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 ); // WHMCS IP whitelist doesn't support IPv6
curl_setopt($ch, CURLOPT_TIMEOUT, 5); //Aggressive 5 second timeout
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $query_string);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$jsondata = curl_exec($ch);
if (curl_error($ch)) {
//die("Connection Error: ".curl_errno($ch).' - '.curl_error($ch));
throw new Exception("WHMCS server down or invalid request.");
}
curl_close($ch);
$arr = json_decode($jsondata); # Decode JSON String
if ($arr->result !== "success") {
Logging::warn("WHMCS API call failed in " . __FUNCTION__);
return;
}
$client = $arr->client;
$stats = $arr->stats;
$currencyCode = $client->currency_code;
//$incomeCents = NumberFormatter::parseCurrency($stats->income, $currencyCode);
$isTrial = true;
if (strpos($stats->income, "0.00") === FALSE) {
$isTrial = false;
}
/*
if ($incomeCents > 0) {
$isTrial = false;
}*/
$plan = Application_Model_Preference::GetPlanLevel();
$country = $client->country;
$postcode = $client->postcode;
//Figure out how long the customer has been around using a mega hack.
//(I'm avoiding another round trip to WHMCS for now...)
//We calculate it based on the trial end date...
$trialEndDateStr = Application_Model_Preference::GetTrialEndingDate();
if ($trialEndDateStr == '') {
$accountDuration = 0;
} else {
$today = new DateTime();
$trialEndDate = new DateTime($trialEndDateStr);
$trialDuration = new DateInterval("P30D"); //30 day trial duration
$accountCreationDate = $trialEndDate->sub($trialDuration);
$interval = $today->diff($accountCreationDate);
$accountDuration = $interval->days;
}
$code = "$( document ).ready(function() {
dataLayer.push({
'ZipCode': '" . $postcode . "',
'UserID': '" . $client->id . "',
'Customer': 'Customer',
'PlanType': '" . $plan . "',
'Trial': '" . $isTrial . "',
'Country': '" . $country . "',
'AccountDuration': '" . strval($accountDuration) . "'
});
});";
}
catch (Exception $e)
{
return "";
}
return $code;
}
} }

View file

@ -0,0 +1,48 @@
<?php
class ThankYouController extends Zend_Controller_Action
{
public function indexAction()
{
//Variable for the template (thank-you/index.phtml)
$this->view->stationUrl = Application_Common_HTTPHelper::getStationUrl();
$this->view->conversionUrl = Application_Common_HTTPHelper::getStationUrl() . 'thank-you/confirm-conversion';
$this->view->gaEventTrackingJsCode = ""; //Google Analytics event tracking code that logs an event.
// Embed the Google Analytics conversion tracking code if the
// user is a super admin and old plan level is set to trial.
if (Application_Common_GoogleAnalytics::didPaidConversionOccur($this->getRequest())) {
$this->view->gaEventTrackingJsCode = Application_Common_GoogleAnalytics::generateConversionTrackingJavaScript();
}
$csrf_namespace = new Zend_Session_Namespace('csrf_namespace');
$csrf_element = new Zend_Form_Element_Hidden('csrf');
$csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label');
$csrf_form = new Zend_Form();
$csrf_form->addElement($csrf_element);
$this->view->form = $csrf_form;
}
/** Confirm that a conversion was tracked. */
public function confirmConversionAction()
{
$this->view->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
$current_namespace = new Zend_Session_Namespace('csrf_namespace');
$observed_csrf_token = $this->_getParam('csrf_token');
$expected_csrf_token = $current_namespace->authtoken;
if($observed_csrf_token != $expected_csrf_token) {
Logging::info("Invalid CSRF token");
return;
}
if ($this->getRequest()->isPost()) {
Logging::info("Goal conversion from trial to paid.");
// Clear old plan level so we prevent duplicate events.
// This should only be called from AJAX. See thank-you/index.phtml
Application_Model_Preference::ClearOldPlanLevel();
}
}
}

View file

@ -0,0 +1,21 @@
<?php
class Zend_Controller_Plugin_ConversionTracking extends Zend_Controller_Plugin_Abstract
{
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
//If user is a super admin and old plan level is set to trial....
if (Application_Common_GoogleAnalytics::didPaidConversionOccur($request))
{
//Redirect to Thank you page, unless the request was already going there...
if ($request->getControllerName() != 'thank-you')
{
$request->setModuleName('default')
->setControllerName('thank-you')
->setActionName('index')
->setDispatched(true);
}
}
}
}

View file

@ -824,7 +824,10 @@ class Application_Model_Preference
public static function SetPlanLevel($plan) public static function SetPlanLevel($plan)
{ {
$oldPlanLevel = GetPlanLevel();
self::setValue("plan_level", $plan); self::setValue("plan_level", $plan);
//We save the old plan level temporarily to facilitate conversion tracking
self::setValue("old_plan_level", $oldPlanLevel);
} }
public static function GetPlanLevel() public static function GetPlanLevel()
@ -837,6 +840,19 @@ class Application_Model_Preference
return $plan; return $plan;
} }
public static function GetOldPlanLevel()
{
$oldPlan = self::getValue("old_plan_level");
return $oldPlan;
}
/** Clearing the old plan level indicates a change in your plan has been tracked (Google Analytics) */
public static function ClearOldPlanLevel()
{
self::setValue("old_plan_level", '');
}
public static function SetTrialEndingDate($date) public static function SetTrialEndingDate($date)
{ {
self::setValue("trial_end_date", $date); self::setValue("trial_end_date", $date);

View file

@ -0,0 +1,20 @@
<script type="text/javascript">
$(document).ready(function() {
<?php if ($this->gaEventTrackingJsCode != "") {
echo($this->gaEventTrackingJsCode);
?>
jQuery.post("<?=$this->conversionUrl?>", { "csrf_token" : $("#csrf").attr('value')},
function( data ) {});
<?php }; //endif ?>
});
</script>
<?php echo $this->form->getElement('csrf') ?>
<div class="ui-widget ui-widget-content block-shadow clearfix padded-strong thankyou-panel">
<center>
<div class="logobox" style="margin-left: 32px;"></div>
<h2><?php echo _pro("Thank you!")?></h2>
<h3><?php echo _pro("Your station has been upgraded successfully.")?></h3>
<p><a href="<?=$this->stationUrl?>"><?php echo _pro("Return to Airtime")?></a></p>
</center>
</div>

View file

@ -3141,3 +3141,15 @@ dd .stream-status {
.quota-reached { .quota-reached {
font-size: 14px !important; font-size: 14px !important;
} }
.thankyou-panel
{
width: 400px;
margin: 0 auto;
margin-bottom: 30px;
}
.thankyou-panel h3
{
color: #222;
}