$event_id, 'type' => $event_type, 'timestamp_ms' => (int) (microtime(true) * 1000), 'action_source' => 'web', 'source_url' => ridoway_pixel_capi_get_source_url(), 'data' => $data, ); // The ad-click identifier ties the conversion back to a specific ChatGPT // ad click on the server side, the same value the browser reports. $oppref = ridoway_pixel_capi_get_oppref(); if ($oppref !== '') { $event['oppref'] = $oppref; } $user_data = array_merge(array( 'ip_address' => ridoway_pixel_capi_get_ip_address(), 'user_agent' => ridoway_pixel_capi_get_user_agent(), ), (array) $user); $obref = ridoway_pixel_capi_get_obref(); if ($obref !== '') { $user_data['obref'] = $obref; } $user_data = array_filter($user_data, function ($value) { return $value !== ''; }); if (!empty($user_data)) { $event['user'] = $user_data; } return $event; } /** * Send one event to the Conversions API synchronously. * * @param array $event A built Conversions API event. * @return array|\WP_Error */ function ridoway_pixel_capi_http_post($event) { $pixel_id = ridoway_pixel_option('pixel_id', ''); $api_key = ridoway_pixel_option('capi_key', ''); $body = array( 'integration_source' => 'ridoway-pixel', 'validate_only' => ridoway_pixel_option_enabled('validate_only'), 'events' => array($event), ); $url = add_query_arg('pid', $pixel_id, 'https://bzr.openai.com/v1/events'); return wp_remote_post($url, array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode($body, JSON_UNESCAPED_SLASHES), 'timeout' => 5, 'redirection' => 0, )); } /** * Has a CAPI request gone wrong? * * @param array|\WP_Error $response * @return bool */ function ridoway_pixel_capi_send_failed($response) { if (is_wp_error($response)) { return true; } $code = (int) wp_remote_retrieve_response_code($response); return $code < 200 || $code >= 300; } /** * Add an event to the retry queue and schedule a flush. * * @param array $event * @param int $attempts */ function ridoway_pixel_capi_enqueue($event, $attempts = 0) { $queue = get_transient('ridoway_pixel_capi_retry_queue'); if (!is_array($queue)) { $queue = array(); } $queue[] = array( 'event' => $event, 'attempts' => (int) $attempts, 'time' => time(), ); // Keep the queue bounded so a broken site cannot grow it forever. $queue = array_slice($queue, -50); set_transient('ridoway_pixel_capi_retry_queue', $queue, DAY_IN_SECONDS); ridoway_pixel_capi_schedule_flush(); } /** * Schedule the retry flush, only once. */ function ridoway_pixel_capi_schedule_flush() { if (get_option('ridoway_pixel_capi_flush_scheduled', '0') === '1') { return; } $scheduled = wp_schedule_single_event(time() + 30, 'ridoway_pixel_capi_flush'); if (false !== $scheduled) { update_option('ridoway_pixel_capi_flush_scheduled', '1', false); } } /** * Cron handler: drain the retry queue. */ function ridoway_pixel_capi_cron_flush() { delete_option('ridoway_pixel_capi_flush_scheduled'); $queue = get_transient('ridoway_pixel_capi_retry_queue'); if (empty($queue) || !is_array($queue)) { return; } if (!ridoway_pixel_capi_is_active()) { return; } $remaining = array(); $reschedule = false; foreach ($queue as $item) { $attempts = isset($item['attempts']) ? (int) $item['attempts'] : 0; // Give up after repeated failures or when the event is stale. if ($attempts >= 6 || (time() - (int) $item['time']) > DAY_IN_SECONDS) { continue; } $response = ridoway_pixel_capi_http_post($item['event']); if (ridoway_pixel_capi_send_failed($response)) { $item['attempts'] = $attempts + 1; $remaining[] = $item; $reschedule = true; if (ridoway_pixel_option_enabled('debug_mode')) { ridoway_pixel_capi_log($item['event'], $response); } } } if (!empty($remaining)) { set_transient('ridoway_pixel_capi_retry_queue', $remaining, DAY_IN_SECONDS); } else { delete_transient('ridoway_pixel_capi_retry_queue'); } if ($reschedule) { ridoway_pixel_capi_schedule_flush(); } } add_action('ridoway_pixel_capi_flush', 'ridoway_pixel_capi_cron_flush'); function ridoway_pixel_capi_log($event, $response) { if (is_wp_error($response)) { error_log('[Ridoway Pixel CAPI] Error: ' . $response->get_error_message() . ' | Event: ' . $event['type']); } else { $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); error_log('[Ridoway Pixel CAPI] Sent: ' . $event['type'] . ' | Status: ' . $code . ' | Response: ' . $body); } } /** * Send a server-side event. * * Debug mode sends synchronously and logs the result. Otherwise the event is * queued and delivered by cron, so the visitor's request is never delayed, and * failures are retried a few times instead of being lost. * * @param string $event_id Shared event id, used for browser<->server dedup. * @param string $event_type OpenAI event name. * @param array $data Event data object. * @param array $user Extra user fields, all optional. * @return bool Whether the event was accepted for delivery. */ function ridoway_pixel_capi_send_event($event_id, $event_type, $data, $user = array()) { if (!ridoway_pixel_capi_is_active()) { return false; } if (!ridoway_pixel_consent_rule()) { return false; } $event = ridoway_pixel_capi_build_event($event_id, $event_type, $data, $user); // In debug mode send inline so the result can be inspected. if (ridoway_pixel_option_enabled('debug_mode')) { $response = ridoway_pixel_capi_http_post($event); ridoway_pixel_capi_log($event, $response); return !ridoway_pixel_capi_send_failed($response); } // Synchronous mode: try now, queue for retry on failure. if (!ridoway_pixel_option_enabled('async_capi', '1')) { $response = ridoway_pixel_capi_http_post($event); if (ridoway_pixel_capi_send_failed($response)) { ridoway_pixel_capi_enqueue($event); return false; } return true; } // Async mode (default): queue and let the cron flush deliver. ridoway_pixel_capi_enqueue($event); return true; }