0 ? (int) $s['window'] : (int) apply_filters( 'aieo_submission_trail_window', self::WINDOW ); $ids = isset( $s['session_ids'] ) && is_array( $s['session_ids'] ) ? array_values( array_filter( array_map( 'strval', $s['session_ids'] ) ) ) : array(); $before = array(); $after = array(); $queried = false; $hit = false; if ( $ids && self::sessions_available() ) { $queried = true; foreach ( self::page_views( $ids, $created, $window ) as $page ) { $hit = true; if ( $page['ts'] < $created ) { $before[] = $page; } else { $after[] = $page; } } } return array( 'before' => array_slice( $before, -self::CAP ), 'after' => array_slice( $after, 0, self::CAP ), 'source_flags' => self::flags( false, $hit, $queried, false ), ); } public static function page_views( array $session_ids, $created, $window ) { $rows = self::events( $session_ids, $created, $window, self::PAGE_TYPES ); if ( ! $rows ) { return array(); } $skew = self::clock_skew( $rows ); $pages = array(); foreach ( $rows as $r ) { $type = (string) $r->event_type; if ( 'page_exit' === $type || 'tab_hidden' === $type ) { continue; } $key = self::visit_key( (string) $r->page_url ); if ( '' === $key ) { continue; } $ts = (int) floor( ( (float) $r->timestamp ) / 1000 ) + $skew; if ( $ts < $created - $window || $ts > $created + $window ) { continue; } $dwell = max( (int) $r->time_on_page, self::dwell_from_data( $r->event_data ) ); if ( isset( $pages[ $key ] ) ) { $pages[ $key ]['dwell'] = max( $pages[ $key ]['dwell'], $dwell ); continue; } $pid = 'product_view' === $type ? self::product_id_from_event( $r ) : 0; $pages[ $key ] = array( 'url' => (string) $r->page_url, 'path' => (string) wp_parse_url( (string) $r->page_url, PHP_URL_PATH ), 'title' => self::title_of( (string) $r->page_url, $pid ), 'product_id' => $pid, 'ts' => $ts, 'at' => gmdate( 'Y-m-d H:i:s', $ts ), 'dwell' => $dwell, ); } foreach ( $rows as $r ) { $type = (string) $r->event_type; if ( 'page_exit' !== $type && 'tab_hidden' !== $type ) { continue; } $key = self::visit_key( (string) $r->page_url ); if ( '' === $key || ! isset( $pages[ $key ] ) ) { continue; } $dwell = max( (int) $r->time_on_page, self::dwell_from_data( $r->event_data ) ); if ( $dwell > $pages[ $key ]['dwell'] ) { $pages[ $key ]['dwell'] = $dwell; } } uasort( $pages, static function ( $a, $b ) { return $a['ts'] <=> $b['ts']; } ); return array_values( $pages ); } const TRACKING_ARGS = '/^(utm_[a-z]+|gclid|gbraid|wbraid|fbclid|msclkid|ttclid|twclid|li_fat_id|mc_cid|mc_eid|_ga|_gl|_hsenc|_hsmi|srsltid)$/'; public static function visit_key( $url ) { $base = self::page_key( $url ); if ( '' === $base ) { return ''; } $query = (string) wp_parse_url( trim( (string) $url ), PHP_URL_QUERY ); if ( '' === $query ) { return $base; } parse_str( $query, $args ); $keep = array(); foreach ( (array) $args as $k => $v ) { if ( ! preg_match( self::TRACKING_ARGS, strtolower( (string) $k ) ) && is_scalar( $v ) ) { $keep[ (string) $k ] = (string) $v; } } if ( ! $keep ) { return $base; } ksort( $keep ); return $base . '?' . http_build_query( $keep ); } private static function title_of( $url, $product_id ) { static $memo = array(); $key = self::visit_key( $url ); if ( isset( $memo[ $key ] ) ) { return $memo[ $key ]; } $title = ''; $pid = $product_id > 0 ? $product_id : (int) url_to_postid( strtok( $url, '#' ) ); if ( $pid > 0 ) { $title = wp_strip_all_tags( get_the_title( $pid ) ); } elseif ( self::visit_key( $url ) === self::visit_key( home_url( '/' ) ) ) { $title = wp_strip_all_tags( get_bloginfo( 'name' ) ); } if ( '' === $title ) { $title = (string) wp_parse_url( $url, PHP_URL_PATH ); } $memo[ $key ] = $title; return $title; } public static function product_views( array $session_ids, $created, $window ) { $rows = self::events( $session_ids, $created, $window, array( 'product_view', 'page_exit', 'tab_hidden' ) ); if ( ! $rows ) { return array(); } $skew = self::clock_skew( $rows ); $out = array(); $by_page = array(); foreach ( $rows as $r ) { if ( 'product_view' !== (string) $r->event_type ) { continue; } $pid = self::product_id_from_event( $r ); if ( $pid <= 0 ) { continue; } $ts = (int) floor( ( (float) $r->timestamp ) / 1000 ) + $skew; if ( $ts < $created - $window || $ts > $created + $window ) { continue; } $key = self::page_key( (string) $r->page_url ); if ( '' !== $key ) { $by_page[ $key ] = $pid; } $dwell = max( (int) $r->time_on_page, self::dwell_from_data( $r->event_data ) ); if ( isset( $out[ $pid ] ) ) { $out[ $pid ]['dwell'] = max( $out[ $pid ]['dwell'], $dwell ); if ( $ts < $out[ $pid ]['ts'] ) { $out[ $pid ]['ts'] = $ts; $out[ $pid ]['at'] = gmdate( 'Y-m-d H:i:s', $ts ); } continue; } $out[ $pid ] = array( 'ts' => $ts, 'at' => gmdate( 'Y-m-d H:i:s', $ts ), 'dwell' => $dwell, ); } if ( ! $out ) { return array(); } foreach ( $rows as $r ) { $type = (string) $r->event_type; if ( 'page_exit' !== $type && 'tab_hidden' !== $type ) { continue; } $pid = (int) $r->product_id; if ( $pid <= 0 || ! isset( $out[ $pid ] ) ) { $key = self::page_key( (string) $r->page_url ); $pid = ( '' !== $key && isset( $by_page[ $key ] ) ) ? (int) $by_page[ $key ] : 0; } if ( $pid <= 0 || ! isset( $out[ $pid ] ) ) { continue; } $dwell = max( (int) $r->time_on_page, self::dwell_from_data( $r->event_data ) ); if ( $dwell > $out[ $pid ]['dwell'] ) { $out[ $pid ]['dwell'] = $dwell; } } if ( count( $out ) > self::CAP ) { uasort( $out, static function ( $a, $b ) { return $a['ts'] <=> $b['ts']; } ); $out = array_slice( $out, 0, self::CAP, true ); } return $out; } public static function events( array $session_ids, $created, $window, array $types ) { global $wpdb; $hot = $wpdb->prefix . 'aieo_sessions'; $archive = $wpdb->prefix . 'aieo_sessions_archive'; $session_ids = array_values( array_unique( array_filter( array_map( 'strval', $session_ids ) ) ) ); $types = array_values( array_filter( array_map( static function ( $t ) { return preg_replace( '/[^a-z_]/', '', (string) $t ); }, $types ) ) ); if ( ! $session_ids || ! $types ) { return array(); } $in = implode( ',', array_fill( 0, count( $session_ids ), '%s' ) ); $types = "('" . implode( "','", $types ) . "')"; $from_ms = ( $created - $window - self::SKEW_TOLERANCE ) * 1000; $to_ms = ( $created + $window + self::SKEW_TOLERANCE ) * 1000; $sql = "SELECT event_type, product_id, event_data, page_url, timestamp, time_on_page, UNIX_TIMESTAMP(created_at) AS ingested FROM `{$hot}` WHERE session_id IN ({$in}) AND event_type IN {$types} AND timestamp BETWEEN %d AND %d"; $params = array_merge( $session_ids, array( $from_ms, $to_ms ) ); if ( self::archive_available() ) { $sql .= " UNION ALL SELECT event_type, product_id, event_data, page_url, timestamp, time_on_page, UNIX_TIMESTAMP(created_at) AS ingested FROM `{$archive}` WHERE session_id IN ({$in}) AND event_type IN {$types} AND timestamp BETWEEN %d AND %d"; $params = array_merge( $params, $session_ids, array( $from_ms, $to_ms ) ); } $sql = 'SELECT * FROM (' . $sql . ') u ORDER BY ABS(CAST(u.timestamp AS SIGNED) - %d) ASC LIMIT ' . ( self::CAP * 10 ); $params[] = $created * 1000; $rows = (array) $wpdb->get_results( $wpdb->prepare( $sql, $params ) ); if ( ! $rows ) { return array(); } usort( $rows, static function ( $a, $b ) { return (float) $a->timestamp <=> (float) $b->timestamp; } ); return $rows; } public static function flags( $beacon, $sessions_hit, $sessions_queried, $recent ) { return array( 'beacon' => (bool) $beacon, 'sessions' => (bool) $sessions_hit, 'sessions_queried' => (bool) $sessions_queried, 'recent' => (bool) $recent, 'tracking_on' => self::tracking_enabled(), 'consent_gated' => self::consent_gated(), ); } public static function tracking_enabled() { return '1' === (string) get_option( 'aieo_session_tracking_enabled', '0' ); } public static function consent_gated() { $bypass = get_option( 'aieo_session_bypass_consent', '1' ); if ( '1' == $bypass ) { return false; } $require = get_option( 'aieo_session_tracking_require_consent', '1' ); return '1' == $require; } public static function sessions_available() { global $wpdb; static $memo = null; if ( null !== $memo ) { return $memo; } $t = $wpdb->prefix . 'aieo_sessions'; $memo = ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t ); return $memo; } public static function archive_available() { global $wpdb; static $memo = null; if ( null !== $memo ) { return $memo; } $t = $wpdb->prefix . 'aieo_sessions_archive'; $memo = ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t ); return $memo; } public static function clock_skew( array $rows ) { $diffs = array(); foreach ( $rows as $r ) { $ingested = isset( $r->ingested ) ? (int) $r->ingested : 0; if ( $ingested <= 0 ) { continue; } $diffs[] = $ingested - (int) floor( ( (float) $r->timestamp ) / 1000 ); } if ( count( $diffs ) < 2 ) { return 0; } sort( $diffs ); $n = count( $diffs ); $q = static function ( $p ) use ( $diffs, $n ) { return $diffs[ min( $n - 1, max( 0, (int) floor( $p * ( $n - 1 ) ) ) ) ]; }; $mid = (int) floor( $n / 2 ); $skew = ( $n % 2 ) ? $diffs[ $mid ] : (int) floor( ( $diffs[ $mid - 1 ] + $diffs[ $mid ] ) / 2 ); if ( abs( $skew ) > self::SKEW_TOLERANCE ) { return 0; } if ( $skew < 0 ) { return $skew; } if ( $skew < self::SKEW_FLOOR ) { return 0; } $low = (int) $q( 0.25 ); $high = (int) $q( 0.75 ); if ( ( $high - $low ) > self::SKEW_FLOOR ) { return 0; } return max( 0, $low ); } public static function dwell_from_data( $event_data ) { $data = json_decode( (string) $event_data, true ); if ( ! is_array( $data ) ) { return 0; } $n = 0; foreach ( array( 'time_on_page', 'time_active', 'dwell' ) as $k ) { if ( isset( $data[ $k ] ) && is_numeric( $data[ $k ] ) ) { $n = max( $n, (int) $data[ $k ] ); } } return min( $n, self::MAX_DWELL ); } public static function page_key( $url ) { $url = trim( $url ); if ( '' === $url ) { return ''; } $p = wp_parse_url( $url ); if ( ! is_array( $p ) ) { return ''; } $host = isset( $p['host'] ) ? strtolower( preg_replace( '/^www\./', '', (string) $p['host'] ) ) : ''; $path = isset( $p['path'] ) ? rtrim( (string) $p['path'], '/' ) : ''; return $host . $path; } public static function product_id_from_event( $row ) { if ( isset( $row->product_id ) && (int) $row->product_id > 0 ) { return (int) $row->product_id; } $data = json_decode( (string) $row->event_data, true ); if ( is_array( $data ) ) { foreach ( array( 'product_id', 'productId', 'post_id' ) as $k ) { if ( ! empty( $data[ $k ] ) && is_numeric( $data[ $k ] ) ) { return (int) $data[ $k ]; } } } $url = (string) $row->page_url; if ( '' === $url ) { return 0; } static $memo = array(); $key = self::page_key( $url ); if ( isset( $memo[ $key ] ) ) { return $memo[ $key ]; } $pid = (int) url_to_postid( strtok( $url, '#' ) ); $memo[ $key ] = ( $pid > 0 && 'product' === get_post_type( $pid ) ) ? $pid : 0; return $memo[ $key ]; } } }