JezK
Edit File: measurement_protocol_api.php
<?php namespace PixelYourSite; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } require_once PYS_PATH.'/modules/google_analytics/server/server_event_helper.php'; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Dto\Common\UserAddress; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Service; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Dto\Request\BaseRequest; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Dto\Common\UserProperties; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Dto\Common\UserProperty; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Dto\Common\UserData; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Dto\Common\UserDataItem; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Dto\Common\ConsentProperty; use PYS_PRO_GLOBAL\Br33f\Ga4\MeasurementProtocol\Enum\ConsentCode; /** * The Measurement Protocol API wrapper class. * * A basic wrapper around the GA Measurement Protocol HTTP API used for making * server-side API calls to track events. * */ class GaMeasurementProtocolAPI { static $uaMap = [ 'traffic_source', 'event_category', ]; /** @var string endpoint for GA API */ public $ga_url = 'https://www.google-analytics.com/collect'; private $access_token = ''; //public $ga_url = 'https://www.google-analytics.com/debug/collect'; //debug private static $_instance; private $isEnabled; private $isDebug; /** * Get singleton instance */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } public function __construct() { add_action('init', array($this, 'init')); } public function init() { $this->isEnabled = GA()->isServerApiEnabled(); $this->isDebug = PYS()->getOption( 'debug_enabled' ); if($this->isEnabled) { // AJAX handlers for fallback //add_action( 'wp_ajax_pys_ga_api_event',array($this,"catchAjaxEvent")); //add_action( 'wp_ajax_nopriv_pys_ga_api_event', array($this,"catchAjaxEvent")); } } /** * Send event in shutdown hook (not work in ajax) * @param SingleEvent[] $events */ public function sendEventsAsync($events) { // not use } /** * Send Event Now * * @param SingleEvent[] $events */ public function sendEventsNow($events) { foreach ($events as $event) { if ($this->isEventDisabledByFilter($event)) { continue; } $ids = $event->payload['trackingIds']; $this->sendEvent($ids, $event); } } private function sendEvent($tags, $event) { if (!$this->access_token) { $this->access_token = GA()->getApiTokens(); } $timestampMicros = (int) (microtime(true) * 1000000); // Filter only GA4 tags (G-*), exclude Google Ads (AW-*) and others $ga4Tags = array_filter($tags, function($tag) { return $this->isGaV4($tag); }); // Log tag filtering GA()->getLog()->debug('GA4 Measurement Protocol - Original tags', $tags); GA()->getLog()->debug('GA4 Measurement Protocol - Filtered GA4 tags', $ga4Tags); foreach ($ga4Tags as $tag) { $data = $event->getData(); $params = $data['params']; $order_id = null; $order = null; if(!empty($data['woo_order'])) { $order_id = $data['woo_order']; $order = wc_get_order($order_id); $type = 'woo'; } elseif (!empty($data['edd_order'])) { $order_id = $data['edd_order']; if (function_exists('edd_get_order')) { $order = edd_get_order($order_id); } $type = 'edd'; } else { continue; } if (empty($this->access_token[$tag]) || empty($order_id)) { continue; } $clientId = GaServerEventHelper::getGAStatFromOrder('clientId', $order_id, $type); if (empty($clientId)) { continue; } // Get session_id for specific measurement_id $sessionId = GaServerEventHelper::getGAStatFromOrder('session_id', $order_id, $type, $tag); GA()->getLog()->debug('GA4 session_id for measurement_id ' . $tag, $sessionId); $ga4Service = new Service($this->access_token[$tag]); $ga4Service->setMeasurementId($tag); $baseRequest = new BaseRequest(); $baseRequest->setClientId($clientId); $eventData = GaServerEventHelper::mapSingleEventToServerDataGA4($event); // Check if eventData is null (unsupported event type) if ( $eventData === null ) { GA()->getLog()->debug('GA4 server event: Unsupported event type, skipping', array( 'event_name' => $event->payload['name'] ?? 'unknown', 'event_id' => $event->getId() )); continue; } $baseRequest->setTimestampMicros($timestampMicros); // Add session_id as event parameter if available if (!empty($sessionId)) { $eventData->setParamValue('session_id', $sessionId); $sessionNumber = GaServerEventHelper::getGAStatFromOrder('session_number', $order_id, $type, $tag); $eventData->setParamValue('session_number', $sessionNumber); GA()->getLog()->debug('Added session_id to event', $sessionId); } // Add custom properties efficiently // Check if order is auto-renewal $isRenewalOrder = $this->isRenewalOrder($order, $type); $renewalName = 'auto-renewal'. (isset($params['payment_method']) && !empty($params['payment_method']) !== null ? '-' . $params['payment_method'] : ''); // Get enrichment data from order meta $enrichData = $this->getOrderEnrichData($order, $type); $is_last_visit = PYS()->getOption('visit_data_model') === 'last_visit'; if(PYS()->getOption( 'track_traffic_source' )){ if ($isRenewalOrder) { $eventData->setParamValue('traffic_source', $renewalName); } elseif (!empty($enrichData)) { // Get traffic source from order meta if ($is_last_visit && isset($enrichData['last_pys_source'])) { $eventData->setParamValue('traffic_source', $enrichData['last_pys_source']); } elseif (!$is_last_visit && isset($enrichData['pys_source'])) { $eventData->setParamValue('traffic_source', $enrichData['pys_source']); } } else { $eventData->setParamValue('traffic_source', getTrafficSource($is_last_visit)); } } if(PYS()->getOption( 'track_utms' )) { if ($isRenewalOrder) { // Set default values for auto-renewal orders $renewalUtms = [ 'utm_source' => $renewalName, 'utm_medium' => $renewalName, 'utm_campaign' => $renewalName, 'utm_content' => $renewalName, 'utm_term' => $renewalName, 'landing_page' => $renewalName, ]; foreach ($renewalUtms as $key => $value) { $eventData->setParamValue($key, $value); } } elseif (!empty($enrichData)) { // Get UTMs from order meta if ($is_last_visit && isset($enrichData['last_pys_utm'])) { $utms = parseUtmString($enrichData['last_pys_utm']); foreach ($utms as $key => $value) { $eventData->setParamValue($key, $value); } } elseif (!$is_last_visit && isset($enrichData['pys_utm'])) { $utms = parseUtmString($enrichData['pys_utm']); foreach ($utms as $key => $value) { $eventData->setParamValue($key, $value); } } // Get landing page from order meta if ($is_last_visit && isset($enrichData['last_pys_landing'])) { $eventData->setParamValue('landing_page', explode('?', $enrichData['last_pys_landing'])[0]); } elseif (!$is_last_visit && isset($enrichData['pys_landing'])) { $eventData->setParamValue('landing_page', explode('?', $enrichData['pys_landing'])[0]); } } else { $utms = getUtms(false, $is_last_visit); foreach ($utms as $key => $value) { $eventData->setParamValue($key, $value); } if (!$is_last_visit) { $eventData->setParamValue('landing_page', sanitize_url(explode('?', $_SESSION['LandingPage'] ?? $_COOKIE['pys_landing_page'] ?? 'undefined')[0])); } else { $eventData->setParamValue('landing_page', sanitize_url(explode('?', $_COOKIE['last_pys_landing_page'] ?? 'undefined')[0])); } } } $event_additional_data = array( 'plugin' => 'PixelYourSite', 'event_day' => PYS()->getOption("enable_event_day_param") ? current_time( "l" ) : '', 'event_month' => PYS()->getOption("enable_event_month_param") ? current_time( "F" ) : '', 'event_hour' => PYS()->getOption("enable_event_time_param") ? $this->getHour() : '', 'user_role' => PYS()->getOption("enable_user_role_param") ? getUserRoles() : '' ); foreach ($event_additional_data as $key => $value) { if($value === '') { continue; } $eventData->setParamValue($key, $value); } GA()->getLog()->debug('Send for GA4', $tag); $consent = new ConsentProperty(); $this->setConsentProperty($params, 'ad_user_data', $consent, 'setAdUserData'); $this->setConsentProperty($params, 'ad_personalization', $consent, 'setAdPersonalization'); $baseRequest->setConsent( $consent ); $baseRequest->addEvent($eventData); if ( isset( $data[ 'woo_order' ] ) && !empty( $data[ 'woo_order' ] ) ) { $order = wc_get_order( $data[ 'woo_order' ] ); if ($order) { $street = $order->get_billing_address_1(); $city = $order->get_billing_city(); $zip = $order->get_billing_postcode(); $country = $order->get_billing_country(); $user_persistence_data = get_persistence_user_data($order->get_billing_email(), $order->get_billing_first_name(), $order->get_billing_last_name(), $order->get_billing_phone()); $userData = new UserData(); if (!empty($user_persistence_data['em'])) { $email = $this->processAndHash($user_persistence_data['em'], 'email'); if (!empty($email)) { $userData->addUserDataItem(new UserDataItem('sha256_email_address', $email)); } } if (!empty($user_persistence_data['tel'])) { $phone = $this->processAndHash($user_persistence_data['tel'], 'phone'); if (!empty($phone)) { $userData->addUserDataItem(new UserDataItem('sha256_phone_number', $phone)); } } $userAdress = new UserAddress(); if ( ! empty( $user_persistence_data['fn'] ) ) { $name = $this->processAndHash( $user_persistence_data['fn'], 'name' ); if (!empty($name)) { $userAdress->addUserAddressItem( new UserDataItem( 'sha256_first_name', $name ) ); } } if ( ! empty( $user_persistence_data['ln'] ) ) { $surname = $this->processAndHash( $user_persistence_data['ln'], 'surname' ); if (!empty($surname)) { $userAdress->addUserAddressItem( new UserDataItem( 'sha256_last_name', $surname ) ); } } if ( $country && $city ) { $userAdress->addUserAddressItem( new UserDataItem( 'city', $city ) ); $userAdress->addUserAddressItem( new UserDataItem( 'country', $country ) ); if ( $zip ) { $userAdress->addUserAddressItem( new UserDataItem( 'postal_code', $zip ) ); } if ( $street ) { $street = $this->processAndHash( $street, 'street' ); if (!empty($street)) { $userAdress->addUserAddressItem( new UserDataItem( 'sha256_street', $street ) ); } } } if($userAdress->getUserAddressItemList()){ $userData->addUserAddress( $userAdress ); } if(GA()->getOption( "track_user_id" )) { $user_id = $order->get_user_id(); if (is_numeric($user_id) && $user_id > 0) { $baseRequest->setUserId((string)$user_id); } } $baseRequest->setUserData($userData); } } elseif ( isset( $data[ 'edd_order' ] ) && !empty( $data[ 'edd_order' ] ) ) { $order = new \EDD_Payment( $data[ 'edd_order' ] ); if ( $order ) { $meta = $order->get_meta(); if ( isset( $meta[ 'user_info' ] ) ) { //object UserProperties $userData = new UserData(); $user_first_name = $meta[ 'user_info' ][ 'first_name' ] ?? ''; $user_last_name = $meta[ 'user_info' ][ 'last_name' ] ?? ''; $user_email = $meta[ 'user_info' ][ 'email' ] ?? ''; $user_persistence_data = get_persistence_user_data( $user_email, $user_first_name, $user_last_name, '' ); if (!empty($user_persistence_data['em'])) { $email = $this->processAndHash($user_persistence_data['em'], 'email'); if (!empty($email)) { $userData->addUserDataItem(new UserDataItem('sha256_email_address', $email)); } } $userAdress = new UserAddress(); if (!empty($user_persistence_data['fn'])) { $first_name = $this->processAndHash($user_persistence_data['fn'], 'name'); if (!empty($first_name)) { $userAdress->addUserAddressItem(new UserDataItem('sha256_first_name', $first_name)); } } if (!empty($user_persistence_data['ln'])) { $last_name = $this->processAndHash($user_persistence_data['ln'], 'surname'); if (!empty($last_name)) { $userAdress->addUserAddressItem(new UserDataItem('sha256_last_name', $last_name)); } } if (isset($meta['user_info']['address'])) { $street = $this->processAndHash($meta['user_info']['address'], 'street'); if (!empty($street)) { $userAdress->addUserAddressItem(new UserDataItem('sha256_street', $street)); } } // Setting user properties for purchaseEventData if ($userAdress->getUserAddressItemList()) { $userData->addUserAddress($userAdress); } if(GA()->getOption( "track_user_id" )){ $user_id = edd_get_payment_user_id($data[ 'edd_order' ]); if (is_numeric($user_id) && $user_id > 0) { $baseRequest->setUserId((string)$user_id); } } $baseRequest->setUserData($userData); } } } GA()->getLog()->debug('Send GA4 server event request', $baseRequest); // We have all the data we need. Just send the request. $debugMode = $this->shouldUseDebugMode($tag); GA()->getLog()->debug('Debug mode for tag ' . $tag . ': ' . ($debugMode ? 'enabled' : 'disabled')); if ($debugMode) { $response = $ga4Service->sendDebug($baseRequest); } else { $response = $ga4Service->send($baseRequest); if(($event->getId() === 'edd_purchase' || $event->getId() === 'woo_purchase') && $response->getStatusCode() >= 200 && $response->getStatusCode() < 300){ if ( isset( $data[ 'woo_order' ] ) && !empty( $data[ 'woo_order' ] ) ) { if ( isWooCommerceVersionGte('3.0.0') ) { $order = wc_get_order($order_id); if($order) { $order->update_meta_data( '_pys_ga_purchase_event_fired', true ); $order->save(); } } else { // WooCommerce < 3.0 update_post_meta( $order_id, '_pys_ga_purchase_event_fired', true ); } } elseif ( isset( $data[ 'edd_order' ] ) && !empty( $data[ 'edd_order' ] ) ) { edd_update_payment_meta( $order_id, '_pys_ga_purchase_event_fired', true ); } } } GA()->getLog()->debug('Send GA4 server event status code: ', $response->getStatusCode()); GA()->getLog()->debug('Send GA4 server event response', $response); // Track fired pixel for meta storage if ( $response->getStatusCode() >= 200 && $response->getStatusCode() < 300 ) { ServerEventsManager::addFiredPixel( 'ga', $tag ); } // Note: else block for Universal Analytics is no longer used, // as we filter only GA4 tags (G-*) at the beginning of the method // else // { // $eventData = GaServerEventHelper::mapSingleEventToServerData($event); // $eventData['v'] = '1';// API version // $eventData['tid'] = $tag; // tracking ID // $eventData['z'] = time(); // // $response = wp_safe_remote_request($this->ga_url, $this->prepareRequestArgs($eventData)); // if (is_wp_error($response)) { // GA()->getLog()->debug('Send GA server event error', $response); // return; // } // GA()->getLog()->debug('Send GA server event response', $response); // } // $response_code = wp_remote_retrieve_response_code( $response ); // $response_message = wp_remote_retrieve_response_message( $response ); // $raw_response_body = wp_remote_retrieve_body( $response ); } } private function prepareRequestArgs($params) { $args = array( 'method' => 'POST', 'timeout' => MINUTE_IN_SECONDS, 'redirection' => 0, // 'httpversion' => '1.0', 'sslverify' => true, 'blocking' => true, // 'user-agent' => $this->get_request_user_agent(), 'headers' => [], 'body' => $this->paramsToString($params), 'cookies' => array(), ); return $args; } public function paramsToString($params) { return http_build_query($params, '', '&'); } public function isGaV4($tag) { return strpos($tag, 'G') === 0; } function isSha256($str) { return preg_match('/^[a-f0-9]{64}$/', $str); } function processAndHash($value, $type) { if (is_array($value) && !empty($value)) { $value = reset($value); } if (!empty($value) && !$this->isSha256($value)) { switch ($type) { case 'email': $value = strtolower($value); if (preg_match('/@(gmail\.com|googlemail\.com)$/', $value)) { $value = preg_replace('/\.(?=[^@]*@)/', '', $value); } $value = str_replace(' ', '', $value); break; case 'phone': $value = preg_replace('/\D/', '', $value); $value = '+' . $value; break; case 'name': case 'surname': $value = preg_replace('/[\d\W]/', '', $value); $value = strtolower($value); $value = trim($value); break; case 'street': $value = strtolower($value); $value = str_replace(' ', '', $value); break; } // Do not hash empty values after processing if (empty($value) || $value === '+') { return ''; } $value = hash('sha256', $value); } return $value; } function setConsentProperty($params, $key, $consent, $method) { if (isset($params[$key]) && !empty($params[$key])) { $consent->$method(strtoupper($params[$key])); } else { $consent->$method(ConsentCode::DENIED); } } /** * Check if debug mode should be used for specific tag * * @param string $tag GA4 measurement ID * @return bool */ private function shouldUseDebugMode($tag) { $debugFlags = GA()->getPixelDebugMode(); // Check if debug mode is enabled globally if (!empty($debugFlags)) { // If we have specific pixel indices, check if this tag matches foreach ($debugFlags as $flag) { if (strpos($flag, 'index_') === 0) { // This is a specific pixel index, we need to check if this tag matches $index = str_replace('index_', '', $flag); $pixelIndex = $this->getPixelIndex($tag); if ($pixelIndex !== null && $pixelIndex == $index) { return true; } } else { // Global debug mode enabled return true; } } } return false; } /** * Get pixel index for specific measurement ID * * @param string $tag GA4 measurement ID * @return int|null */ private function getPixelIndex($tag) { $allPixels = GA()->getAllPixels(); $index = 0; foreach ($allPixels as $pixel) { if ($pixel === $tag) { return $index; } $index++; } return null; } /** * Check if order is auto-renewal (subscription renewal) * * @param int $order_id Order ID * @param string $type Order type ('woo' or 'edd') * @return bool True if order is auto-renewal, false otherwise */ private function isRenewalOrder($order, $type) { if ($type === 'woo') { if (!$order) { return false; } // Check using WooCommerce Subscriptions function if (function_exists('wcs_order_contains_subscription') && wcs_order_contains_subscription($order, 'renewal')) { return true; } // Check created_via field $created_via = method_exists($order, 'get_created_via') ? $order->get_created_via() : ''; if ($created_via === 'subscription_renewal' || $created_via === 'subscription') { return true; } return false; } elseif ($type === 'edd') { if (!$order) { return false; } // If order has parent - it's a renewal payment if (!empty($order->parent) && $order->parent > 0) { return true; } // Check order type if (isset($order->type) && $order->type === 'renewal') { return true; } return false; } return false; } /** * Get enrichment data from order meta * * @param mixed $order Order object (WC_Order or EDD_Payment) * @param string $type Order type ('woo' or 'edd') * @return array Enrichment data or empty array */ private function getOrderEnrichData($order, $type) { $data = array(); if ($type === 'woo' && $order) { if (isWooUseHPStorage()) { $data = $order->get_meta('pys_enrich_data', true); } else { $data = get_post_meta($order->get_id(), 'pys_enrich_data', true); } } elseif ($type === 'edd' && $order) { if (class_exists('EDD_Payment')) { $payment = new \EDD_Payment($order->id ?? $order->ID); if ($payment && $payment->get_meta()) { $meta = $payment->get_meta(); if (isset($meta['pys_enrich_data'])) { $data = $meta['pys_enrich_data']; } } } } return is_array($data) ? $data : array(); } /** * Check if server event should be disabled by filter * * @param SingleEvent $event The event to check * @return bool True if event should be blocked, false otherwise */ private function isEventDisabledByFilter($event) { $event_name = $event->getId(); $woo_order = isset($event->payload['woo_order']) ? $event->payload['woo_order'] : null; $edd_order = isset($event->payload['edd_order']) ? $event->payload['edd_order'] : null; $order_id = $woo_order ?? $edd_order; $is_disabled = apply_filters('pys_disable_server_event_filter', false, $event_name, 'ga4', $order_id); if ($is_disabled) { GA()->getLog()->debug('GA4 server event blocked by pys_disable_server_event_filter', [ 'event_name' => $event_name, 'order_id' => $order_id ]); } return $is_disabled; } /* * If server message is blocked by gprg or it dynamic * we send data by ajax request from js and send the same data like browser event */ public function catchAjaxEvent() { GA()->getLog()->debug('catchAjaxEvent send ga4 server from ajax'); $event = sanitize_text_field($_POST['event']); $eventSlug = sanitize_text_field($_POST['event_slug'] ?? ''); $data = isset($_POST['data']) ? $_POST['data'] : array(); $ids = $_POST['ids']; $eventID = sanitize_text_field($_POST['eventID']); $wooOrder = isset($_POST['woo_order']) && is_numeric($_POST['woo_order']) ? (int) $_POST['woo_order'] : null; $eddOrder = isset($_POST['edd_order']) && is_numeric($_POST['edd_order']) ? (int) $_POST['edd_order'] : null; if ( empty( $_REQUEST['ajax_event'] ) || !wp_verify_nonce( $_REQUEST['ajax_event'], 'ajax-event-nonce' ) ) { wp_die(); return; } $singleEvent = $this->dataToSingleEvent($event,$data,$eventID,$ids,$wooOrder,$eddOrder,$eventSlug); $this->sendEventsNow([$singleEvent]); wp_die(); } /** * @param $eventName * @param $params * @param $eventID * @param $ids * @param $wooOrder * @param $eddOrder * @return SingleEvent */ private function dataToSingleEvent($eventName,$params,$eventID,$ids,$wooOrder,$eddOrder,$eventSlug = "") { $singleEvent = new SingleEvent($eventSlug,""); $payload = [ 'name' => $eventName, 'eventID' => $eventID, 'woo_order' => $wooOrder, 'edd_order' => $eddOrder, 'trackingIds' => $ids ]; $singleEvent->addParams($params); $singleEvent->addPayload($payload); return $singleEvent; } /** * Get current hour in the format supported by Facebook * @return string string */ public function getHour() { $array = [ '00-01', '01-02', '02-03', '03-04', '04-05', '05-06', '06-07', '07-08', '08-09', '09-10', '10-11', '11-12', '12-13', '13-14', '14-15', '15-16', '16-17', '17-18', '18-19', '19-20', '20-21', '21-22', '22-23', '23-24' ]; return $array[ current_time( "G" ) ]; } } /** * @return GaMeasurementProtocolAPI */ function GaMeasurementProtocolAPI() { return GaMeasurementProtocolAPI::instance(); } GaMeasurementProtocolAPI();