indexable_sample_pages($ctx); $wp = (array) $ctx->get('wp', []); $adapters = (array) $ctx->get('adapters', []); $signals = [ 'gtm' => isset($adapters['gtm4wp']) || $this->plugin_active($wp, ['tag manager', 'gtm4wp']), 'ga' => $this->plugin_active($wp, ['google analytics', 'site kit']), 'clarity' => isset($adapters['microsoft_clarity']) || $this->plugin_active($wp, ['microsoft clarity', 'clarity']), 'meta_pixel' => $this->plugin_active($wp, ['facebook pixel', 'meta pixel']), // v3.3.52 P2.2 — non-Google measurement stacks (privacy-first / EU). 'matomo' => $this->plugin_active($wp, ['matomo', 'wp-piwik', 'piwik']), 'plausible' => $this->plugin_active($wp, ['plausible']), 'segment' => false, 'posthog' => false, ]; // v3.3.15 — Integrations module retired. The check now relies only // on client-side gtag/clarity tags + active plugin slugs. The // managed-connection soft-signal path is gone with the feature. // v3.3.50 — track which page surfaced each signal for evidence/debug. $signal_sources = []; foreach ($pages as $p) { $page_url = (string) ($p['url'] ?? ''); // v3.3.50 — primary detection path: inline analytics signals // captured by HtmlCollector::detect_analytics_signals() per page. // Modern GTM/GA4/Clarity are inline snippets, not external script // src — so this is the most reliable signal source. The legacy // js_origins/css_origins scans below act as fallbacks for older // setups (Site Kit, direct gtag.js include, etc.). $as = isset($p['analytics_signals']) && is_array($p['analytics_signals']) ? $p['analytics_signals'] : []; if (!empty($as['gtm_inline'])) { $signals['gtm'] = true; $signal_sources['gtm'][] = $page_url; } if (!empty($as['ga4_inline']) || !empty($as['gtag_inline'])) { $signals['ga'] = true; $signal_sources['ga'][] = $page_url; } // dataLayer alone (without explicit GTM- ID) usually means GTM is // present but the container snippet was inlined with the ID hidden // in an external bundle, OR a custom GA4 install is using gtag. // Treat it as a soft GTM signal so we don't keep emitting "missing". if (!empty($as['datalayer_inline']) && empty($signals['gtm']) && empty($signals['ga'])) { $signals['gtm'] = true; $signal_sources['gtm'][] = $page_url . ' (dataLayer inline)'; } if (!empty($as['clarity_inline'])) { $signals['clarity'] = true; $signal_sources['clarity'][] = $page_url; } if (!empty($as['meta_pixel_inline'])) { $signals['meta_pixel'] = true; $signal_sources['meta_pixel'][] = $page_url; } // v3.3.52 P2.2 — non-Google measurement stacks count as a valid // measurement stack (a site on Matomo/Plausible/Segment/PostHog IS // instrumented; flagging it "missing" was a false positive). foreach (['matomo', 'plausible', 'segment', 'posthog'] as $alt) { if (!empty($as[$alt . '_inline'])) { $signals[$alt] = true; $signal_sources[$alt][] = $page_url; } } // Legacy + nested-aware asset origin scan. $origins = []; // Flat top-level js_origins/css_origins (legacy crawl payload shape). foreach (['js_origins', 'css_origins'] as $key) { if (isset($p[$key]) && is_array($p[$key])) { foreach ($p[$key] as $origin) { if (is_string($origin)) { $origins[] = strtolower($origin); } } } } // v3.3.50 — nested asset_details.{js,css}_origins. Each entry is a // structured dict { type, slug, label, count, examples[] }, so we // flatten label + slug + examples into the lookup blob. Without // this the check sees an empty origins array on the crawl payload // shape currently produced by CrawlCollector and falsely reports // measurement stack missing on instrumented sites (wsend.pro // round-5 false positive). $ad = isset($p['asset_details']) && is_array($p['asset_details']) ? $p['asset_details'] : []; foreach (['js_origins', 'css_origins'] as $key) { $entries = isset($ad[$key]) && is_array($ad[$key]) ? $ad[$key] : []; foreach ($entries as $entry) { if (!is_array($entry)) { continue; } $label = (string) ($entry['label'] ?? ''); $slug = (string) ($entry['slug'] ?? ''); if ($label !== '') { $origins[] = strtolower($label); } if ($slug !== '') { $origins[] = strtolower($slug); } if (isset($entry['examples']) && is_array($entry['examples'])) { foreach ($entry['examples'] as $ex) { if (is_string($ex)) { $origins[] = strtolower($ex); } } } } } $blob = implode(' ', $origins); if (strpos($blob, 'googletagmanager.com') !== false) { $signals['gtm'] = true; $signal_sources['gtm'][] = $page_url . ' (external)'; } if (strpos($blob, 'google-analytics.com') !== false || strpos($blob, 'analytics.google.com') !== false || strpos($blob, 'gtag') !== false) { $signals['ga'] = true; $signal_sources['ga'][] = $page_url . ' (external)'; } if (strpos($blob, 'clarity.ms') !== false || strpos($blob, 'clarity.microsoft') !== false) { $signals['clarity'] = true; $signal_sources['clarity'][] = $page_url . ' (external)'; } if (strpos($blob, 'connect.facebook.net') !== false) { $signals['meta_pixel'] = true; $signal_sources['meta_pixel'][] = $page_url . ' (external)'; } // v3.3.52 P2.2 — non-Google stacks via external origins. if (strpos($blob, 'matomo') !== false || strpos($blob, 'piwik') !== false) { $signals['matomo'] = true; $signal_sources['matomo'][] = $page_url . ' (external)'; } if (strpos($blob, 'plausible.io') !== false) { $signals['plausible'] = true; $signal_sources['plausible'][] = $page_url . ' (external)'; } if (strpos($blob, 'segment.com') !== false) { $signals['segment'] = true; $signal_sources['segment'][] = $page_url . ' (external)'; } if (strpos($blob, 'posthog') !== false) { $signals['posthog'] = true; $signal_sources['posthog'][] = $page_url . ' (external)'; } } // v3.3.52 P2.2 — a non-Google measurement stack is still a valid stack. $has_google = $signals['gtm'] || $signals['ga']; $alt_detected = []; foreach (['matomo' => 'Matomo', 'plausible' => 'Plausible', 'segment' => 'Segment', 'posthog' => 'PostHog'] as $alt_key => $alt_label) { if (!empty($signals[$alt_key])) { $alt_detected[] = $alt_label; } } if (!$has_google && empty($alt_detected)) { $findings[] = $this->finding( 'analytics_measurement_stack_missing', I18n::tr('No visitor analytics tool found', 'لا توجد أداة لقياس الزيارات'), 'Analytics', 'med', -4, [ 'source' => 'runtime', 'details' => I18n::tr('No GTM/GA signal — not from inline page code, not from active plugins/adapters, and not from script sources.', 'لا توجد إشارة GTM/GA في الأكواد المضمّنة في الصفحة ولا في الإضافات أو المحوّلات المفعّلة ولا في مصادر السكربتات.'), 'detected' => $signals, 'signal_sources' => array_map(static function ($urls) { return array_slice(array_values(array_unique($urls)), 0, 3); }, $signal_sources), ], I18n::tr('Without an analytics tool you cannot see where visitors come from, what they do, or whether a change helped.', 'بدون أداة قياس لا تعرف من أين يأتي زوارك ولا ماذا يفعلون، ولا تستطيع قياس أثر أي تحسين.'), [ I18n::tr('Connect the right search-analytics platform to export keywords/pages.', 'اربط منصة تحليلات البحث المناسبة لتصدير الكلمات والصفحات.'), I18n::tr('Install Google Tag Manager or GA4/Site Kit. GTM is better if you manage more than one tag.', 'ثبّت أداة قياس مثل Google Analytics عن طريق Google Tag Manager أو إضافة Site Kit، ويُفضل Tag Manager إذا كنت تدير أكثر من أداة.'), I18n::tr('Use the free Microsoft Clarity for session recordings and heatmaps if your privacy policy allows.', 'استخدم Microsoft Clarity المجاني لتسجيل جلسات الزوار والخرائط الحرارية إذا سمحت سياسة الخصوصية بذلك.'), I18n::tr('Do not add paid APIs by default; this check expects only free/direct integrations.', 'لا يحتاج هذا الفحص أي خدمة مدفوعة، ويكفي تثبيت أدوات القياس المجانية.'), ], [ 'how' => I18n::tr('Re-run the audit after adding GTM/GA/Site Kit.', 'أعد الفحص بعد إضافة GTM/GA/Site Kit.'), 'pass_condition' => I18n::tr('Google Tag Manager or Google Analytics is detected.', 'يُرصد Google Tag Manager أو Google Analytics.'), ], $this->id() ); } elseif (!$has_google && !empty($alt_detected)) { // A non-Google analytics stack is present — instrumented, just not // GA/GTM. info/0, no penalty: we acknowledge it and only nudge // toward a search-analytics source (the SEO-specific data GA alternatives lack). $findings[] = $this->finding( 'analytics_non_google_stack_detected', I18n::tr('Non-Google analytics tool detected (', 'أداة قياس غير Google مرصودة (') . implode(I18n::tr(', ', '، '), $alt_detected) . ')', 'Analytics', 'info', 0, [ 'source' => 'runtime', 'details' => I18n::tr('Detected ', 'رُصد ') . implode(I18n::tr(', ', '، '), $alt_detected) . I18n::tr(' instead of GA/GTM. The site already has an analytics tool, so there is no penalty for missing measurement.', ' بدلًا من GA/GTM. الموقع مزوّد فعلًا بأداة قياس، فلا خصم على غياب القياس.'), 'detected' => $signals, 'signal_sources' => array_map(static function ($urls) { return array_slice(array_values(array_unique((array) $urls)), 0, 3); }, $signal_sources), ], I18n::tr('A privacy-respecting or self-hosted analytics tool (Matomo/Plausible/Segment/PostHog) is a valid measurement choice. The only difference from Google is the keyword/page data in a search-analytics platform, which these tools do not provide.', 'أداة قياس تحترم الخصوصية أو تعمل على خادمك (Matomo/Plausible/Segment/PostHog) خيار قياس صحيح. الفرق الوحيد عن Google هو بيانات الكلمات والصفحات في منصة تحليلات البحث وهي بيانات لا توفرها هذه الأدوات.'), [ I18n::tr('Stay on your current analytics tool; nothing needs to change for measurement coverage.', 'استمر على أداة القياس الحالية، فلا يلزم أي تغيير لتغطية القياس.'), I18n::tr('Still, connect the right search-analytics platform to export organic keywords/pages (SEO data your current tool does not capture).', 'اربط أيضًا Google Search Console لمعرفة الكلمات والصفحات التي تجلب الزيارات من البحث، فهذه بيانات لا تلتقطها أداة القياس وحدها.'), ], [ 'how' => I18n::tr('Make sure the search-analytics platform is connected; otherwise ignore this note.', 'تأكد من أن منصة تحليلات البحث متصلة وإلا فتجاهل هذه الملاحظة.'), 'pass_condition' => I18n::tr('The search-analytics platform is connected, or the note was intentionally ignored.', 'منصة تحليلات البحث متصلة، أو الملاحظة اتجاهلت بقصد.'), ], $this->id() ); } elseif (!$signals['clarity']) { $findings[] = $this->finding( 'analytics_clarity_not_detected', I18n::tr('Microsoft Clarity not detected (free UX diagnostics)', 'أداة Microsoft Clarity غير موجودة (أداة مجانية لفهم سلوك الزوار)'), 'Analytics', 'info', 0, [ 'source' => 'runtime', 'details' => I18n::tr('GTM/GA is present, but no Clarity signal is detected.', 'توجد أداة قياس لكن لا توجد Microsoft Clarity.'), 'detected' => $signals, ], I18n::tr('Clarity is optional, but it is a free way to see scroll depth, rage clicks, and UX obstacles that may explain weak organic conversion.', 'Clarity اختيارية، لكنها وسيلة مجانية لرؤية مدى تمرير الزوار ونقراتهم المتكررة وما يعيقهم، وهذا قد يفسر ضعف تحويل زيارات البحث.'), [ I18n::tr('If acceptable under your site\'s privacy policy, add Microsoft Clarity directly or via GTM.', 'إذا كانت سياسة خصوصية الموقع تسمح بذلك فأضف Clarity مباشرة أو عن طريق Tag Manager.'), I18n::tr('Use Clarity\'s findings to improve landing pages that receive traffic from the search-analytics platform.', 'استخدم نتائج Clarity لتحسين الصفحات التي تصلها زيارات البحث.'), ], [ 'how' => I18n::tr('Re-run the audit after adding Clarity, or ignore this note if you do not want it.', 'أعد الفحص بعد إضافة Clarity، أو تجاهل هذه الملاحظة إن لم تكن تريدها.'), 'pass_condition' => I18n::tr('Clarity is detected, or this note was ignored on purpose.', 'تُرصد Clarity، أو تم تجاهل هذه الملاحظة عمدًا.'), ], $this->id() ); } return $findings; } }