|
| 1 | +--- |
| 2 | +/** |
| 3 | + * Analytics instrumentation via AWS WebSDK (Adobe Analytics under the hood). |
| 4 | + * Dispatches custom events through the WebSDK's event listener system. |
| 5 | + * Included in Head.astro for all pages. |
| 6 | + */ |
| 7 | +--- |
| 8 | + |
| 9 | +<script is:inline> |
| 10 | +;(function () { |
| 11 | + 'use strict' |
| 12 | + |
| 13 | + // Prevent duplicate initialization on View Transition re-executions. |
| 14 | + // The astro:page-load listener (registered on first init) handles transitions. |
| 15 | + if (window.__strandsAnalyticsInit) return |
| 16 | + window.__strandsAnalyticsInit = true |
| 17 | + |
| 18 | + var CUSTOM_EVENT_LISTENER = 'custom-awsm-acs-analytics-event-listener' |
| 19 | + var SEARCH_EVENT_LISTENER = 'custom-awsm-acs-search-event-listener' |
| 20 | + |
| 21 | + var scrollData = { initial: 0, max: 0, latest: 0 } |
| 22 | + var scrollSent = false |
| 23 | + |
| 24 | + // --- Helpers --- |
| 25 | + |
| 26 | + function deriveSections(pathname) { |
| 27 | + var segments = pathname.replace(/^\/|\/$/g, '').split('/').filter(Boolean) |
| 28 | + var start = segments[0] === 'docs' ? 1 : 0 |
| 29 | + return { |
| 30 | + siteSection: segments[start] || 'home', |
| 31 | + subSection1: segments[start + 1] || '', |
| 32 | + subSection2: segments[start + 2] || '', |
| 33 | + hierarchy: segments.slice(start).join('|'), |
| 34 | + } |
| 35 | + } |
| 36 | + |
| 37 | + // Consent is enforced by the WebSDK listener itself - events dispatched here |
| 38 | + // are silently dropped if the user has not accepted performance cookies via Shortbread. |
| 39 | + function dispatchAnalyticsEvent(eventType, data, useBeacon) { |
| 40 | + var detail = { |
| 41 | + eventType: eventType, |
| 42 | + data: data, |
| 43 | + useBeacon: !!useBeacon, |
| 44 | + } |
| 45 | + window.dispatchEvent(new CustomEvent(CUSTOM_EVENT_LISTENER, { detail: detail })) |
| 46 | + } |
| 47 | + |
| 48 | + function dispatchCTAClick(name, type) { |
| 49 | + dispatchAnalyticsEvent('web.awsm.customCTAClick', { |
| 50 | + pageInteraction: { |
| 51 | + click: { name: name, type: type || 'customClick' }, |
| 52 | + }, |
| 53 | + }) |
| 54 | + } |
| 55 | + |
| 56 | + function dispatchSearchEvent(searchTerm, resultCount) { |
| 57 | + window.dispatchEvent( |
| 58 | + new CustomEvent(SEARCH_EVENT_LISTENER, { |
| 59 | + detail: { |
| 60 | + eventType: 'web.awsm.search', |
| 61 | + data: { |
| 62 | + searchOperations: { |
| 63 | + searchTerm: searchTerm, |
| 64 | + searchFilter: [], |
| 65 | + resultCount: resultCount, |
| 66 | + }, |
| 67 | + }, |
| 68 | + }, |
| 69 | + }) |
| 70 | + ) |
| 71 | + } |
| 72 | + |
| 73 | + // --- 1. Page View with Site Sections --- |
| 74 | + |
| 75 | + function trackPageView() { |
| 76 | + var sections = deriveSections(window.location.pathname) |
| 77 | + dispatchAnalyticsEvent('web.awsm.pageInteraction', { |
| 78 | + pageInteraction: { |
| 79 | + name: 'pageView', |
| 80 | + siteSection: sections.siteSection, |
| 81 | + subSection1: sections.subSection1, |
| 82 | + subSection2: sections.subSection2, |
| 83 | + hierarchy: sections.hierarchy, |
| 84 | + }, |
| 85 | + }) |
| 86 | + } |
| 87 | + |
| 88 | + // --- 2. Click Tracking (anchors, nav tabs, code copy, outbound) --- |
| 89 | + |
| 90 | + function setupClickTracking() { |
| 91 | + document.addEventListener('click', function (e) { |
| 92 | + if (!e.target || !e.target.closest) return |
| 93 | + |
| 94 | + // Anchor / TOC clicks |
| 95 | + var anchorLink = e.target.closest('a[href^="#"]') |
| 96 | + if (anchorLink) { |
| 97 | + var anchor = anchorLink.getAttribute('href').replace('#', '') |
| 98 | + if (!anchor) return |
| 99 | + var isToc = !!anchorLink.closest('nav.right-sidebar, [class*="toc"], starlight-toc') |
| 100 | + var prefix = isToc ? 'toc-click' : 'anchor-click' |
| 101 | + dispatchCTAClick(prefix + ':' + anchor, 'linkClick') |
| 102 | + return |
| 103 | + } |
| 104 | + |
| 105 | + // Nav tab clicks |
| 106 | + var navTab = e.target.closest('.nav-tab, .mobile-nav-link') |
| 107 | + if (navTab) { |
| 108 | + var label = navTab.textContent.trim().replace(/\s*↗$/, '') |
| 109 | + dispatchCTAClick('nav-tab:' + label, 'click') |
| 110 | + return |
| 111 | + } |
| 112 | + |
| 113 | + // Code copy button |
| 114 | + var copyBtn = e.target.closest('.copy button, button.copy, [data-code] button') |
| 115 | + if (copyBtn) { |
| 116 | + var pageSections = deriveSections(window.location.pathname) |
| 117 | + dispatchCTAClick('code-copy:' + pageSections.hierarchy, 'customClick') |
| 118 | + return |
| 119 | + } |
| 120 | + |
| 121 | + // Outbound links |
| 122 | + var link = e.target.closest('a[href]') |
| 123 | + if (link && link.hostname && link.hostname !== window.location.hostname) { |
| 124 | + dispatchCTAClick('outbound:' + link.hostname + link.pathname, 'linkClick') |
| 125 | + } |
| 126 | + }) |
| 127 | + } |
| 128 | + |
| 129 | + // --- 3. Search Events --- |
| 130 | + |
| 131 | + function setupSearchTracking() { |
| 132 | + var searchTimeout = null |
| 133 | + |
| 134 | + document.addEventListener('input', function (e) { |
| 135 | + var input = e.target |
| 136 | + if (!input || !input.matches || !input.matches('[data-pagefind-ui] input')) return |
| 137 | + |
| 138 | + clearTimeout(searchTimeout) |
| 139 | + // 800ms debounce captures user intent; result count may lag if Pagefind |
| 140 | + // hasn't responded yet, but this is acceptable for analytics purposes. |
| 141 | + searchTimeout = setTimeout(function () { |
| 142 | + var term = input.value |
| 143 | + if (!term) return |
| 144 | + var results = document.querySelectorAll('[data-pagefind-ui] .pagefind-ui__result') |
| 145 | + dispatchSearchEvent(term, results.length) |
| 146 | + }, 800) |
| 147 | + }) |
| 148 | + } |
| 149 | + |
| 150 | + // --- 4. Scroll Depth --- |
| 151 | + |
| 152 | + function updateScroll() { |
| 153 | + var scrollTop = window.scrollY || document.documentElement.scrollTop |
| 154 | + var docHeight = document.documentElement.scrollHeight - window.innerHeight |
| 155 | + var depth = docHeight > 0 ? Math.round((scrollTop / docHeight) * 100) : 0 |
| 156 | + scrollData.latest = depth |
| 157 | + if (depth > scrollData.max) scrollData.max = depth |
| 158 | + } |
| 159 | + |
| 160 | + function sendScrollDepth() { |
| 161 | + if (scrollSent) return |
| 162 | + scrollSent = true |
| 163 | + updateScroll() |
| 164 | + dispatchAnalyticsEvent( |
| 165 | + 'web.awsm.pageInteraction', |
| 166 | + { |
| 167 | + pageInteraction: { |
| 168 | + name: 'scroll', |
| 169 | + scrollDepths: { |
| 170 | + initial: scrollData.initial, |
| 171 | + max: scrollData.max, |
| 172 | + latest: scrollData.latest, |
| 173 | + }, |
| 174 | + }, |
| 175 | + }, |
| 176 | + true |
| 177 | + ) |
| 178 | + } |
| 179 | + |
| 180 | + function setupScrollTracking() { |
| 181 | + var debounceTimer |
| 182 | + window.addEventListener('scroll', function () { |
| 183 | + clearTimeout(debounceTimer) |
| 184 | + debounceTimer = setTimeout(updateScroll, 200) |
| 185 | + }, { passive: true }) |
| 186 | + |
| 187 | + window.addEventListener('beforeunload', sendScrollDepth) |
| 188 | + |
| 189 | + // visibilitychange is more reliable on mobile (iOS Safari doesn't always fire beforeunload) |
| 190 | + document.addEventListener('visibilitychange', function () { |
| 191 | + if (document.visibilityState === 'hidden') { |
| 192 | + sendScrollDepth() |
| 193 | + } |
| 194 | + }) |
| 195 | + } |
| 196 | + |
| 197 | + function resetScrollData() { |
| 198 | + scrollData.initial = 0 |
| 199 | + scrollData.max = 0 |
| 200 | + scrollData.latest = 0 |
| 201 | + scrollSent = false |
| 202 | + setTimeout(function () { |
| 203 | + updateScroll() |
| 204 | + scrollData.initial = scrollData.latest |
| 205 | + }, 500) |
| 206 | + } |
| 207 | + |
| 208 | + // --- 5. SPA / View Transition Support --- |
| 209 | + |
| 210 | + document.addEventListener('astro:page-load', function () { |
| 211 | + trackPageView() |
| 212 | + resetScrollData() |
| 213 | + }) |
| 214 | + |
| 215 | + // --- Initial setup (runs once) --- |
| 216 | + // astro:page-load fires on initial load and all subsequent navigations, |
| 217 | + // so it handles trackPageView() and resetScrollData() for every page. |
| 218 | + setupClickTracking() |
| 219 | + setupSearchTracking() |
| 220 | + setupScrollTracking() |
| 221 | +})() |
| 222 | +</script> |
0 commit comments