Permalink
Browse files

initial commit

  • Loading branch information...
zzhong committed Jul 22, 2011
0 parents commit 8c5ceba212a69048609923c622703f6d8d6f59c7
@@ -0,0 +1,17 @@
+<?php
+/**
+ * Interface for classes that implement A/B logging.
+ *
+ *
+ */
+interface AB2_Logger {
+ /**
+ * Log a selection event. This is called by Test.select() when a non-null
+ * selection has been made.
+ *
+ * @param string $testKey
+ * @param string $variantKey
+ * @param mixed $subjectKey
+ */
+ public function log($testKey, $variantKey, $subjectKey);
+}
@@ -0,0 +1,29 @@
+<?php
+require_once "AB2/Logger.php";
+
+/**
+ * Logger that passes calls through to multiple loggers.
+ *
+ */
+class AB2_Logger_MultiLogger implements AB2_Logger {
+
+ private $loggers;
+
+ public function __construct($loggers) {
+ if (!is_array($loggers)) {
+ throw new InvalidArgumentException('we need an array of loggers here');
+ }
+ $this->loggers = $loggers;
+ }
+
+ public function log($testKey, $variantKey, $subjectKey) {
+ foreach ($this->loggers as $logger) {
+ try {
+ $logger->log($testKey, $variantKey, $subjectKey);
+ } catch (Exception $err) {
+ // if one logger fails, log the error and go on to the next one
+ Logger::log_error("AB Logger failed: $err", 'AB');
+ }
+ }
+ }
+}
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * Logger that maintains a test name -> variant name map of test selections.
+ * Subject IDs are ignored. If a given (test, variant) pair is logged multiple
+ * times with different subject IDs, the values of the last call will be returned
+ * by getMap().
+ *
+ */
+
+class AB2_Logger_TestVarMap implements AB2_Logger {
+
+ /** @var array test name -> variant name map */
+ private $_testVarMap;
+
+ public function __construct() {
+ $this->_testVarMap = array();
+ }
+
+ public function log($testKey, $variantKey, $subjectKey) {
+ $this->_testVarMap[$testKey] = $variantKey;
+ }
+
+ /**
+ * Clear the map.
+ *
+ * @return void
+ */
+ public function clear() {
+ $this->_testVarMap = array();
+ }
+
+ /**
+ * @return array a test name to variant name map. May be empty, but never null.
+ */
+ public function getMap() {
+ return $this->_testVarMap;
+ }
+}
@@ -0,0 +1,29 @@
+<?php
+require_once 'AB2/Logger.php';
+
+/**
+ * A logging filter that ignores duplicate calls.
+ *
+ */
+class AB2_Logger_UniqueEntries implements AB2_Logger {
+
+ private $_log;
+ private $_logger;
+
+ public function __construct($logger) {
+ $this->_log = array();
+ $this->_logger = $logger;
+ }
+
+ public function log($testKey, $variantKey, $subjectKey) {
+ $logParams = "$testKey:$variantKey:$subjectKey";
+ if (!isset($this->_log[$logParams])) {
+ $this->_logger->log($testKey, $variantKey, $subjectKey);
+ $this->_log[$logParams] = true;
+ }
+ }
+
+ public function __toString() {
+ return __CLASS__ . "[$this->_logger]";
+ }
+}
@@ -0,0 +1,11 @@
+<?php
+
+interface AB2_Selector {
+ /**
+ * selects a variant.
+ *
+ * @param string $subjectID
+ * @return variant key (name)
+ */
+ public function select($subjectID);
+}
@@ -0,0 +1,27 @@
+<?php
+
+require_once "AB2/Selector.php";
+
+/**
+ * A variant selector that returns a fixed variant.
+ *
+ */
+class AB2_Selector_Fixed implements AB2_Selector {
+ private $_varName;
+
+ public function __construct($varName) {
+ $this->_varName = $varName;
+ }
+
+ /**
+ * @param mixed $subject
+ * @return string variant name
+ */
+ public function select($subject) {
+ return $this->_varName;
+ }
+
+ public function getVariantName() {
+ return $this->_varName;
+ }
+}
@@ -0,0 +1,73 @@
+<?php
+
+require_once "AB2/Selector/Randomizer.php";
+
+
+/**
+ * This randomizer hashes a (subject ID, test key) pair.
+ *
+ */
+class AB2_Selector_HashRandomizer implements AB2_Selector_Randomizer {
+ private $_algo = 'sha256';
+ private $_testKey;
+ private $_testKeyHash;
+
+ /**
+ * @param string $testKey
+ * @return void
+ */
+ public function __construct($testKey) {
+ $this->_testKey = $testKey;
+ }
+
+ /**
+ * Map a subject (user) ID to a value in the half-open interval [0, 1).
+ *
+ * @param $subjectID
+ * @return float
+ */
+ public function randomize($subjectID) {
+ return !is_null($subjectID) ? $this->hash1($subjectID) : 0;
+ }
+
+ private function hash1($subjectID) {
+ $h = hash($this->_algo, "$this->_testKey-$subjectID");
+ return $this->mapHex($h);
+ }
+
+ private function hash2($subjectID) {
+ $h = hash($this->_algo, "$this->_testKey-$subjectID");
+ $h = hash($this->_algo, $h);
+ $w = $this->mapHex($h);
+ return $w;
+ }
+
+ private function hash3($subjectID) {
+ if (is_null($this->_testKeyHash)) {
+ $this->_testKeyHash = substr(hash($this->_algo, $this->_testKey), 0, 24);
+ }
+ $h = hash($this->_algo, "$this->_testKeyHash-$subjectID");
+ $h = hash($this->_algo, $h);
+ $w = $this->mapHex($h);
+ return $w;
+ }
+
+ /**
+ * Map a hex value to the half-open interval [0, 1) while
+ * preserving uniformity of the input distribution.
+ *
+ * @param string $hex a hex string
+ * @return float
+ */
+ private function mapHex($hex) {
+ $len = min(40, strlen($hex));
+ $vMax = 1 << $len;
+ $v = 0;
+ for ($i = 0; $i < $len; $i++) {
+ $bit = hexdec($hex[$i]) < 8 ? 0 : 1;
+ $v = ($v << 1) + $bit;
+ }
+ $w = $v / $vMax;
+ return $w;
+ }
+}
@@ -0,0 +1,17 @@
+<?php
+
+require_once "AB2/Selector/Randomizer.php";
+
+class AB2_Selector_Random implements AB2_Selector_Randomizer {
+
+ private $_randMax;
+
+ public function __construct() {
+ $this->_randMax = min((1 << 31) - 1, mt_getrandmax());
+ }
+
+ public function randomize($subjectID) {
+ $w = mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax();
+ return $w;
+ }
+}
@@ -0,0 +1,11 @@
+<?php
+
+interface AB2_Selector_Randomizer {
+ /**
+ * Map a subject (user) ID to a value in the half-open interval [0, 1).
+ *
+ * @param $subjectID
+ * @return float
+ */
+ function randomize($subjectID);
+}
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * A compound selector that tries each selector from a list, and returns the
+ * first non-null selection.
+ *
+ */
+class AB2_Selector_Sequence implements AB2_Selector {
+
+ private $_selectors;
+
+ public function __construct($selectors) {
+ $this->_selectors = !is_null($selectors) ? $selectors : array();
+ }
+
+ public function select($subjectKey) {
+ foreach ($this->_selectors as $s) {
+ $v = $s->select($subjectKey);
+ if (!is_null($v)) {
+ return $v;
+ }
+ }
+ return null;
+ }
+}
@@ -0,0 +1,90 @@
+<?php
+
+require_once "AB2/Selector.php";
+
+
+/**
+ *
+ */
+class AB2_Selector_Weighted implements AB2_Selector {
+ private $_weights;
+ private $_sumWeights;
+ private $_partitions;
+ /**
+ * @var AB2_Selector_Randomizer
+ */
+ private $_randomizer;
+
+ /**
+ * @param string $testKey
+ * @param array $weights an associative array of variation ID => weights
+ * @param AB2_Selector_Randomizer $randomizer
+ * @throws Exception_ArgumentException
+ */
+ public function __construct($weights, $randomizer) {
+ if (!is_array($weights)) {
+ throw new InvalidArgumentException("weights must be a numerical array");
+ }
+ if (is_null($randomizer)) {
+ throw new InvalidArgumentException('A non-null randomizer is required.');
+ }
+
+ // check the weights and build the partition array
+ $sum = 0;
+ $parts = array();
+ foreach ($weights as $var => $w) {
+ if (!(is_numeric($w) && $w >= 0)) {
+ throw new InvalidArgumentException("invalid weight: $w");
+ }
+ // ignore the 0-weighted variations
+ if ($w > 0) {
+ $sum += floatval($w);
+ $parts[strval($sum)] = $var;
+ }
+ }
+
+ $this->_weights = $weights;
+ $this->_sumWeights = $sum;
+ $this->_partitions = $parts;
+ $this->_randomizer = $randomizer;
+ }
+
+ /**
+ * @param mixed $subjectID subject ID used for hashing.
+ * @return string variant key or null if no selection could be made
+ * (probably because no subject ID could be determined).
+ */
+ public final function select($subjectID) {
+ if (!empty($this->_partitions)) {
+ $r = $this->_randomizer->randomize($subjectID);
+ $w = $r * $this->getSumWeights();
+ return $this->findPartition($w);
+ }
+ return null;
+ }
+
+ private function getSumWeights() {
+ return $this->_sumWeights;
+ }
+
+ private function findPartition($w) {
+ foreach ($this->_partitions as $max => $var) {
+ if ($w < $max) {
+ return $var;
+ }
+ }
+
+ /* return the last partition under the assumption that a round-off error
+ * caused $w to exceed the upper bound. */
+ return $this->_partitions[$this->_sumWeights];
+ }
+
+ public function __toString() {
+ $strs = array();
+ foreach ($this->_weights as $var => $w) {
+ $strs[] = "$var=>$w";
+ }
+ $weights = join(", ", $strs);
+ return "weighted selector: weights=[$weights]";
+ }
+}
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * A SubjectIDProvider is used by some selectors to obtain a subject ID
+ * when one isn't passed to the selector explicitly. E.g., a provider
+ * may read the ID from a cookie.
+ */
+interface AB2_SubjectIDProvider {
+ /**
+ * @abstract
+ * @return string
+ */
+ public function getID();
+}
Oops, something went wrong.

0 comments on commit 8c5ceba

Please sign in to comment.