diff --git a/src/Pluf/AB.php b/src/Pluf/AB.php index 3667f2f..6af59f6 100644 --- a/src/Pluf/AB.php +++ b/src/Pluf/AB.php @@ -164,6 +164,70 @@ class Pluf_AB } /** + * Register a property set for the user. + * + * This allows you to segment your users with these properties. + * + * @param $request Pluf_HTTP_Request + * @param $props array Properties + */ + public static function register($request, $props) + { + $pabuid = (isset($request->pabuid)) ? + $request->pabuid : + self::getUid($request); + if ($pabuid == 'bot') { + return; + } + $request->pabuid = $pabuid; + $request->pabprops = array_merge($request->pabprops, $props); + } + + /** + * Track a funnel. + * + * The array of properties can be used to track different A/B + * testing cases. + * + * The list of properties must be the same at all the steps of the + * funnel, you cannot pass array('gender' => 'M') at step 1 and + * array('age' => 32) at step 2. You need to pass both of them at + * all steps. + * + * @param $funnel string Name of the funnel + * @param $step int Step in the funnel, from 1 to n + * @param $stepname string Readable name for the step + * @param $request Pluf_HTTP_Request Request object + * @param $props array Array of properties associated with the funnel (array()) + */ + public static function trackFunnel($funnel, $step, $stepname, $request, $props=array()) + { + $pabuid = (isset($request->pabuid)) ? + $request->pabuid : + self::getUid($request); + if ($pabuid == 'bot') { + return; + } + $request->pabuid = $pabuid; + $cache = Pluf_Cache::factory(); + $key = 'pluf_ab_funnel_'.crc32($funnel.'#'.$step.'#'.$pabuid); + if ($cache->get($key, false)) { + return; // The key is valid 60s not to track 2 steps within 60s + } + $cache->set($key, '1', 60); + $what = array( + 'f' => $funnel, + 's' => $step, + 'sn' => $stepname, + 't' => (int) gmdate('Ymd', $request->time), + 'u' => $pabuid, + 'p' => array_merge($request->pabprops, $props), + ); + $db = self::getDb(); + $db->funnellogs->insert($what); + } + + /** * Process the response of a view. * * If the request has no cookie and the request has a pabuid, set @@ -179,6 +243,10 @@ class Pluf_AB and $request->pabuid != 'bot') { $response->cookies['pabuid'] = $request->pabuid; } + if (isset($request->pabprops) and count($request->pabprops) + and $request->pabuid != 'bot') { + $response->cookies['pabprops'] = Pluf_Sign::dumps($request->pabprops, null, true); + } return $response; } @@ -196,6 +264,12 @@ class Pluf_AB self::check_uid($request->COOKIE['pabuid'])) { $request->pabuid = $request->COOKIE['pabuid']; } + if (isset($request->COOKIE['pabprops'])) { + try { + $request->pabprops = Pluf_Sign::loads($request->COOKIE['pabprops']); + } catch (Exception $e) { + } + } return false; } diff --git a/src/Pluf/AB/Funnel.php b/src/Pluf/AB/Funnel.php new file mode 100644 index 0000000..7a36587 --- /dev/null +++ b/src/Pluf/AB/Funnel.php @@ -0,0 +1,118 @@ +funnellogs->ensureIndex(array($k => 1), + array('background' => true)); + } + $nf = $db->command(array('distinct' => 'funnellogs', 'key' => 'f')); + if ((int) $nf['ok'] == 1) { + sort($nf['values']); + return $nf['values']; + } + return array(); + } + + /** + * Get stats for a given funnel. + * + * @param $funnel string Funnel + * @param $period string Time period 'yesterday', ('today'), '7days', 'all' + * @param $prop string Property to filter (null) + */ + public static function getStats($funnel, $period='today', $prop=null) + { + $db = Pluf_AB::getDb(); + $steps = array(); + for ($i=1;$i<=20;$i++) { + $steps[$i] = array(); + } + switch ($period) { + case 'yesterday': + $q = array('t' => array('$eq' => (int) gmdate('Ymd', time()-86400))); + break; + case 'today': + $q = array('t' => (int) gmdate('Ymd')); + break; + case '7days': + $q = array('t' => array('$gte' => (int) gmdate('Ymd', time()-604800))); + break; + case 'all': + default: + $q = array(); + break; + } + $q['f'] = $funnel; + $uids = array(); + // With very big logs, we will need to find by schunks, this + // will be very easy to adapt. + foreach ($db->funnellogs->find($q) as $log) { + if (!isset($uids[$log['u'].'##'.$log['s']])) { + $uids[$log['u'].'##'.$log['s']] = true; + $step = $log['s']; + $steps[$step]['name'] = $log['sn']; + if ($prop and !isset($steps[$step]['props'])) { + $steps[$step]['props'] = array(); + } + $steps[$step]['total'] = (isset($steps[$step]['total'])) ? + $steps[$step]['total'] + 1 : 1; + if ($prop) { + $steps[$step]['props'][$log['p'][$prop]] = (isset($steps[$step]['props'][$log['p'][$prop]])) ? + $steps[$step]['props'][$log['p'][$prop]] + 1 : 1; + } + } + } + // Now, compile the stats for steps 2 to n + for ($i=2;$i<=20;$i++) { + if ($steps[$i] and $steps[$i-1]) { + //$steps[$i]['conv'] = sprintf('%d', (float)$steps[$i-1]['total']/$steps[$i]['total']*100.0); + $steps[$i]['conv'] = sprintf('%01.2f%%', (float)$steps[$i-1]['total']/$steps[$i]['total']*100.0); + $steps[$i]['conv1'] = sprintf('%01.2f%%', (float)$steps[$i-1]['total']/$steps[1]['total']*100.0); + } + } + + return $steps; + } +} diff --git a/src/Pluf/AB/Views.php b/src/Pluf/AB/Views.php index 745867f..5b6a392 100644 --- a/src/Pluf/AB/Views.php +++ b/src/Pluf/AB/Views.php @@ -87,6 +87,38 @@ class Pluf_AB_Views } /** + * Display the list of funnels. + * + */ + public $funnels_precond = array(array('Pluf_Precondition::hasPerm', + 'Pluf_AB.view-funnels')); + public function funnels($request, $match) + { + $url = Pluf_HTTP_URL_urlForView('pluf_ab_funnels'); + $funnels = Pluf_AB_Funnel::getFunnels(); + return Pluf_Shortcuts_RenderToResponse('pluf/ab/funnels.html', + array('funnels' => $funnels, + ), + $request); + } + + /** + * Display a given funnel stats. + * + */ + public $funnel_precond = array(array('Pluf_Precondition::hasPerm', + 'Pluf_AB.view-funnels')); + public function funnel($request, $match) + { + $stats = Pluf_AB_Funnel::getStats($match[1]); + return Pluf_Shortcuts_RenderToResponse('pluf/ab/funnel.html', + array('stats' => $stats, + 'funnel' => $match[1], + ), + $request); + } + + /** * A simple view to redirect a user and convert it. * * To convert the user for the test 'my_test' and redirect it to diff --git a/src/Pluf/templates/pluf/ab/base.html b/src/Pluf/templates/pluf/ab/base.html index a06b082..72f4a04 100644 --- a/src/Pluf/templates/pluf/ab/base.html +++ b/src/Pluf/templates/pluf/ab/base.html @@ -13,7 +13,7 @@ body { background: #fff; font-family: Lucida Grande, Verdana, sans-serif; padding: 1em 2em; - margin-left: 100px; + magrin-left: 100px; width: 600px; } @@ -66,10 +66,28 @@ pre { font-size: 0.8em; } +.right { + text-align: right; +} + p.note { margin-top: 2em; } +.percent { + display: block; + float: left; + height: 10px; + width: 100%; + background-color: #e9b96e; + border: 1px solid #888a85; +} +.percent span { + background-color: #3465a4; + display:block; + float:left; + height:10px; +} {/literal} diff --git a/src/Pluf/templates/pluf/ab/funnel.html b/src/Pluf/templates/pluf/ab/funnel.html new file mode 100644 index 0000000..fa8c812 --- /dev/null +++ b/src/Pluf/templates/pluf/ab/funnel.html @@ -0,0 +1,39 @@ +{extends "pluf/ab/base.html"} + +{block body} +

Funnel {$funnel}

+{assign $i=1} +{foreach $stats as $step}{if $step} +{if $i>1} +

{$step['total']} ({$step['conv']})

+{/if} +

Step {$i}: {$step['name']}

+ +

+{if $i>1} +  +{else} + +  +{/if}
{assign $t = $step['total']} +{blocktrans $t}{$t} unique visitor.{plural}{$t} uniques visitors.{/blocktrans} +

+ +{assign $i += 1} +{/if}{/foreach} + +
+ +{assign $i = $i-1} +

{$stats[$i]['total']} out of {$stats['1']['total']} visitors reached step {$i} of this funnel. +This is a completion rate of {$stats[$i]['conv1']}.

+ + +

+Note that if a user skip a given step, this can make the results a bit +off. +

+ + +{/block} + diff --git a/src/Pluf/templates/pluf/ab/funnels.html b/src/Pluf/templates/pluf/ab/funnels.html new file mode 100644 index 0000000..633c61b --- /dev/null +++ b/src/Pluf/templates/pluf/ab/funnels.html @@ -0,0 +1,15 @@ +{extends "pluf/ab/base.html"} + +{block body} +

Funnel Dashboard

+ +{if count($funnels)} + +{else} +

You no funnel analysis running at the moment.

+{/if} + +{/block} +