-
Notifications
You must be signed in to change notification settings - Fork 0
WIP DO NOT MERGE: Matomo Code-based Tracking #366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
djanelle-mit
wants to merge
42
commits into
main
Choose a base branch
from
dave-testing-matomo-code
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 40 commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
8f397eb
Simple link tracking
djanelle-mit 1de21a6
Matomo click helper function
djanelle-mit b330074
Removed manual tracking test
djanelle-mit afe58f3
Commenting click listener code
djanelle-mit 42a85f2
Visibility tracking helper refactor plus initial test attribute
djanelle-mit 7c9495f
Updated to make seen tracking work for Turbo:load
djanelle-mit 769c5d0
Testing results found tracking
djanelle-mit d7d5880
Add helper function to get tab names and apply to no results tracking
djanelle-mit f169c56
Attempt to interpolate function names in attributes for helper functions
djanelle-mit 1601c54
added child link tracking to -click attribute
djanelle-mit 4255038
Experimenting with adding parent element click tracking to tabs
djanelle-mit 1665023
Adding getElementText helper function and implementing on tabs
djanelle-mit b01d4b5
Testing sidebar link tracking and seen
djanelle-mit fd9045c
Experimenting with suggestion visibility tracking
djanelle-mit cb19cc9
Helper to find results page number
djanelle-mit b8d6f43
Updated -seen tracking to be truly when visible not when loaded in DOM
djanelle-mit 986031b
Adding capture to force the getActiveTab name helper to fire before l…
djanelle-mit 6625405
Testing intervention tracking
djanelle-mit 8945b41
Experimenting with Content Tracking for Interventions
djanelle-mit 4a124b4
Updated to listen for content tracking attributes loaded async
djanelle-mit a6c601d
removing unnecessary tracking from experiments
djanelle-mit 97ed976
Adding global alert partial from theme to introduce matomo tracking
djanelle-mit 8a4aca8
Batch of tracking on various elements
djanelle-mit f357887
Removing sidebar event tracking and adding result link click event
djanelle-mit a0f15e0
Experimenting with content tracking on Sidebar and Suggestions
djanelle-mit f79edd2
Fix markup bug in pagination
djanelle-mit 964c4e9
Fixed syntax of tracking from comma to semicolon
djanelle-mit 38d6b41
Added denominator to header link tracking event
djanelle-mit a9e5c4b
Add event tracking to sidebar on top of content tracking
djanelle-mit b32ea8a
Moved sidebar event tracking and added to callouts
djanelle-mit 8aabb24
using correct helper in pagination
djanelle-mit e8b5d52
testing browzine link event tracking
djanelle-mit 64150c4
browzine tracking event category changed
djanelle-mit ab7b572
Libkey link tracking
djanelle-mit 16fceaf
fixed type in footer event tracking
djanelle-mit 5c6fac4
Adding result count event tracking
djanelle-mit 9e5073a
Content and event tracking for results
djanelle-mit 099e87e
Browzine content tracking
djanelle-mit 903dcf7
Renaming escape hatch events and adding search performed event
djanelle-mit c977136
Added content tracking to result title links
djanelle-mit 24acc28
Fixing syntax error in title content tracking
djanelle-mit 1fe9f30
Added content tracking to Availability LInk
djanelle-mit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| // Matomo event tracking via data attributes. | ||
| // | ||
| // CLICK TRACKING | ||
| // Add `data-matomo-click="Category, Action, Name"` to any element to track | ||
| // clicks as Matomo events. The Name segment is optional. | ||
| // | ||
| // Examples: | ||
| // <a href="/file.pdf" data-matomo-click="Downloads, PDF Click, My Paper">Download</a> | ||
| // <button data-matomo-click="Search, Boolean Toggle">AND/OR</button> | ||
| // | ||
| // Event delegation on `document` means this works for elements loaded | ||
| // asynchronously (Turbo frames, content-loader, etc.) without re-binding. | ||
| // | ||
| // SEEN TRACKING | ||
| // Add `data-matomo-seen="Category, Action, Name"` to any element to fire a | ||
| // Matomo event when that element becomes visible in the viewport. The Name | ||
| // segment is optional. Each element fires at most once per page load. | ||
| // Works for elements present on initial page load and for elements injected | ||
| // later by Turbo frames or async content loaders. | ||
| // | ||
| // Examples: | ||
| // <div data-matomo-seen="Impressions, Result Card, Alma">...</div> | ||
| // <a data-matomo-seen="Promotions, Banner Shown">...</a> | ||
| // | ||
| // DYNAMIC VALUES ({{...}} interpolation) | ||
| // Wrap a helper name in double curly braces anywhere inside a segment to have | ||
| // it replaced with the return value of that function at tracking time. Helpers | ||
| // must be registered on `window.MatomoHelpers` (see bottom of this file). | ||
| // Multiple tokens in one segment are supported. | ||
| // | ||
| // Examples: | ||
| // <h2 data-matomo-seen="Search, Results Found, Tab: {{getActiveTabName}}">...</h2> | ||
| // <a data-matomo-click="Nav, {{getActiveTabName}} Link Click">...</a> | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Shared helper | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // Parse a "Category, Action, Name" attribute string and push a trackEvent call | ||
| // to the Matomo queue. Name is optional; returns early if fewer than 2 parts. | ||
| // `context` is the DOM element that triggered the event; it is forwarded to | ||
| // every helper so functions like getElementText can reference it. | ||
| function pushMatomoEvent(raw, context) { | ||
|
|
||
| // Split on commas, trim whitespace from each part, drop any empty strings. | ||
| const parts = (raw || "").split(",").map((s) => s.trim()).filter(Boolean); | ||
| // Matomo requires at least a Category and an Action. | ||
| if (parts.length < 2) return; | ||
|
|
||
| // Resolve any {{functionName}} tokens by calling the matching helper. | ||
| // Each token is replaced in-place, so it can appear anywhere in a segment. | ||
| // The context element is passed as the first argument so helpers can | ||
| // inspect the element that triggered the event (e.g. getElementText). | ||
| const helpers = window.MatomoHelpers || {}; | ||
| const resolved = parts.map((part) => | ||
| part.replace(/\{\{(\w+)\}\}/g, (_, fnName) => { | ||
| const fn = helpers[fnName]; | ||
| // Call the function if it exists; otherwise leave the token as-is. | ||
| return (typeof fn === "function") ? fn(context) : `{{${fnName}}}`; | ||
| }) | ||
| ); | ||
|
|
||
| // Destructure into named variables; `name` will be undefined if not provided. | ||
| const [category, action, name] = resolved; | ||
|
|
||
| // Ensure _paq exists even if the Matomo snippet hasn't loaded yet | ||
| // (e.g. in development). Matomo will replay queued calls once it initialises. | ||
| window._paq = window._paq || []; | ||
| const payload = ["trackEvent", category, action]; | ||
| if (name) payload.push(name); | ||
| window._paq.push(payload); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Click tracking | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // Attach a single click listener to the entire document using the capture | ||
| // phase (third argument { capture: true }). Capture phase fires top-down | ||
| // before any bubble-phase listeners, which guarantees helpers like | ||
| // getActiveTabName() read pre-click DOM state before other listeners | ||
| // (e.g. loading_spinner.js's swapTabs) synchronously update it. | ||
| document.addEventListener("click", (event) => { | ||
| // Walk up the DOM from the clicked element to find the nearest ancestor | ||
| // (or the element itself) that has a data-matomo-click attribute. | ||
| const el = event.target.closest("[data-matomo-click]"); | ||
| // If no such element exists in the ancestor chain, ignore this click. | ||
| if (!el) return; | ||
|
|
||
| // Only fire when the click originated from an interactive element (link, | ||
| // button, or form control). This allows data-matomo-click to be placed on | ||
| // a container and track only meaningful interactions within it, ignoring | ||
| // clicks on surrounding text, padding, or decorative children. | ||
| const interactive = event.target.closest("a, button, input, select, textarea"); | ||
| if (!interactive) return; | ||
|
|
||
| // Confirm the interactive element is actually inside the tracked container | ||
| // (guards against the unlikely case where closest() finds an ancestor of el). | ||
| if (!el.contains(interactive) && el !== interactive) return; | ||
|
|
||
| // Pass the interactive element as context so helpers like getElementText | ||
| // can read the text of the specific link or button that was clicked. | ||
| pushMatomoEvent(el.dataset.matomoClick, interactive); | ||
| }, { capture: true }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Seen tracking | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // Track elements already registered with the viewport observer to avoid | ||
| // double-registration if the same node is added to the DOM more than once. | ||
| const seenRegistered = new WeakSet(); | ||
|
|
||
| // Fire a Matomo event when an observed element intersects the viewport. | ||
| // Unobserve immediately so the event fires at most once per element. | ||
| const viewportObserver = new IntersectionObserver((entries) => { | ||
| entries.forEach((entry) => { | ||
| if (!entry.isIntersecting) return; | ||
| // Stop watching — we only want to fire once per element. | ||
| viewportObserver.unobserve(entry.target); | ||
| pushMatomoEvent(entry.target.dataset.matomoSeen, entry.target); | ||
| }); | ||
| }); | ||
|
|
||
| // Register a single element with the viewport observer if it carries | ||
| // data-matomo-seen and hasn't been registered yet. | ||
| function registerIfSeen(el) { | ||
| // Only process element nodes (not text nodes, comments, etc.). | ||
| if (el.nodeType !== Node.ELEMENT_NODE) return; | ||
| // Skip if already registered. | ||
| if (seenRegistered.has(el)) return; | ||
|
|
||
| // Register the element itself if it has the attribute. | ||
| if (el.dataset.matomoSeen) { | ||
| seenRegistered.add(el); | ||
| viewportObserver.observe(el); | ||
| } | ||
|
|
||
| // Also register any descendants — content loaders often inject a whole | ||
| // subtree at once, so walking deep ensures every marked element is caught. | ||
| el.querySelectorAll("[data-matomo-seen]").forEach((child) => { | ||
| if (seenRegistered.has(child)) return; | ||
| seenRegistered.add(child); | ||
| viewportObserver.observe(child); | ||
| }); | ||
| } | ||
|
|
||
| // Register all elements already present in the DOM on initial page load. | ||
| document.querySelectorAll("[data-matomo-seen]").forEach((el) => { | ||
| seenRegistered.add(el); | ||
| viewportObserver.observe(el); | ||
| }); | ||
|
|
||
| // Watch for any new nodes added to the DOM after initial load. | ||
| // --------------------------------------------------------------------------- | ||
| // Matomo native content tracking | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // Matomo's built-in content tracking (data-track-content / data-content-name / | ||
| // data-content-piece) only scans the DOM at page load. For content injected | ||
| // asynchronously (e.g. by the content-loader Stimulus controller), we must | ||
| // manually notify Matomo by calling trackContentImpressionsWithinNode on the | ||
| // newly-added node. | ||
| function trackContentImpressionsIfPresent(el) { | ||
| if (el.nodeType !== Node.ELEMENT_NODE) return; | ||
| // Check the element itself or any descendant for data-track-content. | ||
| const hasContent = | ||
| el.hasAttribute("data-track-content") || | ||
| el.querySelector("[data-track-content]") !== null; | ||
| if (!hasContent) return; | ||
|
|
||
| window._paq = window._paq || []; | ||
| // Ask Matomo to scan the subtree for content impressions. | ||
| window._paq.push(["trackContentImpressionsWithinNode", el]); | ||
| } | ||
|
|
||
| // Watch for any new nodes added to the DOM after initial load. | ||
| // MutationObserver fires synchronously after each DOM mutation, so it catches | ||
| // both Turbo frame renders and content-loader replacements immediately. | ||
| const observer = new MutationObserver((mutations) => { | ||
| mutations.forEach((mutation) => { | ||
| // Each mutation record lists the nodes that were added in this batch. | ||
| mutation.addedNodes.forEach((node) => { | ||
| registerIfSeen(node); | ||
| trackContentImpressionsIfPresent(node); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| // Observe the entire document subtree so no async insertion is missed. | ||
| observer.observe(document.body, { childList: true, subtree: true }); | ||
|
|
||
| // Turbo Drive navigation replaces document.body with a brand new element, | ||
| // which detaches the MutationObserver from the old body. Re-scan and | ||
| // re-attach on every turbo:load so full-page navigations are handled. | ||
| // (Turbo frame and content-loader updates are covered by the observer above | ||
| // because they mutate within the existing body rather than replacing it.) | ||
| document.addEventListener("turbo:load", () => { | ||
| // Register any seen elements that arrived with the navigation. | ||
| document.querySelectorAll("[data-matomo-seen]").forEach((el) => { | ||
| if (seenRegistered.has(el)) return; | ||
| seenRegistered.add(el); | ||
| viewportObserver.observe(el); | ||
| }); | ||
|
|
||
| // Re-attach the MutationObserver to the new document.body instance. | ||
| observer.observe(document.body, { childList: true, subtree: true }); | ||
| }); | ||
|
|
||
|
|
||
| // =========================================================================== | ||
| // HELPER FUNCTIONS | ||
| // Custom JS to enhance the payload information we provide to Matomo. | ||
| // =========================================================================== | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Get the name of the active search results tab, if any. | ||
| // --------------------------------------------------------------------------- | ||
| function getActiveTabName() { | ||
| var tabs = document.querySelector('#tabs'); | ||
| if (!tabs) { | ||
| return "None"; // #tabs not found | ||
| } | ||
|
|
||
| var activeAnchor = tabs.querySelector('a.active'); | ||
| if (!activeAnchor) { | ||
| return "None"; // no active tab | ||
| } | ||
|
|
||
| return activeAnchor.textContent.trim(); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Get the visible text of the element that triggered the event. | ||
| // For click tracking this is the interactive element (link, button, etc.). | ||
| // For seen tracking this is the element carrying data-matomo-seen. | ||
| // Returns an empty string if no context element is available. | ||
| // --------------------------------------------------------------------------- | ||
| function getElementText(el) { | ||
| if (!el) return ""; | ||
| return el.textContent.trim(); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Get the current results page number from the `page` URL parameter. | ||
| // Returns "1" when the parameter is absent (the first page has no page param). | ||
| // --------------------------------------------------------------------------- | ||
| function getCurrentResultsPage() { | ||
| const params = new URLSearchParams(window.location.search); | ||
| return params.get("page") || "1"; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Register helpers on window.MatomoHelpers so they can be referenced with the | ||
| // {{functionName}} syntax in data-matomo-seen and data-matomo-click attributes. | ||
| // Add new helpers here as needed. | ||
| // --------------------------------------------------------------------------- | ||
| window.MatomoHelpers = { | ||
| getActiveTabName, | ||
| getElementText, | ||
| getCurrentResultsPage, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <% if ENV['GLOBAL_ALERT'] %> | ||
| <div class="wrap-notices info layout-band"> | ||
| <div class="wrap-notice"> | ||
| <div class="alert alert-global" data-matomo-click="Banner, Link Engaged, Link: {{getElementText}}"> | ||
| <h1 class="title"><%= sanitize(ENV['GLOBAL_ALERT']) %></h1> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <% end %> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.