token = trim( (string) get_option('pxsync_capi_token') ); $this->pixel_id = trim( (string) get_option('pxsync_pixel_id') ); $this->test_code = trim( (string) get_option('pxsync_test_event_code') ); } public function send( $event_name, $event_id, $external_id, $properties = [], $user_context = [] ) { if ( empty($this->token) || empty($this->pixel_id) ) return false; $payload = $this->build_payload($event_name, $event_id, $external_id, $properties, $user_context); if ( ! empty( $this->test_code ) ) { $payload['test_event_code'] = $this->test_code; } $response = wp_remote_post( $this->api_url, [ 'headers' => [ 'Access-Token' => $this->token, 'Content-Type' => 'application/json' ], 'body' => wp_json_encode($payload), 'timeout' => 20, ]); $status_code = wp_remote_retrieve_response_code( $response ); // ✅ NEW: Retry queue with exponential backoff // Never forget comments: Uses Action Scheduler (bundled with WooCommerce). // _pxsync_retry tracks attempt count — max 3 retries total (1min, 2min, 4min). if ( $status_code !== 200 && function_exists('as_schedule_single_action') ) { $retry_count = isset($properties['_pxsync_retry']) ? (int) $properties['_pxsync_retry'] : 0; if ( $retry_count < 3 ) { as_schedule_single_action( time() + ( 60 * pow( 2, $retry_count ) ), 'pxsync_retry_capi_event', [[ 'event' => $event_name, 'event_id' => $event_id, 'external_id' => $external_id, 'properties' => array_merge( $properties, ['_pxsync_retry' => $retry_count + 1] ), 'user' => $user_context, ]], 'pxsync-capi' ); } } return $response; } private function build_payload( $event_name, $event_id, $external_id, $properties = [], $user_context = [] ) { // TikTok rejects empty-string identifiers ("Email address is invalid"), so only send what we have. $user = array_filter( [ 'external_id' => $external_id, 'email' => $user_context['email'] ?? '', 'phone' => $user_context['phone'] ?? '', 'ttp' => isset( $_COOKIE['_ttp'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['_ttp'] ) ) : '', ], function ( $v ) { return $v !== '' && $v !== null; } ); $ad = ! empty( $_COOKIE['ttclid'] ) ? [ 'callback' => sanitize_text_field( wp_unslash( $_COOKIE['ttclid'] ) ) ] : []; return [ 'pixel_code' => $this->pixel_id, 'event' => $event_name, 'event_id' => $event_id, 'timestamp' => gmdate('Y-m-d\TH:i:s\Z'), 'context' => [ 'ad' => (object) $ad, 'user' => (object) $user, 'user_agent' => $this->get_user_agent(), 'ip' => $this->get_client_ip(), // 'library' => ['name' => 'php', 'version' => '1.0.0'], 'page' => [ 'url' => $user_context['url'] ?? $this->get_current_url(), 'referrer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', ], ], 'properties' => $properties ]; } private function get_current_url() { $host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; return esc_url_raw( ( is_ssl() ? 'https://' : 'http://' ) . $host . $uri ); } // Cloudflare-aware IP detection // Behind Cloudflare, REMOTE_ADDR is the CDN IP. // CF-Connecting-IP is the real visitor IP and must be trusted first. public function get_client_ip() { $headers = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR' ]; foreach ( $headers as $h ) { if ( empty( $_SERVER[ $h ] ) ) continue; $raw = sanitize_text_field( wp_unslash( $_SERVER[ $h ] ) ); foreach ( explode( ',', $raw ) as $candidate ) { $candidate = trim( $candidate ); if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) return $candidate; } } return '127.0.0.1'; } public function get_user_agent() { return isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; } }