$since, 'source' => 'none', 'arms' => $arms, 'surfaces' => array(), 'totals' => array(), 'reasons' => array(), 'origins' => array(), ); } list($rows, $source) = self::measure_rows($surfaces, $since); $spelling = array(); foreach ($surfaces as $s) { $spelling[strtolower((string) $s)] = (string) $s; } $by_surface = array(); $totals = array(); foreach ($rows as $r) { $surface = (string) $r['surface']; $lower = strtolower($surface); if (isset($spelling[$lower])) { $surface = $spelling[$lower]; } $arm = (string) $r['arm']; if (!in_array($arm, $arms, true)) { continue; } if (!isset($by_surface[$surface])) { $by_surface[$surface] = array(); } $by_surface[$surface][$arm] = self::measures($r); if (!isset($totals[$arm])) { $totals[$arm] = self::measures(array()); } foreach (self::MEASURE_KEYS as $k) { $totals[$arm][$k] += isset($r[$k]) ? (int) $r[$k] : 0; } } $baseline = $arms[0]; $out_surfaces = array(); foreach ($by_surface as $surface => $per_arm) { $out_surfaces[] = array( 'surface' => $surface, 'arms' => $per_arm, 'engaged' => self::compare_all($per_arm, $arms, $baseline, 'click_sessions', 'sessions'), 'per_appearance' => self::rates($per_arm, 'clicks', 'impressions'), ); } usort($out_surfaces, static function ($a, $b) { $an = self::arm_total($a['arms'], 'impressions'); $bn = self::arm_total($b['arms'], 'impressions'); return ($bn === $an) ? strcmp($a['surface'], $b['surface']) : (($bn > $an) ? 1 : -1); }); return array( 'since' => $since, 'source' => $source, 'arms' => $arms, 'baseline' => $baseline, 'surfaces' => $out_surfaces, 'totals' => $totals, 'engaged' => self::compare_all($totals, $arms, $baseline, 'click_sessions', 'sessions'), 'reasons' => self::suppression_reasons($surfaces, $arms, $since), 'origins' => self::origin_rows($surfaces, $arms, $since), ); } const MEASURE_KEYS = array('impressions', 'clicks', 'suppressions', 'sessions', 'click_sessions'); public static function window_start(array $exp, $days) { $days = max(1, (int) $days); $floor = gmdate('Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS); $start = isset($exp['started_at']) ? (string) $exp['started_at'] : ''; return ('' !== $start && $start > $floor) ? $start : $floor; } private static function measure_rows(array $surfaces, $since) { global $wpdb; $rollup = $wpdb->prefix . 'aieo_reco_arm_daily'; $in = self::in_list($surfaces); $rows = array(); if (self::table_exists($rollup)) { $rows = (array) $wpdb->get_results($wpdb->prepare( "SELECT surface, arm, SUM(impressions) impressions, SUM(clicks) clicks, SUM(suppressions) suppressions, SUM(sessions) sessions, SUM(click_sessions) click_sessions FROM `{$rollup}` WHERE stat_date >= %s AND surface IN ({$in}) GROUP BY surface, arm", substr($since, 0, 10) ), ARRAY_A); } if (!empty($rows)) { return array($rows, 'rollup'); } return array(self::live_rows($surfaces, $since), 'live'); } private static function live_rows(array $surfaces, $since) { global $wpdb; $reco = $wpdb->prefix . 'aieo_reco_events'; if (!self::table_exists($reco) || !class_exists('AIEO_Display_Conditions')) { return array(); } $shown = AIEO_Display_Conditions::raw_types_sql('shown'); $engaged = AIEO_Display_Conditions::raw_types_sql('engaged'); $suppressed = AIEO_Display_Conditions::raw_types_sql('suppressed'); $in = self::in_list($surfaces); return (array) $wpdb->get_results($wpdb->prepare( "SELECT surface, arm, COUNT(DISTINCT CASE WHEN event_type IN ({$shown}) THEN id END) AS impressions, COUNT(DISTINCT CASE WHEN event_type IN ({$engaged}) THEN id END) AS clicks, COUNT(DISTINCT CASE WHEN event_type IN ({$suppressed}) THEN id END) AS suppressions, -- Suppressed rows are the page views where the surface did -- NOT appear, so they can never contribute to a numerator. -- Counting them in the denominator would inflate every -- per-visitor rate with exactly the visitors the numerator -- can never contain — and an arm that suppressed MORE would -- look like it converted LESS. COUNT(DISTINCT CASE WHEN event_type NOT IN ({$suppressed}) THEN session_id END) AS sessions, COUNT(DISTINCT CASE WHEN event_type IN ({$engaged}) THEN session_id END) AS click_sessions FROM `{$reco}` WHERE ts >= %s AND surface IN ({$in}) GROUP BY surface, arm", $since ), ARRAY_A); } public static function suppression_reasons(array $surfaces, array $arms, $since) { global $wpdb; $reco = $wpdb->prefix . 'aieo_reco_events'; if (!self::table_exists($reco)) { return array(); } if (!self::column_exists($reco, 'reason')) { return array(); } $in = self::in_list($surfaces); $rows = (array) $wpdb->get_results($wpdb->prepare( "SELECT arm, reason, COUNT(*) n FROM `{$reco}` WHERE ts >= %s AND surface IN ({$in}) AND event_type = 'suppressed' GROUP BY arm, reason ORDER BY n DESC", $since ), ARRAY_A); $out = array(); foreach ($rows as $r) { if (!in_array((string) $r['arm'], $arms, true)) { continue; } $out[] = array( 'arm' => (string) $r['arm'], 'reason' => '' === (string) $r['reason'] ? 'unrecorded' : (string) $r['reason'], 'n' => (int) $r['n'], ); } return $out; } public static function origin_rows(array $surfaces, array $arms, $since) { global $wpdb; $rollup = $wpdb->prefix . 'aieo_reco_origin_daily'; if (!self::table_exists($rollup)) { return array(); } if (!class_exists('AIEO_Display_Conditions') || !AIEO_Display_Conditions::ensure_origin()) { return array(); } $origins = AIEO_Display_Origin::table(); if (!self::table_exists($origins)) { return array(); } $kind = str_replace('%', '%%', AIEO_Display_Origin::kind_sql('o')); $in = self::in_list($surfaces); $rows = (array) $wpdb->get_results($wpdb->prepare( "SELECT d.arm, d.origin_id, COALESCE({$kind}, '') AS kind, COALESCE(o.canon_campaign, '') AS campaign, COALESCE(o.canon_source, '') AS source, SUM(d.impressions) AS impressions, SUM(d.clicks) AS clicks, SUM(d.sessions) AS sessions, SUM(d.click_sessions) AS click_sessions FROM `{$rollup}` d LEFT JOIN `{$origins}` o ON o.id = d.origin_id WHERE d.stat_date >= %s AND d.surface IN ({$in}) GROUP BY d.arm, d.origin_id ORDER BY SUM(d.impressions) DESC", substr($since, 0, 10) ), ARRAY_A); $by_origin = array(); foreach ($rows as $r) { if (!in_array((string) $r['arm'], $arms, true)) { continue; } $id = (int) $r['origin_id']; if (!isset($by_origin[$id])) { $by_origin[$id] = array( 'origin_id' => $id, 'label' => self::origin_label($r), 'arms' => array(), 'total' => 0, ); } $by_origin[$id]['arms'][(string) $r['arm']] = self::measures($r); $by_origin[$id]['total'] += (int) $r['impressions']; } uasort($by_origin, static function ($a, $b) { return $b['total'] - $a['total']; }); return array_slice(array_values($by_origin), 0, self::ORIGIN_LIMIT); } private static function origin_label(array $r) { if (0 === (int) $r['origin_id']) { return __('Direct or unknown', 'ai-eshop-optimizer'); } $bits = array_filter(array( (string) $r['kind'], (string) $r['campaign'] !== '' ? (string) $r['campaign'] : (string) $r['source'], )); return $bits ? implode(' · ', $bits) : sprintf('#%d', (int) $r['origin_id']); } private static function compare_all(array $per_arm, array $arms, $baseline, $num, $den) { if (!class_exists('AIEO_Reco_Experiment_Report')) { return array(); } $b = isset($per_arm[$baseline]) ? $per_arm[$baseline] : self::measures(array()); $out = array(); foreach ($arms as $arm) { if ($arm === $baseline) { $out[$arm] = null; continue; } $a = isset($per_arm[$arm]) ? $per_arm[$arm] : self::measures(array()); $out[$arm] = AIEO_Reco_Experiment_Report::compare_counts( (int) $b[$num], (int) $b[$den], (int) $a[$num], (int) $a[$den] ); } return $out; } private static function rates(array $per_arm, $num, $den) { $out = array(); foreach ($per_arm as $arm => $m) { $n = (int) $m[$den]; $out[$arm] = $n > 0 ? ((int) $m[$num]) / $n : null; } return $out; } private static function measures(array $r) { $out = array(); foreach (self::MEASURE_KEYS as $k) { $out[$k] = isset($r[$k]) ? (int) $r[$k] : 0; } return $out; } private static function arm_total(array $per_arm, $key) { $n = 0; foreach ($per_arm as $m) { $n += isset($m[$key]) ? (int) $m[$key] : 0; } return $n; } private static function in_list(array $surfaces) { $clean = array(); foreach ($surfaces as $s) { $s = sanitize_key((string) $s); if ('' !== $s) { $clean[] = esc_sql($s); } } if (empty($clean)) { return "''"; } return "'" . implode("','", $clean) . "'"; } private static function table_exists($table) { global $wpdb; return (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table; } private static function column_exists($table, $column) { global $wpdb; return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s", $table, $column )) > 0; } }