get_error_code(), true ); return; } $connection = abtestkit_hosted_get_connection(); if ( ! is_array( $connection ) ) { return; } abtestkit_connected_reconcile_baselines(); abtestkit_connected_collect_persisted_milestones(); abtestkit_connected_onboarding_reconcile(); if ( ! empty( $connection['pending_credential'] ) ) { if ( ! abtestkit_hosted_rotate_connection_credential( $connection ) ) { abtestkit_connected_transport_state( 'credential_recovery_failed' ); return; } $connection = abtestkit_hosted_get_connection(); if ( ! is_array( $connection ) ) { return; } } $now = time(); $claimed = ABTestKit_Connected_Outbox::claim( $now ); if ( is_wp_error( $claimed ) ) { abtestkit_connected_transport_state( 'storage_unavailable' ); return; } if ( ! $claimed ) { return; } $events = array(); foreach ( $claimed as $row ) { $event = json_decode( (string) $row['payload'], true ); if ( ! is_array( $event ) || is_wp_error( ABTestKit_Connected_Event::validate( $event, $now ) ) || ! hash_equals( (string) $installation['installation_id'], (string) ( $event['installation_id'] ?? '' ) ) ) { abtestkit_connected_transport_state( 'invalid_local_event', true ); return; } $events[] = $event; } $response = abtestkit_hosted_service_request( 'POST', '/sites/' . (string) $connection['site_ref'] . '/events', array( 'schema_version' => 1, 'installation' => array( 'installation_id' => (string) $installation['installation_id'], 'home_url' => (string) $installation['home_url'], 'environment' => (string) $installation['environment'], 'created_at' => (int) $installation['created_at'], ), 'events' => $events, ), (string) $connection['credential'] ); $finished_at = time(); if ( is_wp_error( $response ) ) { abtestkit_connected_transport_retry( $claimed, 'transport', $finished_at ); return; } $status = (int) $response['status']; if ( 401 === $status || 404 === $status ) { if ( ! abtestkit_hosted_remove_local_connection( $connection ) ) { abtestkit_connected_transport_retry( $claimed, 'transport', $finished_at ); } return; } if ( 429 === $status || $status >= 500 ) { abtestkit_connected_transport_retry( $claimed, 429 === $status ? 'rate_limited' : 'service_unavailable', $finished_at ); return; } if ( ! in_array( $status, array( 200, 202 ), true ) ) { abtestkit_connected_transport_state( 'service_rejected_batch', true ); return; } $data = $response['data']; $keys = array_keys( $data ); sort( $keys ); if ( array( 'accepted', 'duplicates' ) !== $keys || ! is_int( $data['accepted'] ) || ! is_int( $data['duplicates'] ) || $data['accepted'] < 0 || $data['duplicates'] < 0 || $data['accepted'] + $data['duplicates'] !== count( $claimed ) ) { abtestkit_connected_transport_retry( $claimed, 'transport', $finished_at ); return; } foreach ( $claimed as $index => $row ) { if ( ! ABTestKit_Connected_Outbox::acknowledge( (string) $row['event_id'], (string) $row['claim_token'], $finished_at ) ) { abtestkit_connected_transport_state( 'acknowledgement_failed' ); return; } abtestkit_connected_milestone_mark_acknowledged( $events[ $index ], (string) $connection['site_ref'] ); abtestkit_connected_onboarding_mark_acknowledged( $events[ $index ], (string) $connection['site_ref'] ); } abtestkit_connected_transport_state( '', false, true ); } add_action( ABTESTKIT_CONNECTED_TRANSPORT_HOOK, 'abtestkit_connected_transport_run' ); /** A quiet site still reports contact, without sending experiment or visitor data. */ function abtestkit_connected_heartbeat_run() : void { if ( ! abtestkit_connected_platform_can_create_or_launch() ) { return; } $connection = abtestkit_hosted_get_connection(); if ( ! is_array( $connection ) || ! empty( $connection['pending_credential'] ) ) { return; } $state = get_option( ABTESTKIT_CONNECTED_HEARTBEAT_OPTION, array() ); $state = is_array( $state ) ? $state : array(); $now = time(); if ( (int) ( $state['last_success_at'] ?? 0 ) > $now - DAY_IN_SECONDS || (int) ( $state['last_attempt_at'] ?? 0 ) > $now - HOUR_IN_SECONDS ) { return; } $state['last_attempt_at'] = $now; $state['last_error_code'] = 'transport'; update_option( ABTESTKIT_CONNECTED_HEARTBEAT_OPTION, $state, false ); $response = abtestkit_hosted_service_request( 'POST', '/sites/' . (string) $connection['site_ref'] . '/heartbeat', array( 'schema_version' => 1 ), (string) $connection['credential'] ); if ( is_wp_error( $response ) ) { return; } if ( 404 === (int) $response['status'] ) { $state['last_error_code'] = 'service_route_unavailable'; } elseif ( 401 === (int) $response['status'] || 403 === (int) $response['status'] ) { $state['last_error_code'] = 'service_denied'; } elseif ( 200 === (int) $response['status'] && array( 'acknowledged' => true ) === $response['data'] ) { $state['last_success_at'] = time(); $state['last_error_code'] = ''; } else { $state['last_error_code'] = 'service_unavailable'; } update_option( ABTESTKIT_CONNECTED_HEARTBEAT_OPTION, $state, false ); } add_action( ABTESTKIT_CONNECTED_TRANSPORT_HOOK, 'abtestkit_connected_heartbeat_run', 20 ); /** Let an administrator retry a permanently paused batch after investigation. */ function abtestkit_connected_transport_retry_now() : void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to manage this connection.', 'abtestkit' ) ); } check_admin_referer( 'abtestkit_connected_transport_retry_now' ); if ( ! abtestkit_connected_platform_can_create_or_launch() ) { abtestkit_hosted_redirect( 'connection-failed' ); } $state = get_option( ABTESTKIT_CONNECTED_TRANSPORT_STATE_OPTION, array() ); $state = is_array( $state ) ? $state : array(); $state['paused'] = false; $state['last_error_code'] = ''; update_option( ABTESTKIT_CONNECTED_TRANSPORT_STATE_OPTION, $state, false ); if ( ! wp_next_scheduled( ABTESTKIT_CONNECTED_TRANSPORT_HOOK, array( 'manual' ) ) ) { // A paused batch may still hold its five-minute claim lease. wp_schedule_single_event( time() + ABTestKit_Connected_Outbox::LEASE_SECONDS + 1, ABTESTKIT_CONNECTED_TRANSPORT_HOOK, array( 'manual' ) ); } abtestkit_hosted_redirect( 'sync-queued' ); } add_action( 'admin_post_abtestkit_connected_transport_retry_now', 'abtestkit_connected_transport_retry_now' ); /** Show coverage gaps and paused delivery without exposing an event payload. */ function abtestkit_connected_transport_render_status() : void { $state = get_option( ABTESTKIT_CONNECTED_TRANSPORT_STATE_OPTION, array() ); $collection = get_option( ABTESTKIT_CONNECTED_COLLECTION_STATE_OPTION, array() ); $state = is_array( $state ) ? $state : array(); $collection = is_array( $collection ) ? $collection : array(); if ( ! empty( $state['paused'] ) ) { echo '
' . esc_html__( 'Connected data sync is paused. Your local tests and reports are unaffected.', 'abtestkit' ) . '
'; echo ''; } if ( ! empty( $collection['last_gap_at'] ) ) { echo '' . esc_html__( 'Some connected activity could not be queued. Local test results remain available; connected activity may be incomplete.', 'abtestkit' ) . '
'; } $registration = abtestkit_connected_registration_state(); $pending = 0; $site = abtestkit_hosted_get_connection(); foreach ( $registration['records'] as $record ) { if ( is_array( $record ) && ( (int) ( $record['confirmed_revision'] ?? 0 ) !== (int) ( $record['revision'] ?? 0 ) || (string) ( $record['confirmed_site_ref'] ?? '' ) !== (string) ( $site['site_ref'] ?? '' ) ) ) { ++$pending; } } if ( $pending > 0 ) { /* translators: %d: number of experiments awaiting hosted registration. */ echo '' . esc_html( sprintf( _n( '%d experiment is awaiting connected registration; local test behavior is unaffected.', '%d experiments are awaiting connected registration; local test behavior is unaffected.', $pending, 'abtestkit' ), $pending ) ) . '
'; } if ( ! empty( $registration['last_error_code'] ) ) { echo '' . esc_html__( 'Connected registration could not be confirmed. The plugin will retry; existing local tests are unaffected.', 'abtestkit' ) . '
'; } }