1/** 2 * Advanced Search Filter Analytics - Tracking Script 3 * Automatically tracks search queries and results 4 */ 5 6jQuery(document).ready(function($) { 7 8 console.log('â ASF Analytics tracking initialized'); 9 10 // Check if tracking is enabled 11 if (typeof asfAnalytics === 'undefined') { 12 console.log('â ASF Analytics not configured'); 13 return; 14 } 15 16 if (!asfAnalytics.settings.trackingEnabled) { 17 console.log('â ASF Analytics tracking is disabled'); 18 return; 19 } 20 21 // Track page load time and views 22 var pageLoadTime = new Date().getTime(); 23 var pageViews = 1; 24 25 // Detect device type 26 var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 27 var deviceType = isMobile ? 'mobile' : 'desktop'; 28 console.log(' Device Type:', deviceType); 29 30 // Get traffic source (referrer) 31 var trafficSource = document.referrer || 'Direct'; 32 if (trafficSource && trafficSource !== 'Direct') { 33 try { 34 var referrerUrl = new URL(trafficSource); 35 trafficSource = referrerUrl.hostname; 36 } catch(e) { 37 trafficSource = trafficSource.substring(0, 100); 38 } 39 } 40 console.log(' Traffic Source:', trafficSource); 41 42 // Function to calculate time spent on page (in seconds) 43 function getTimeSpent() { 44 var currentTime = new Date().getTime(); 45 return Math.round((currentTime - pageLoadTime) / 1000); 46 } 47 48 // Function to collect all active filters 49 function collectFilters(viewType) { 50 var filters = {}; 51 52 try { 53 // Get post types 54 filters.post_types = []; 55 $('input[name=asf-posttype]:checked').each(function() { 56 filters.post_types.push($(this).val()); 57 }); 58 59 // Set view type - use passed parameter first, then fallback 60 if (viewType) { 61 filters.view_type = viewType; 62 console.log(' View type set from parameter:', viewType); 63 } else if (window.lastClickedButton) { 64 filters.view_type = window.lastClickedButton; 65 console.log(' View type set from lastClickedButton:', window.lastClickedButton); 66 } else if ($('.map-view-active').length || $('#map-container').is(':visible')) { 67 filters.view_type = 'map'; 68 console.log(' View type detected: map'); 69 } else { 70 filters.view_type = 'list'; 71 console.log(' View type default: list'); 72 } 73 74 // Get regions - ONLY from the dropdown matching the selected post type 75 var regions = []; 76 var regionSelector = ''; 77 78 // Determine which region dropdown to use based on selected post type 79 // If ONLY upstream is selected, use upstream dropdown 80 // Otherwise (tenders, opportunities, or all-post-types), use the general region dropdown 81 if (filters.post_types.length === 1 && filters.post_types[0] === 'upstream') { 82 regionSelector = 'select[name="asf-upstream-region-category[]"]'; 83 } else { 84 // For tenders, opportunities, or all-post-types, use the general region dropdown 85 regionSelector = 'select[name="asf-region-category[]"]'; 86 } 87 88 console.log(' DEBUG: Using region selector:', regionSelector); 89 90 $(regionSelector).each(function() { 91 var $select = $(this); 92 var selectName = $select.attr('name') || ''; 93 94 console.log(' DEBUG: Processing region select:', selectName); 95 console.log(' DEBUG: Selected options count:', $select.find('option:selected').length); 96 97 $select.find('option:selected').each(function() { 98 var val = $(this).val(); 99 var text = $(this).text().trim(); 100 if (val && val !== '' && val !== 'all' && val !== '0' && text) { 101 regions.push(text); 102 } 103 }); 104 }); 105 106 // Remove duplicates 107 regions = regions.filter(function(item, pos, self) { 108 return self.indexOf(item) === pos; 109 }); 110 111 if (regions.length > 0) { 112 filters.regions = regions; 113 console.log(' â Captured regions:', regions); 114 } else { 115 console.log(' No regions captured'); 116 } 117 118 // Get countries - ONLY from the dropdown matching the selected post type 119 var countries = []; 120 var countrySelector = ''; 121 122 // Determine which country dropdown to use based on selected post type 123 // Priority: tenders > opportunities > upstream 124 if (filters.post_types.indexOf('tenders') !== -1) { 125 countrySelector = 'select[name="asf-tenders-country[]"]'; 126 } else if (filters.post_types.indexOf('opportunities') !== -1) { 127 countrySelector = 'select[name="asf-opp-country[]"]'; 128 } else if (filters.post_types.length === 1 && filters.post_types[0] === 'upstream') { 129 countrySelector = 'select[name="asf-upstream-country[]"]'; 130 } 131 132 console.log(' DEBUG: Using country selector:', countrySelector); 133 134 if (countrySelector) { 135 $(countrySelector).each(function() { 136 var $select = $(this); 137 var selectName = $select.attr('name') || ''; 138 139 console.log(' DEBUG: Processing country select:', selectName);
140 console.log(' DEBUG: Selected options count:', $select.find('option:selected').length); 141 142 $select.find('option:selected').each(function(index) { 143 var val = $(this).val(); 144 var text = $(this).text().trim(); 145 if (index < 5) { // Only log first 5 to avoid clutter 146 console.log(' DEBUG: Selected option', index, '- val:', val, 'text:', text); 147 } 148 if (val && val !== '' && val !== 'all' && val !== '0' && text) { 149 countries.push(text); 150 } 151 }); 152 }); 153 } 154 155 // Remove duplicates 156 countries = countries.filter(function(item, pos, self) { 157 return self.indexOf(item) === pos; 158 }); 159 160 if (countries.length > 0) { 161 filters.countries = countries; 162 console.log(' â Captured countries:', countries); 163 } else { 164 console.log(' No countries captured'); 165 } 166 167 // NOTE: We deliberately do NOT capture all checkboxes and dropdowns 168 // Only capture specific filters that are meaningful (regions, countries, view_type, etc.) 169 // This prevents storing massive arrays of unselected options 170 171 // If you need to track specific additional filters, add them explicitly here 172 // Example: Get awards checkbox 173 // if ($('.asf-chk-awards').is(':checked')) { 174 // filters.awards_only = true; 175 // } 176 177 } catch (error) { 178 console.error('Error collecting filters:', error); 179 } 180 181 return filters; 182 } 183 184 // Function to count results 185 function countResults() { 186 try { 187 var count = 0; 188 189 console.log(' Searching for results count...'); 190 191 // FIRST: Check for asf-records-found element (priority) 192 var $asfRecords = $('.asf-records-found, #asf-records-found, .asf-recounds-found, #asf-recounds-found'); 193 if ($asfRecords.length > 0) { 194 var asfText = $asfRecords.text(); 195 console.log(' Found asf-records-found element:', asfText); 196 197 // Check if text contains multiple post type counts (e.g., "81 tenders, 0 opportunities and 7 upstreams") 198 // Pattern: "X tenders, Y opportunities and Z upstreams" 199 var multiTypeMatch = asfText.match(/(\d+)\s+tenders?,\s*(\d+)\s+opportunities\s+and\s+(\d+)\s+upstreams?/i); 200 if (multiTypeMatch) { 201 var tenders = parseInt(multiTypeMatch[1]) || 0; 202 var opportunities = parseInt(multiTypeMatch[2]) || 0; 203 var upstreams = parseInt(multiTypeMatch[3]) || 0; 204 count = tenders + opportunities + upstreams; 205 console.log(' â Found multi-type count - Tenders:', tenders, 'Opportunities:', opportunities, 'Upstreams:', upstreams, 'Total:', count); 206 return count; 207 } 208 209 // Pattern: "X tenders and Y opportunities" (no upstreams) 210 var twoTypeMatch = asfText.match(/(\d+)\s+tenders?\s+and\s+(\d+)\s+opportunities/i); 211 if (twoTypeMatch) { 212 var tenders = parseInt(twoTypeMatch[1]) || 0; 213 var opportunities = parseInt(twoTypeMatch[2]) || 0; 214 count = tenders + opportunities; 215 console.log(' â Found two-type count - Tenders:', tenders, 'Opportunities:', opportunities, 'Total:', count); 216 return count; 217 } 218 219 // Fallback: just get first number if no pattern matches 220 var asfMatch = asfText.match(/(\d+)/); 221 if (asfMatch) { 222 count = parseInt(asfMatch[0]); 223 console.log(' â Using first number from asf-records-found:', count); 224 return count; 225 } 226 } 227 228 // Try to find total count in text (like "Total 895 records foun
228d") 229 var totalText = $('body').text(); 230 console.log(' Searching for total in page text...'); 231 232 // Try multiple patterns 233 var patterns = [ 234 /Total\s+(\d+)\s+tenders?\s+found/i, 235 /Total\s+(\d+)\s+records?\s+found/i, 236 /(\d+)\s+tenders?\s+found/i, 237 /(\d+)\s+records?\s+found/i, 238 /(\d+)\s+results?\s+found/i, 239 /Found\s+(\d+)\s+results?/i, 240 /Showing\s+\d+[-â]\d+\s+of\s+(\d+)/i, 241 /Displaying\s+\d+[-â]\d+\s+of\s+(\d+)/i, 242 /of\s+(\d+)\s+total/i, 243 /(\d+)\s+total\s+results?/i, 244 /(\d+)\s+opportunities/i 245 ]; 246 247 for (var i = 0; i < patterns.length; i++) { 248 var matches = totalText.match(patterns[i]); 249 if (matches && matches[1]) { 250 count = parseInt(matches[1]); 251 console.log(' â Found total in text (pattern ' + i + '):', count); 252 return count; 253 } 254 } 255 256 // Try to find in specific elements with common class names 257 var selectors = [ 258 '.asf-records-found', '#asf-records-found', 259 '.results-count', '.result-count', '.total-results', 260 '#results-count', '.asf-result-count', '.search-count', 261 '.found-posts', '.wp-pagenavi', '.pagination-info', 262 '[class*="result"][class*="count"]', '[id*="result"][id*="count"]', 263 '[class*="records"][class*="found"]' 264 ]; 265 266 for (var j = 0; j < selectors.length; j++) { 267 var $elem = $(selectors[j]); 268 if ($elem.length > 0) { 269 var text = $elem.text(); 270 var numMatch = text.match(/(\d+)/); 271 if (numMatch) { 272 count = parseInt(numMatch[0]); 273 console.log(' â Found count in element ' + selectors[j] + ':', count); 274 return count; 275 } 276 } 277 } 278 279 // Check for AJAX response data (if available in window object) 280 if (window.asfSearchResults && window.asfSearchResults.total) { 281 count = parseInt(window.asfSearchResults.total); 282 console.log(' â Found count in window object:', count); 283 return count; 284 } 285 286 // Fallback: count DOM elements 287 var selector = asfAnalytics.settings.resultSelector; 288 count = $(selector).length; 289 290 if (count === 0) { 291 count = $('.result-item').length; 292 } 293 if (count === 0) { 294 count = $('.search-result').length; 295 } 296 if (count === 0) { 297 count = $('.post-item').length; 298 } 299 if (count === 0) { 300 count = $('article').length; 301 } 302 303 console.log(' â Fallback: Counted DOM elements:', count); 304 console.warn(' Could not find total results text. Consider checking page HTML structure.'); 305 return count; 306 } catch (error) { 307 console.error('Error counting results:', error); 308 return 0; 309 } 310 } 311 312 // Tracking state to prevent duplicates 313 var lastTrackedSearch = null; 314 var lastTrackedTime = 0; 315 var trackingInProgress = false; 316 317 // Function to track search 318 function trackSearch(keyword, postType, filters, resultsCount) { 319 // Keyword already validated in handleSearchTracking, so just use it 320 keyword = keyword.trim(); 321 322 // Create a unique identifier for this search 323 var searchId = keyword + '|' + postType + '|' + JSON.stringify(filters); 324 var currentTime = Date.now(); 325 326 // Prevent duplicate tracking of the same search within 10 seconds 327 if (lastTrackedSearch === searchId && (currentTime - lastTrackedTime) < 10000) { 328 console.log('â Skipping duplicate search tracking (same search within 10 seconds)'); 329 return; 330 } 331 332 // Prevent any tracking while previous request is in progress 333 if (trackingInProgress) { 334 console.log('â Skipping: tracking already in progress'); 335 return; 336 } 337 338 console.log('â Proceeding with tracking:', keyword); 339 console.log(' Results count:', resultsCount); 340 console.log(' Post type:', postType); 341 342 trackingInProgress = true; 343 lastTrackedSearch = searchId; 344 lastTrackedTime = currentTime; 345 346 var filtersJson = JSON.stringify(filters); 347 var timeSpent = getTimeSpent(); 348 349 console.log(' Sending filters JSON:', filtersJson); 350 console.log(' Filters object:', filters); 351 console.log(' Time spent on page:', timeSpent, 'seconds'); 352 console.log(' Page views:', pageViews); 353 console.log(' Traffic source:', trafficSource); 354 console.log(' Device type:', deviceType); 355 356 console.log('ð¤ Sending AJAX request to:', asfAnalytics.ajaxUrl); 357 358 $.ajax({
359 url: asfAnalytics.ajaxUrl, 360 type: 'POST', 361 data: { 362 action: 'asf_save_search_analytics', 363 nonce: asfAnalytics.nonce, 364 search_keyword: keyword.trim(), 365 post_type: postType || 'unknown', 366 filters_applied: filtersJson, 367 results_count: resultsCount || 0, 368 page_views: pageViews, 369 time_spent: timeSpent, 370 traffic_source: trafficSource, 371 device_type: deviceType 372 }, 373 success: function(response) { 374 console.log('ð¥ AJAX Response received:', response); 375 376 if (response.success) { 377 console.log('â Search tracked successfully (ID: ' + response.data.id + ')'); 378 if (response.data.filters_saved) { 379 console.log(' Filters saved to DB:', response.data.filters_saved); 380 } 381 } else { 382 console.error('â Tracking failed:', response.data.message); 383 if (response.data.error) { 384 console.error(' Error details:', response.data.error); 385 } 386 if (response.data.query) { 387 console.error(' Failed query:', response.data.query); 388 } 389 } 390 trackingInProgress = false; 391 }, 392 error: function(xhr, status, error) { 393 console.error('â AJAX Request Failed'); 394 console.error(' Status:', status); 395 console.error(' Error:', error); 396 console.error(' Response:', xhr.responseText); 397 console.error(' Status Code:', xhr.status); 398 399 try { 400 var errorData = JSON.parse(xhr.responseText); 401 console.error(' Parsed error:', errorData); 402 } catch(e) { 403 console.error(' Could not parse error response'); 404 } 405 406 trackingInProgress = false; 407 }, 408 complete: function() { 409 // Always reset tracking flag after completion 410 trackingInProgress = false; 411 } 412 }); 413 } 414 415 // Function to handle tracking (called directly from button click) 416 function handleSearchTracking(viewType) { 417 console.log('â Search button clicked, will track after results load...'); 418 console.log(' View type parameter:', viewType); 419 420 // Collect search data immediately (BEFORE any delays) 421 var keyword = $('#global_search_keyword_as').val() || 422 $('#global_search_keyword').val() || 423 $('input[name="global_search_keyword"]').val() || 424 $('input[type="search"]').val() || 425 $('input.search-field').val() || ''; 426 427 // Trim and store immediately 428 keyword = keyword.trim(); 429 430 // Check keyword length immediately 431 if (!keyword || keyword.length < 3) { 432 console.log('â Skipping tracking: keyword too short ("' + keyword + '" - min 3 chars required)'); 433 return; 434 } 435 436 var postType = $('#hidden-asf-posttype').val() || 437 $('input[name="asf-posttype"]:checked').val() || 438 'all-post-types'; 439 440 var filters = collectFilters(viewType); 441 442 console.log(' â Keyword captured:', keyword); 443 console.log(' â Post Type:', postType); 444 console.log(' â View Type:', filters.view_type || 'unknown'); 445 console.log(' â Regions:', filters.regions || 'none'); 446 console.log(' â Countries:', filters.countries || 'none'); 447 console.log(' â All Filters:', filters); 448 449 // Wait for results to load before counting and tracking 450 var timeout = parseInt(asfAnalytics.settings.resultTimeout) || 8000; 451 452 setTimeout(function() { 453 var resultsCount = countResults(); 454 console.log(' Final results count:', resultsCount); 455 456 if (resultsCount === 0) { 457 // Map view may take longer to update, retry with longer delays 458 console.log(' Results count is 0, waiting 3 more seconds...'); 459 setTimeout(function() { 460 resultsCount = countResults(); 461 console.log(' Retry 1: Final results count:', resultsCount); 462 463 if (resultsCount === 0) { 464 // One final retry for map view 465 console.log(' Still 0, waiting 3 more seconds...'); 466 setTimeout(function() { 467 resultsCount = countResults(); 468 console.log(' Retry 2: Final results count:', resultsCount); 469 trackSearch(keyword, postType, filters, resultsCount); 470 }, 3000); 471 } else { 472 trackSearch(keyword, postType, filters, resultsCount); 473 } 474 }, 3000); 475 } else { 476 trackSearch(keyword, postType, filters, resultsCount); 477 } 478 }, timeout); 479 } 480 481 // Hook into specific search submit buttons (including map/list view buttons and search icon) 482 // Use both 'click' and 'touchend' events for mobile support 483 $(document).on('click touchend', '#submit, #submit-map, #global_btn_search, .btn-map-view, .btn-list-view, .lnk-submit, .search-icon, .fa-search, button[type="submit"]', function(e) { 484 var buttonId = $(this).attr('id'); 485 var buttonClass = $(this).attr('class') || ''; 486 console.log('â ASF: Search button clicked - ID:', buttonId, 'Class:', buttonClass); 487 console.log(' Event type:', e.type); 488 console.log(' Device: Mobile =', /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); 489 490 // Determine view type based on button 491 var viewType = 'list'; // default 492 if (buttonId === 'submit-map' || buttonClass.indexOf('btn-map-view') !== -1 || buttonClass.indexOf('map') !== -1) { 493 viewType = 'map'; 494 console.log(' View type: MAP'); 495 } else if (buttonId === 'submit' || buttonClass.indexOf('btn-list-view') !== -1 || buttonClass.indexOf('list') !== -1) { 496 viewType = 'list'; 497 console.log(' View type: LIST'); 498 } 499 500 // Store for backup 501 window.lastClickedButton = viewType; 502 503 // Call tracking directly with view type 504 handleSearchTracking(viewType); 505 });
506 507 // Track when user presses Enter in search input (mobile keyboard support) 508 $(document).on('keypress keyup', '#global_search_keyword_as, #global_search_keyword, input[name="global_search_keyword"]', function(e) { 509 if (e.which === 13 || e.keyCode === 13) { // Enter key 510 console.log('â ASF: Enter key pressed in search field'); 511 console.log(' Event type:', e.type); 512 513 // Prevent duplicate triggering from both keypress and keyup 514 if (e.type === 'keyup') return; 515 516 // Determine view type - check which view is currently active 517 var viewType = 'list'; // default 518 if ($('.btn-map-view').is(':visible') && $('.btn-list-view').css('display') === 'none') { 519 viewType = 'map'; 520 console.log(' View type: MAP (map view active)'); 521 } else { 522 console.log(' View type: LIST (list view active or default)'); 523 } 524 525 // Call tracking with determined view type 526 handleSearchTracking(viewType); 527 528 // Note: We don't prevent default because the search needs to actually execute 529 // The duplicate prevention in trackSearch() will handle any double-tracking 530 } 531 }); 532 533 // Debug: Log ALL button clicks/touches to see what's being clicked on mobile 534 $(document).on('click touchend', 'button, input[type="submit"], input[type="button"], a.button', function(e) { 535 var elem = $(this); 536 console.log('ASF Debug: Button interacted -', 537 'Event:', e.type, 538 'ID:', elem.attr('id') || 'none', 539 'Class:', elem.attr('class') || 'none', 540 'Text:', elem.text().substring(0, 20)); 541 }); 542 543 // Also try to hook into any form submissions 544 $(document).on('submit', 'form', function(e) { 545 var $form = $(this); 546 console.log('ASF Debug: Form submitted -', 547 'ID:', $form.attr('id') || 'none', 548 'Action:', $form.attr('action') || 'none'); 549 550 // Check if it's a search form 551 if ($form.find('#global_search_keyword_as, #global_search_keyword').length > 0) { 552 console.log('â ASF: Search form submitted, tracking...'); 553 handleSearchTracking(); 554 } 555 }); 556 557 console.log('â Event handlers attached to document'); 558 console.log('â Debug logging enabled - will show all button clicks'); 559 560 // AJAX monitoring disabled - tracking happens via button clicks only 561 // This prevents duplicate tracking when both button and AJAX fire 562 console.log('â Tracking via button clicks only (AJAX monitoring disabled)'); 563 564});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.