Have you ever wanted to have a smart rotator sending traffic to your best path?

That's exactly what we're going to do in this tutorial!

First, let's define what we call the best path for a funnel such as the one below:

The best path, is the one giving us the highest revenue per view. If on average you earn $0.37 for each view of offer 1, while you earn $0.45 for each view of offer 3, you will want to show offer 3 to your visitors more often than offer 1. Granted you have reached statistical significance.

You can see the earning per view (EPV) by enabling the following heatmap:

And you can see how confident you are that an offer will bring more revenue than others by enabling the EPv Winners calculator:

In this example, we are only 49% confident that offer 3 will be more profitable than the other offers.

What we will do is take this EPv winner rate to automatically split the traffic to these offers. 

In the example above, since we are about 26% confident that offer 1 and offer 2 will outperform the others, and about 49% confident that offer 3 will be the best performer, we will route 26% of traffic to offer 1, 26% to offer 2 and the rest to offer 3.

The more confident FunnelFlux's Bayes calculator is that an offer is going to outperform others, the more percentage of traffic it will send toward it.

So here are the steps to put this in place.

First, you have to split the traffic via a PHP node instead of a regular rotator. Let's call this PHP Node "EPV Weight Auto-Routing"

Here is the code that you will use in that PHP node:

<?php

require_once __DIR__ . './../admin/api/v2/toolbox.php';

$cacheExpirationInMinutes = 5;
$checkLastHours = '[[epv-auto-routing-lasthours]]';
$checkLastHours = empty($checkLastHours) ? 24 : $checkLastHours;

$cacheKey = '{node-id}-{trafficsource-id}';
$cache = Cache::getInstance( Cache::TYPE_FILE );
$nodeWeights = $cache->get( $cacheKey );
if( $nodeWeights === Cache::NOT_FOUND )
{
  $nodeWeights = [];
  $nodeConnections = DBTableCampaignFunnelConnections::getNodeExitConnections( '{node-id}' );
  if( !empty( $nodeConnections ) )
  {
    $nodeIds = [];
    foreach( $nodeConnections as $connection )
    {
      $targetNodeId = $connection->getTargetNodeId();
      $nodeIds[] = $targetNodeId;
      $nodeWeights[ $targetNodeId ] = -1;
    }

    $to = time();
    $from = $to - ($checkLastHours * 60 * 60);

    $to = (new DateTime())->setTimestamp( $to );
    $from = (new DateTime())->setTimestamp( $from );

    $drilldownRequest = [
      'timeRange' => [
        'start' => [
          'date' => [ 'year' => $from->format('Y'), 'month' => $from->format('m'), 'day' => $from->format('d') ],
          'time' => [ 'hour' => $from->format('H'), 'minutes' => $from->format('i') ]
        ],
        'end' => [
          'date' => [ 'year' => $to->format('Y'), 'month' => $to->format('m'), 'day' => $to->format('d') ],
          'time' => [ 'hour' => $to->format('H'), 'minutes' => $to->format('i') ]
        ],
      ],
      'timeZone'  => [ 'name' => 'UTC' ],
      'options'   => [
        'idFunnelFilter' => '{funnel-id}',
        'idTrafficSourceFilter' => '{trafficsource-id}',
        'computeEPVConfidenceRate' => true,
        'confidenceRateIncludeAll' => true
      ],
      'groupings' => [
        [ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ]
      ]
    ];

    $report = \FluxAPI\v2\Toolbox::statsDrilldownRequest( $drilldownRequest );
    $nodesInReport = 0;
    foreach( $report['rows'] as $row )
    {
      $nodeId = $row['cells'][0]['raw'];
      $epvWinnerRate = $row['epvConfidenceRate']['rate'];
      if( $epvWinnerRate != -1 )
      {
        $nodeWeights[ $nodeId ] = $epvWinnerRate;
        $nodesInReport++;
      }
    }

    // If some nodes have no traffic yet, we need to re-scale the weights of the nodes that did get traffic
    if( $nodesInReport < count($nodeIds) )
    {
      $ratio = $nodesInReport / count($nodeIds);
      $assignedWeight = 0;

      foreach( $nodeWeights as $k => $w )
      {
        if( $nodeWeights[ $k ] != -1 )
        {
          $nodeWeights[ $k ] = $w * $ratio;
          $assignedWeight += $nodeWeights[ $k ];
        }
      }

      $missingWeight = 1.0 - $assignedWeight;
      $missingNodesCount = count($nodeIds) - $nodesInReport;
      if( $missingNodesCount > 0 )
      {
        $missingWeightPerNode = $missingWeight / $missingNodesCount;

        foreach( $nodeIds as $nodeId )
        {
          if( $nodeWeights[ $nodeId ] == -1 )
          {
            $nodeWeights[ $nodeId ] = $missingWeightPerNode;
          }
        }
      }
    }

    $nodeWeights = array_values( $nodeWeights );
  }

  $cache->set( $cacheKey, $nodeWeights, $cacheExpirationInMinutes * 60 );
}

$choice = mt_rand() / mt_getrandmax();
if( empty($nodeWeights) )
{
  $iRoute = -1;
}
else
{
  foreach( $nodeWeights as $iRoute => $weight )
  {
    if( $choice < $weight )
    {
      break;
    }

    $choice -= $weight;
  }
}

return( $iRoute + 1 );

?>


By default, it checks the stats of the past 24 hours. You can change this range by adding a custom token called epv-auto-routing-lasthours in the funnels where you place this PHP node:


Super useful additional notes

  • The connections going out from this PHP node must be sequential and start with "On Done 1". Do not skip numbers. "On Done 1", then "On Done 3" would not be valid.
  • There is some added caching to avoid fetching stats from the API at each visit. By default, it will check the stats and re-calculate the path weights every 5 minutes. You can change this timing by editing this line:
    $cacheExpirationInMinutes = 5;
  • The cache method by default is a local file as we can't guarantee you have memcached on your server. If you do (you definitely should if using FunnelFlux), you can change this line (10 lines in):
    Cache::TYPE_FILE
    to this:
    Cache::TYPE_MEMCACHED
  • You can put any node type after this PHP Node, it doesn't have to be only offers. If you want to place landers after, or a mix of node types, it's perfectly fine. It will still be able to calculate the best performing paths.
  • To reiterate, by default, it checks the stats of the past 24 hours. You can change this range by adding a custom token called epv-auto-routing-lasthours in the funnels where you place this PHP node:

Changing segmentation level

You can alter the segmentation level, i.e. the buckets it optimises within, by changing the JSON payload in the reporting API call used and then cacheKey parameter.

In the above code then report groupings are pulled as below:

'groupings' => [
  [ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ]
]

You could change this to group by another parameter as well, e.g.

'groupings' => [
  [ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ],
  [ 'groupBy' => 'Location: Country Code', 'whitelistFilters' => [ '{location-countrycode}' ] ]
]

After doing this, you could then update cacheKey to:

$cacheKey = '{node-id}-{trafficsource-id}-{location-countrycode}';


Note that the call to the reporting API in the initial code above takes advantage of cached reports, so it's always fast. If some other groupings are added, it may have to load the reports from raw data, which would be slower. 

Best to check within the FunnelFlux drilldown reports page if the specific grouping combination one wants to optimize for is going to be pulled from raw data or not before changing the JSON payload.

Did this answer your question?