get_var( $wpdb->prepare( 'SELECT MAX(id) FROM %i WHERE ab_test_id = %s', ABTESTKIT_EVENTS_TABLE, $test_id ) ); return '' === $wpdb->last_error ? (int) $id : new WP_Error( 'abtestkit_connected_milestone_storage_unavailable' ); } function abtestkit_connected_milestone_baseline_key( string $test_id ) : string { return ABTESTKIT_CONNECTED_MILESTONE_BASELINE_PREFIX . hash( 'sha256', $test_id ); } function abtestkit_connected_milestone_event_key( string $site_ref, string $test_ref, string $name ) : string { return ABTESTKIT_CONNECTED_MILESTONE_EVENT_PREFIX . hash( 'sha256', $site_ref . '|' . $test_ref . '|' . $name ); } /** Called only after a newly created test has been committed, before traffic. */ function abtestkit_connected_milestone_seed_new_test( string $test_id ) : void { $connection = abtestkit_hosted_get_connection(); if ( ! is_array( $connection ) ) { return; } $latest = abtestkit_connected_milestone_latest_row( $test_id ); if ( is_wp_error( $latest ) ) { abtestkit_connected_collection_gap( $latest->get_error_code() ); return; } $key = abtestkit_connected_milestone_baseline_key( $test_id ); if ( ! add_option( $key, array( 'site_ref' => (string) $connection['site_ref'], 'row_id' => $latest ), '', 'no' ) && null === get_option( $key, null ) ) { abtestkit_connected_collection_gap( 'milestone_baseline_write_failed' ); } } /** A different hosted site starts an honest new observation window. */ function abtestkit_connected_milestone_baseline( string $test_id, string $site_ref ) { $key = abtestkit_connected_milestone_baseline_key( $test_id ); $stored = get_option( $key, null ); if ( null === $stored || ( is_array( $stored ) && ( $stored['site_ref'] ?? '' ) !== $site_ref ) ) { $latest = abtestkit_connected_milestone_latest_row( $test_id ); if ( is_wp_error( $latest ) ) { return $latest; } $baseline = array( 'site_ref' => $site_ref, 'row_id' => $latest ); if ( null === $stored ) { add_option( $key, $baseline, '', 'no' ); } else { update_option( $key, $baseline, false ); } $saved = get_option( $key, null ); return $saved === $baseline ? null : new WP_Error( 'abtestkit_connected_milestone_baseline_write_failed' ); } if ( ! is_array( $stored ) || ! is_int( $stored['row_id'] ?? null ) || $stored['row_id'] < 0 ) { return new WP_Error( 'abtestkit_connected_milestone_baseline_invalid' ); } return $stored['row_id']; } /** Existence checks use fixed indexed queries and return no visitor data. */ function abtestkit_connected_milestone_has_row( string $test_id, int $after_id, string $type, string $constraint = '' ) { global $wpdb; if ( 'order' === $constraint ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Indexed milestone lookup must observe current persisted evidence. $found = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE ab_test_id = %s AND event_type = %s AND id > %d AND order_id > 0 LIMIT 1', ABTESTKIT_EVENTS_TABLE, $test_id, $type, $after_id ) ); } elseif ( 'revenue' === $constraint ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Indexed milestone lookup must observe current persisted evidence. $found = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE ab_test_id = %s AND event_type = %s AND id > %d AND order_id > 0 AND amount > 0 LIMIT 1', ABTESTKIT_EVENTS_TABLE, $test_id, $type, $after_id ) ); } elseif ( '' === $constraint ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Indexed milestone lookup must observe current persisted evidence. $found = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE ab_test_id = %s AND event_type = %s AND id > %d LIMIT 1', ABTESTKIT_EVENTS_TABLE, $test_id, $type, $after_id ) ); } else { return new WP_Error( 'abtestkit_connected_milestone_constraint_invalid' ); } return '' === $wpdb->last_error ? null !== $found : new WP_Error( 'abtestkit_connected_milestone_storage_unavailable' ); } /** Reserve a single immutable event ID before touching the bounded outbox. */ function abtestkit_connected_milestone_reserve( string $name, array $test, string $site_ref, string $test_ref ) { $key = abtestkit_connected_milestone_event_key( $site_ref, $test_ref, $name ); $marker = get_option( $key, null ); if ( null !== $marker ) { return $marker; } $payload = abtestkit_connected_test_payload( $test, 'observed' ); $installation = ABTestKit_Connected_Installation::get_or_create(); $event_id = ABTestKit_Connected_Event::new_id(); if ( is_wp_error( $payload ) || is_wp_error( $installation ) || is_wp_error( $event_id ) ) { return new WP_Error( 'abtestkit_connected_milestone_event_unavailable' ); } $event = array( 'schema_version' => 1, 'event_id' => $event_id, 'installation_id' => (string) $installation['installation_id'], 'event_name' => $name, 'occurred_at' => time(), 'plugin_version' => abtestkit_hosted_sync_plugin_version(), 'test_ref' => $test_ref, 'payload' => $payload, ); if ( is_wp_error( ABTestKit_Connected_Event::validate( $event, time() ) ) ) { return new WP_Error( 'abtestkit_connected_milestone_event_invalid' ); } add_option( $key, array( 'event' => $event, 'acknowledged' => false ), '', 'no' ); $marker = get_option( $key, null ); return is_array( $marker ) ? $marker : new WP_Error( 'abtestkit_connected_milestone_reservation_failed' ); } /** An unacknowledged event is replayed with its original ID, never recreated. */ function abtestkit_connected_milestone_queue_marker( string $key, array $marker ) : bool { if ( ! is_bool( $marker['acknowledged'] ?? null ) || ! is_array( $marker['event'] ?? null ) ) { abtestkit_connected_collection_gap( 'milestone_marker_invalid' ); return false; } if ( $marker['acknowledged'] ) { return true; } if ( ! empty( $marker['expired'] ) ) { return false; } if ( (int) ( $marker['event']['occurred_at'] ?? 0 ) <= time() - ABTestKit_Connected_Event::MAX_AGE ) { $marker['expired'] = true; update_option( $key, $marker, false ); abtestkit_connected_collection_gap( 'milestone_expired' ); return false; } $queued = ABTestKit_Connected_Outbox::enqueue( $marker['event'], time() ); if ( is_wp_error( $queued ) ) { abtestkit_connected_collection_gap( $queued->get_error_code() ); return false; } return true; } /** At most 20 tests and 20 milestone enqueues per 15-minute cron tick. */ function abtestkit_connected_collect_persisted_milestones() : void { global $wpdb; if ( ! abtestkit_connected_platform_can_create_or_launch() ) { return; } $connection = abtestkit_hosted_get_connection(); $tests = get_option( 'abtestkit_page_tests', array() ); if ( ! is_array( $connection ) || ! is_array( $tests ) || ! $tests ) { return; } $tests = array_values( $tests ); $total = count( $tests ); $cursor = (int) get_option( ABTESTKIT_CONNECTED_MILESTONE_CURSOR_OPTION, 0 ) % $total; $processed = 0; $queued = 0; while ( $processed < min( 20, $total ) && $queued < 20 ) { $test = $tests[ $cursor ] ?? null; $cursor = ( $cursor + 1 ) % $total; ++$processed; if ( ! is_array( $test ) || empty( $test['id'] ) ) { continue; } $test_id = (string) $test['id']; $baseline = abtestkit_connected_milestone_baseline( $test_id, (string) $connection['site_ref'] ); if ( is_wp_error( $baseline ) ) { abtestkit_connected_collection_gap( $baseline->get_error_code() ); continue; } if ( null === $baseline ) { continue; } $payload = abtestkit_connected_test_payload( $test, 'observed' ); if ( is_wp_error( $payload ) || 'draft' === $payload['status'] ) { continue; } $ref = abtestkit_connected_test_ref_record( $test_id ); if ( is_wp_error( $ref ) ) { abtestkit_connected_collection_gap( $ref->get_error_code() ); continue; } $site_ref = (string) $connection['site_ref']; $test_ref = (string) $ref['test_ref']; $names = array( 'first_impression_recorded', 'both_variants_recorded', 'first_conversion_recorded' ); if ( 'purchase' === $payload['goal'] ) { $names[] = 'first_order_attributed'; $names[] = 'first_revenue_attributed'; } $variants = null; foreach ( $names as $name ) { if ( $queued >= 20 ) { break; } $key = abtestkit_connected_milestone_event_key( $site_ref, $test_ref, $name ); $marker = get_option( $key, null ); if ( is_array( $marker ) && ( ! empty( $marker['acknowledged'] ) || ! empty( $marker['expired'] ) ) ) { continue; } if ( null === $marker ) { $found = false; if ( in_array( $name, array( 'first_impression_recorded', 'both_variants_recorded' ), true ) ) { if ( null === $variants ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Indexed milestone lookup must observe current persisted evidence. $variants = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT variant FROM %i WHERE ab_test_id = %s AND event_type = 'impression' AND id > %d AND variant IN ('A','B') LIMIT 2", ABTESTKIT_EVENTS_TABLE, $test_id, $baseline ) ); if ( '' !== $wpdb->last_error ) { abtestkit_connected_collection_gap( 'milestone_storage_unavailable' ); break; } } $found = 'first_impression_recorded' === $name ? ! empty( $variants ) : in_array( 'A', $variants, true ) && in_array( 'B', $variants, true ); } elseif ( 'first_conversion_recorded' === $name ) { $found = abtestkit_connected_milestone_has_row( $test_id, $baseline, 'purchase' === $payload['goal'] ? 'purchase' : 'click' ); } elseif ( 'first_order_attributed' === $name ) { $found = abtestkit_connected_milestone_has_row( $test_id, $baseline, 'purchase', 'order' ); } elseif ( 'first_revenue_attributed' === $name ) { $found = abtestkit_connected_milestone_has_row( $test_id, $baseline, 'purchase', 'revenue' ); } if ( is_wp_error( $found ) ) { abtestkit_connected_collection_gap( $found->get_error_code() ); break; } if ( ! $found ) { continue; } $marker = abtestkit_connected_milestone_reserve( $name, $test, $site_ref, $test_ref ); } if ( is_wp_error( $marker ) || ! is_array( $marker ) || ! abtestkit_connected_milestone_queue_marker( $key, $marker ) ) { if ( is_wp_error( $marker ) ) { abtestkit_connected_collection_gap( $marker->get_error_code() ); } elseif ( ! is_array( $marker ) ) { abtestkit_connected_collection_gap( 'milestone_marker_invalid' ); } break; } ++$queued; } } update_option( ABTESTKIT_CONNECTED_MILESTONE_CURSOR_OPTION, $cursor, false ); } /** Called only after a complete hosted batch response and local acknowledgement. */ function abtestkit_connected_milestone_mark_acknowledged( array $event, string $site_ref ) : void { $name = (string) ( $event['event_name'] ?? '' ); if ( ! in_array( $name, array( 'first_impression_recorded', 'both_variants_recorded', 'first_conversion_recorded', 'first_order_attributed', 'first_revenue_attributed' ), true ) ) { return; } $key = abtestkit_connected_milestone_event_key( $site_ref, (string) ( $event['test_ref'] ?? '' ), $name ); $marker = get_option( $key, null ); if ( ! is_array( $marker ) || ! hash_equals( (string) ( $marker['event']['event_id'] ?? '' ), (string) ( $event['event_id'] ?? '' ) ) ) { abtestkit_connected_collection_gap( 'milestone_ack_marker_missing' ); return; } $marker['acknowledged'] = true; update_option( $key, $marker, false ); if ( get_option( $key, null ) !== $marker ) { abtestkit_connected_collection_gap( 'milestone_ack_write_failed' ); } }