Skip to content

Add WebSDK analytics instrumentation for site sections and engagement#845

Open
yonib05 wants to merge 6 commits into
strands-agents:mainfrom
yonib05:feat/analytics-instrumentation
Open

Add WebSDK analytics instrumentation for site sections and engagement#845
yonib05 wants to merge 6 commits into
strands-agents:mainfrom
yonib05:feat/analytics-instrumentation

Conversation

@yonib05
Copy link
Copy Markdown
Member

@yonib05 yonib05 commented May 15, 2026

Description

Adds client-side analytics instrumentation that dispatches events through the existing AWS WebSDK. Currently the WebSDK is loaded but no custom events are being pushed. This wires up tracking for:

  • Page views enriched with site section hierarchy (derived from URL path segments)
  • In-page anchor and table-of-contents link clicks
  • Pagefind search usage (term + result count) via delegated input listener
  • Scroll depth (initial/max/latest, sent via beacon on unload + visibilitychange)
  • Header nav tab clicks, code copy button clicks, and outbound link clicks
  • Re-fires page-level events on Astro View Transitions for SPA navigation

All events use the standard _aws XDM namespace patterns (web.awsm.customCTAClick, web.awsm.search, web.awsm.pageInteraction) so they flow into CJA without additional schema work.

Related Issues

N/A

Type of Change

  • Other: Analytics instrumentation

Testing

Ran npm run dev and verified all event types dispatch correctly by attaching a listener to custom-awsm-acs-analytics-event-listener in the browser console and triggering each interaction.

Page view with site sections

Navigated to /docs/user-guide/quickstart/typescript/ and confirmed a single web.awsm.pageInteraction event fires with correct hierarchy:

{
  "eventType": "web.awsm.pageInteraction",
  "data": {
    "pageInteraction": {
      "name": "pageView",
      "siteSection": "user-guide",
      "subSection1": "quickstart",
      "subSection2": "typescript",
      "hierarchy": "user-guide|quickstart|typescript"
    }
  },
  "useBeacon": false
}

TOC anchor click

Clicked a starlight-toc link (#language-support). Confirmed one event:

{
  "eventType": "web.awsm.customCTAClick",
  "data": {
    "pageInteraction": {
      "click": { "name": "toc-click:language-support", "type": "linkClick" }
    }
  }
}

Code copy button

Clicked the copy button on a code block:

{
  "eventType": "web.awsm.customCTAClick",
  "data": {
    "pageInteraction": {
      "click": { "name": "code-copy:user-guide|quickstart|typescript", "type": "customClick" }
    }
  }
}

Outbound link click

Clicked an external link to docs.npmjs.com:

{
  "eventType": "web.awsm.customCTAClick",
  "data": {
    "pageInteraction": {
      "click": { "name": "outbound:docs.npmjs.com/downloading-and-installing-node-js-and-npm", "type": "linkClick" }
    }
  }
}

Nav tab click

Clicked the first .nav-tab element:

{
  "eventType": "web.awsm.customCTAClick",
  "data": {
    "pageInteraction": {
      "click": { "name": "nav-tab:Home", "type": "click" }
    }
  }
}

Init guard (no duplicate listeners)

Verified window.__strandsAnalyticsInit is set to true after first execution. On View Transition re-execution the IIFE returns early. Manually firing astro:page-load twice results in exactly 2 page view events (one per fire, no accumulation from duplicate listeners).

Scroll depth

beforeunload and visibilitychange listeners are registered. Uses a scrollSent flag to prevent double-sending when both events fire on the same page close.

Search

Uses a delegated input event listener on document targeting [data-pagefind-ui] input. Debounces at 800ms before reading result count. Full functional testing requires the Pagefind index (npm run build), not available in dev mode.

Checklist

  • I have read the CONTRIBUTING document
  • My changes follow the project's documentation style
  • I have tested the documentation locally using npm run dev
  • Links in the documentation are valid and working

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

…ngagement tracking

Wire up custom event dispatching through the AWS WebSDK to track page views
with site section hierarchy, in-page anchor and TOC clicks, search usage,
scroll depth, and navigation interactions. Handles Astro View Transitions
for SPA-style page loads.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

Documentation Preview Ready

Your documentation preview has been successfully deployed!

Preview URL: https://d3ehv1nix5p99z.cloudfront.net/pr-cms-845/docs/user-guide/quickstart/overview/

Updated at: 2026-05-19T17:39:45.861Z

Comment thread src/components/Analytics.astro
Comment thread src/components/Analytics.astro
Comment thread src/components/Analytics.astro Outdated
Comment thread src/components/Analytics.astro Outdated
Comment thread src/components/Analytics.astro Outdated
Comment thread src/components/Analytics.astro Outdated
Comment thread src/components/Analytics.astro Outdated
@github-actions
Copy link
Copy Markdown
Contributor

Assessment: Request Changes

The analytics instrumentation logic is well-structured and covers a good range of user interactions. However, there are a few functional issues that should be addressed before merging to avoid duplicate events and potential compliance concerns.

Review Categories
  • Event listener lifecycle: The is:inline script re-executes on Astro View Transitions, causing listener accumulation. Scroll, click, and mutation observers need guards or cleanup to prevent duplicates.
  • Cookie consent: Events dispatch without checking Shortbread consent status. Either gate on consent or document that the WebSDK handles this downstream.
  • Mobile reliability: beforeunload is unreliable on iOS; consider visibilitychange as a fallback for scroll depth.
  • Observer cleanup: The search MutationObserver is never disconnected.

The overall approach of using the existing WebSDK event listener pattern is sound and the code is readable.

…e mobile support

- Add initialization guard to prevent duplicate listeners on View Transitions
- Move scroll tracking setup out of the per-transition init path
- Add visibilitychange listener for reliable scroll depth on mobile
- Disconnect search MutationObserver on astro:before-swap
- Re-attach search observer on astro:page-load for new DOM
- Add e.target.closest guard for SVG text node edge case
- Rename shadowed variable to pageSections
- Add comment clarifying consent is enforced by the WebSDK listener
Comment thread src/components/Analytics.astro Outdated
Comment thread src/components/Analytics.astro
Comment thread src/components/Analytics.astro Outdated
Comment thread src/components/Analytics.astro
Comment thread src/components/Analytics.astro
Comment thread src/components/Analytics.astro Outdated
@github-actions
Copy link
Copy Markdown
Contributor

Assessment: Request Changes

The previous review feedback was well-addressed (init guard, observer cleanup, visibilitychange, consent comment, etc.). However, there's a remaining functional bug: trackPageView() fires twice on every navigation due to both the init-guard path and the astro:page-load listener triggering it independently.

Review Categories
  • Double-fire bug (Critical): The init guard at line 15 calls onPageTransition(), and the astro:page-load listener (line 252) also calls it — resulting in duplicate page view events on every navigation and initial load. Simplest fix: remove the onPageTransition() from the guard and let astro:page-load be the sole trigger.
  • Closure isolation (Important): Re-executions of the IIFE create new local scopes; resetScrollData() called from re-executions operates on throwaway scrollData. This is resolved if the double-fire is fixed.
  • Observer lifecycle (Minor): bodyObserver for search isn't tracked/disconnected on swap.
  • Scroll double-send (Minor): visibilitychange + beforeunload can both fire sendScrollDepth() for the same page.

The overall instrumentation coverage is comprehensive and the code is well-organized into logical sections.

…lick handlers

- Remove onPageTransition() from the init guard; let astro:page-load be
  the sole transition handler (fixes double trackPageView on initial load)
- Add scrollSent flag to prevent duplicate scroll depth dispatch when both
  visibilitychange and beforeunload fire
- Track and disconnect searchBodyObserver on astro:before-swap
- Merge anchor and nav click handlers into a single delegated listener
- Add comment explaining the partial aria-label selector for search dialog
@yonib05 yonib05 enabled auto-merge (squash) May 18, 2026 17:40
zastrowm
zastrowm previously approved these changes May 18, 2026
Comment thread src/components/Analytics.astro
Comment thread src/components/Analytics.astro Outdated
Comment thread src/components/Analytics.astro
Simpler approach: listen for input events on [data-pagefind-ui] input
via event delegation. Removes the MutationObserver + body observer
setup/teardown complexity entirely.
Track internal navigation clicks as inbound:{pathname} for diagnosing
navigation patterns and identifying dead-end pages.
setupClickTracking()
setupSearchTracking()
setupScrollTracking()
trackPageView()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: trackPageView() still fires twice on initial page load. Line 222 calls it explicitly, and astro:page-load (line 213) also fires on initial load per Astro docs, triggering trackPageView() again at line 214.

Suggestion: Remove the explicit trackPageView() at line 222 and let astro:page-load be the sole trigger. It fires on both initial load and View Transition navigations, so it covers all cases:

   // --- Initial setup (runs once) ---
   setupClickTracking()
   setupSearchTracking()
   setupScrollTracking()
-  trackPageView()
 })()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does seem odd; do we need to remove?

debounceTimer = setTimeout(updateScroll, 200)
}, { passive: true })

setTimeout(function () {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Similarly, resetScrollData() is called from astro:page-load on initial load (line 215), which conflicts with setupScrollTracking()'s own initial measurement at line 185-188. Both schedule a setTimeout(…, 500) to set scrollData.initial. Since resetScrollData first zeros the object, the race is harmless but redundant.

Suggestion: If trackPageView() is removed from line 222 (per above comment), consider also removing the initial measurement in setupScrollTracking() (lines 185-188) and letting resetScrollData() from the astro:page-load handler be the sole initializer. This makes the flow clearer — astro:page-load owns all per-page state initialization.

searchTimeout = setTimeout(function () {
var term = input.value
if (!term) return
var results = document.querySelectorAll('[data-pagefind-ui] .pagefind-ui__result')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The 800ms debounce fires after typing pauses, then immediately queries document.querySelectorAll('[data-pagefind-ui] .pagefind-ui__result'). However, Pagefind fetches results asynchronously — if the network response hasn't arrived within those 800ms, results.length will be 0 or reflect stale results from a previous query.

Suggestion: This is a known limitation and may be acceptable for analytics purposes (you're capturing user intent + approximate results). If accuracy matters, consider a slightly longer debounce (e.g., 1200ms) or observing the Pagefind results container for mutations after the input event. A brief comment noting this tradeoff would help future maintainers understand the design choice.

// --- 4. Scroll Depth ---

function updateScroll() {
var scrollTop = window.pageYOffset || document.documentElement.scrollTop
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: window.pageYOffset is deprecated (though still widely supported). Since this code already uses modern APIs like CustomEvent, visibilitychange, and { passive: true }, it would be more consistent to use the modern equivalent.

Suggestion: Replace with window.scrollY:

var scrollTop = window.scrollY || document.documentElement.scrollTop

@github-actions
Copy link
Copy Markdown
Contributor

Assessment: Request Changes

The code has been significantly improved from previous review rounds — the refactoring to a single click handler, input-based search tracking (eliminating MutationObserver complexity), and the scrollSent flag are all solid. One functional bug remains: trackPageView() still double-fires on initial page load.

Review Categories
  • Double-fire (Critical): trackPageView() at line 222 + the astro:page-load handler (which fires on initial load per Astro docs) results in duplicate page view events on every first load. Fix: remove line 222.
  • Timing (Suggestion): Search result count may be stale if Pagefind hasn't rendered by the time the debounce fires.
  • Deprecated API (Suggestion): window.pageYOffsetwindow.scrollY for consistency with other modern APIs used.

The overall structure is clean and well-commented. Once the double-fire is resolved, this should be good to go.

setupClickTracking()
setupSearchTracking()
setupScrollTracking()
trackPageView()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: trackPageView() fires twice on initial page load. This explicit call fires immediately, and then astro:page-load (line 213) fires shortly after — which also calls trackPageView(). Per Astro docs, astro:page-load fires on initial load and subsequent navigations when using the Client Router (Starlight's default).

Suggestion: Remove this line. Let the astro:page-load handler at line 213 be the sole trigger for page views on both initial load and navigations. This also eliminates the double resetScrollData() / initial-scroll-measurement on first load (since setupScrollTracking() at line 185 and resetScrollData() at line 215 both schedule the same setTimeout to capture initial scroll).

searchTimeout = setTimeout(function () {
var term = input.value
if (!term) return
var results = document.querySelectorAll('[data-pagefind-ui] .pagefind-ui__result')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The 800ms debounce fires and immediately queries the DOM for .pagefind-ui__result elements. However, Pagefind performs search asynchronously — results may still be rendering when this query executes, potentially reporting an inaccurate (usually lower) result count.

Suggestion: Consider observing the Pagefind result container for stabilization, or listening for Pagefind's internal completion signal if available. Alternatively, increase the timeout or add a brief secondary delay after the input debounce to let rendering complete:

searchTimeout = setTimeout(function () {
  var term = input.value
  if (!term) return
  // Give Pagefind a moment to render after debounce
  setTimeout(function () {
    var results = document.querySelectorAll('[data-pagefind-ui] .pagefind-ui__result')
    dispatchSearchEvent(term, results.length)
  }, 300)
}, 800)

@github-actions
Copy link
Copy Markdown
Contributor

Assessment: Request Changes

One functional issue remains from the previous review cycle: trackPageView() still double-fires on initial page load due to both the explicit call (line 222) and the astro:page-load listener (line 213) triggering independently. The fix is a one-line removal.

Review Categories
  • Double page view (Important): The explicit trackPageView() at the end of init is redundant since astro:page-load fires on initial load with Starlight's Client Router. Removing it also eliminates the duplicate initial scroll measurement.
  • Search result accuracy (Suggestion): The DOM query for result count fires on a fixed 800ms debounce, but Pagefind renders asynchronously — results may not be fully populated yet.

The codebase is much cleaner after addressing the prior round of feedback. The consolidated click handler, scrollSent guard, visibilitychange support, and input-based search tracking are all well-implemented.

dispatchCTAClick('outbound:' + link.hostname + link.pathname, 'linkClick')
}
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Tracking every inbound link click (any same-origin navigation) will generate extremely high event volume on a docs site where users frequently click between pages. This could inflate analytics pipeline costs and dilute meaningful engagement signals (outbound/anchor/nav-tab clicks).

Suggestion: Consider removing the inbound: tracking or making it opt-in per link (e.g., tracking only links with a specific data attribute). Page views already capture navigation patterns — tracking every internal click is largely redundant with that signal. If inbound click attribution is needed, consider sampling or limiting to specific content areas.

@github-actions
Copy link
Copy Markdown
Contributor

Assessment: Request Changes

One functional bug remains: trackPageView() fires twice on initial page load because both the explicit call at line 225 and the astro:page-load listener (which fires on initial load in Starlight's Client Router) both invoke it. The fix is a one-line removal.

Review Categories
  • Double-fire (Critical): Remove the explicit trackPageView() at line 225 — astro:page-load already handles initial + SPA navigations.
  • Event volume (Important): The inbound: link tracking on line 127 generates an event for every internal navigation click, which may be excessively noisy and redundant with page view tracking.

Great progress addressing all prior feedback — the init guard, consolidated click handler, input-event-based search tracking, and scroll depth deduplication are all solid improvements.

@yonib05 yonib05 requested a review from zastrowm May 19, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants