Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Commit ae1040f

Browse files
authored
Add WebSDK analytics instrumentation for site sections and engagement (#845)
1 parent f0b2e81 commit ae1040f

3 files changed

Lines changed: 230 additions & 0 deletions

File tree

src/components/Analytics.astro

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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>

src/components/overrides/Head.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
import { getCollection } from 'astro:content'
88
import SiteScripts from '../SiteScripts.astro'
9+
import Analytics from '../Analytics.astro'
910
import { baseSchemas } from '../../util/structured-data'
1011
import { relatedUserGuideFor } from '../../util/related-docs'
1112
@@ -123,6 +124,9 @@ const structuredData = {
123124
<!-- AWS Shortbread cookie consent -->
124125
<SiteScripts />
125126

127+
<!-- Analytics instrumentation (site sections, anchors, search, scroll, nav) -->
128+
<Analytics />
129+
126130
<!-- Structured Data -->
127131
<script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
128132

src/layouts/LandingLayout.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import ThemeProvider from 'virtual:starlight/components/ThemeProvider'
2525
2626
import Header from '../components/overrides/Header.astro'
2727
import SiteScripts from '../components/SiteScripts.astro'
28+
import Analytics from '../components/Analytics.astro'
2829
import { pathWithBase } from '../util/links'
2930
import { baseSchemas } from '../util/structured-data'
3031
@@ -138,6 +139,9 @@ Astro.locals.t = mockT
138139
<!-- AWS Shortbread cookie consent -->
139140
<SiteScripts />
140141

142+
<!-- Analytics instrumentation -->
143+
<Analytics />
144+
141145
<ThemeProvider />
142146

143147
<style is:global>

0 commit comments

Comments
 (0)