prefix . 'aieo_workflow_ab'; } public static function ensure_table() { global $wpdb; $t = self::table(); if ( self::table_exists( $t ) ) { return true; } $charset = aieo_get_charset_collate(); $wpdb->query( "CREATE TABLE IF NOT EXISTS `{$t}` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `workflow_id` bigint unsigned NOT NULL, `step_idx` int unsigned NOT NULL, `variant` int unsigned NOT NULL DEFAULT 0, `user_id` bigint unsigned NOT NULL DEFAULT 0, `recipient_email` varchar(190) NOT NULL DEFAULT '', `assigned_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_recipient` (`workflow_id`,`step_idx`,`user_id`,`recipient_email`), KEY `wf_step` (`workflow_id`,`step_idx`) ) {$charset};" ); return self::table_exists( $t ); } public static function variants( $step ) { $base = array( 'subject' => (string) ( $step['subject'] ?? '' ), 'body' => (string) ( $step['body'] ?? '' ), ); $vs = trim( (string) ( $step['variant_subject'] ?? '' ) ); $vb = trim( (string) ( $step['variant_body'] ?? '' ) ); if ( '' === $vs && '' === $vb ) { return array( $base ); } return array( $base, array( 'subject' => '' !== $vs ? $vs : $base['subject'], 'body' => '' !== $vb ? $vb : $base['body'], ), ); } public static function choose( $workflow_id, $step_idx, $user_id, $email, $count ) { global $wpdb; $count = (int) $count; if ( $count <= 1 ) { return 0; } self::ensure_table(); $t = self::table(); $user_id = (int) $user_id; $email = ( $user_id > 0 ) ? '' : strtolower( trim( (string) $email ) ); $workflow_id = (int) $workflow_id; $step_idx = (int) $step_idx; $existing = $wpdb->get_var( $wpdb->prepare( "SELECT variant FROM `{$t}` WHERE workflow_id=%d AND step_idx=%d AND user_id=%d AND recipient_email=%s", $workflow_id, $step_idx, $user_id, $email ) ); if ( null !== $existing ) { return (int) $existing; } $epsilon = (float) apply_filters( 'aieo_ab_epsilon', 0.15 ); $explore = ( $epsilon > 0 ) && ( mt_rand( 1, 1000 ) <= (int) round( $epsilon * 1000 ) ); $variant = $explore ? mt_rand( 0, $count - 1 ) : self::best_variant( $workflow_id, $step_idx, $count ); $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$t}` (workflow_id, step_idx, variant, user_id, recipient_email, assigned_at) VALUES (%d,%d,%d,%d,%s,%s)", $workflow_id, $step_idx, (int) $variant, $user_id, $email, gmdate( 'Y-m-d H:i:s' ) ) ); $final = $wpdb->get_var( $wpdb->prepare( "SELECT variant FROM `{$t}` WHERE workflow_id=%d AND step_idx=%d AND user_id=%d AND recipient_email=%s", $workflow_id, $step_idx, $user_id, $email ) ); return ( null !== $final ) ? (int) $final : (int) $variant; } public static function best_variant( $workflow_id, $step_idx, $count ) { $perf = self::performance( $workflow_id, $step_idx ); $rows = isset( $perf['variants'] ) ? $perf['variants'] : array(); $reach = array_fill( 0, (int) $count, 0 ); $reward = array_fill( 0, (int) $count, -1.0 ); foreach ( $rows as $r ) { $i = (int) $r['variant']; if ( $i >= 0 && $i < $count ) { $reach[ $i ] = (int) $r['reach']; $reward[ $i ] = (float) $r['reward']; } } foreach ( $reach as $i => $n ) { if ( $n < self::MIN_REACH ) { return $i; } } $best = 0; $bestR = -1.0; foreach ( $reward as $i => $rw ) { if ( $rw > $bestR ) { $bestR = $rw; $best = $i; } } return $best; } public static function performance( $workflow_id, $step_idx ) { global $wpdb; $workflow_id = (int) $workflow_id; $step_idx = (int) $step_idx; $t = self::table(); $disp = $wpdb->prefix . 'aieo_push_dispatch'; $out = array( 'available' => false, 'variants' => array(), 'winner' => null ); if ( ! self::table_exists( $t ) || ! self::table_exists( $disp ) ) { return $out; } $trigger = 'wf_' . $workflow_id; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT x.variant AS variant, COUNT(*) AS reach, SUM(x.opened) AS opens, SUM(x.clicked) AS clicks, SUM(x.converted) AS conversions, SUM(x.revenue) AS revenue FROM ( SELECT ab.variant AS variant, ab.id AS abid, MAX(d.opened_at IS NOT NULL) AS opened, MAX(d.state = 'clicked') AS clicked, MAX(CASE WHEN d.converted_order_id > 0 THEN 1 ELSE 0 END) AS converted, COALESCE(SUM(d.revenue), 0) AS revenue FROM `{$t}` ab JOIN `{$disp}` d ON d.trigger_id = %s AND d.channel IN ('email','push') AND d.state IN ('sent','clicked','converted','dismissed') AND ( ( ab.user_id > 0 AND d.user_id = ab.user_id ) OR ( ab.user_id = 0 AND d.recipient_email = ab.recipient_email COLLATE utf8mb4_general_ci ) ) WHERE ab.workflow_id = %d AND ab.step_idx = %d GROUP BY ab.id, ab.variant ) x GROUP BY x.variant ORDER BY x.variant ASC", $trigger, $workflow_id, $step_idx ), ARRAY_A ); if ( empty( $rows ) ) { return $out; } $variants = array(); $winner = null; $best_rw = -1.0; foreach ( $rows as $r ) { $reach = (int) $r['reach']; $opens = (int) $r['opens']; $clicks = (int) $r['clicks']; $conv = (int) $r['conversions']; $reward = $reach > 0 ? ( ( $clicks + 0.25 * $opens ) / $reach ) : 0.0; $v = array( 'variant' => (int) $r['variant'], 'reach' => $reach, 'opens' => $opens, 'open_rate' => $reach > 0 ? round( 100 * $opens / $reach, 1 ) : 0.0, 'clicks' => $clicks, 'ctr' => $reach > 0 ? round( 100 * $clicks / $reach, 1 ) : 0.0, 'conversions' => $conv, 'revenue' => round( (float) $r['revenue'], 2 ), 'reward' => round( $reward, 4 ), ); $variants[] = $v; if ( $reach >= self::MIN_REACH && $reward > $best_rw ) { $best_rw = $reward; $winner = (int) $r['variant']; } } return array( 'available' => true, 'variants' => $variants, 'winner' => $winner ); } private static function table_exists( $t ) { global $wpdb; return (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t; } }