Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/helpers/search_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def format_highlight_label(field_name)

def link_to_result(result)
if result[:source_link].present?
link_to(result[:title], result[:source_link])
link_to(result[:title], result[:source_link], data: { 'matomo-record-id': result[:identifier], 'matomo-record-title': result[:title] })
else
result[:title]
end
Expand All @@ -45,7 +45,7 @@ def link_to_tab(target, label = nil)
end

def view_record(record_id)
link_to 'View full record', record_path(id: record_id), class: 'button button-primary'
link_to 'View full record', record_path(id: record_id), class: 'button button-primary', data: { 'matomo-record-id': record_id, 'matomo-record-title': "View full record for #{record_id}" }
end

# 'Coverage' and 'issued' seem to be the most prevalent types; 'coverage' is typically formatted as
Expand Down
1 change: 1 addition & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
import "matomo_events"
import "loading_spinner"

// Show the progress bar after 200 milliseconds, not the default 500
Expand Down
30 changes: 30 additions & 0 deletions app/javascript/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,33 @@ function initFilterToggle() {
}

initFilterToggle();

// Track filter clicks
document.addEventListener('click', function(event) {
const clickedElement = event.target;

// Check if the click is on a filter term link (nested inside .category-terms)
const filterLink = clickedElement.closest('.category-terms .term a');
if (filterLink) {
// Find the parent filter-category to get the filter name
const filterCategory = filterLink.closest('.filter-category');
if (filterCategory) {
// Get the filter name from the summary
const filterLabel = filterCategory.querySelector('.filter-label');
if (filterLabel && window.matomoTracker) {
// Extract the filter term from the link text (it's in a .name span)
const termSpan = filterLink.querySelector('.name');
const filterTerm = termSpan ? termSpan.textContent.trim() : 'unknown';

// Extract the filter category from the label text
const filterCategoryName = filterLabel.textContent.trim();

// Determine if this is adding or removing a filter
const isApplied = filterLink.classList.contains('applied');
const action = isApplied ? 'remove' : 'add';

window.matomoTracker.trackFilterClick(filterCategoryName, filterTerm, action);
}
}
}
}, true); // Use capture phase to catch events before they propagate
27 changes: 27 additions & 0 deletions app/javascript/loading_spinner.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ document.addEventListener('click', function(event) {
// Position the window at the top of the results
window.scrollTo({ top: 0, behavior: 'smooth' });

// Track pagination click with page information
const clickedParams = new URLSearchParams(clickedElement.search);
const pageParam = clickedParams.get('page');
const direction = clickedElement.matches('.first a') ? 'first' :
clickedElement.matches('.previous a') ? 'previous' : 'next';
if (window.matomoTracker) {
window.matomoTracker.trackPagination(pageParam || 1, direction);
}

window.pendingFocusAction = 'pagination';
}

Expand All @@ -80,6 +89,11 @@ document.addEventListener('click', function(event) {
// Position the window at the top of the results
window.scrollTo({ top: 0, behavior: 'smooth' });

// Track tab click
if (window.matomoTracker) {
window.matomoTracker.trackTabClick(newTab || 'unknown');
}

swapTabs(newTab);

window.pendingFocusAction = 'tab';
Expand All @@ -100,4 +114,17 @@ document.addEventListener('turbo:load', function(event) {
}
});

// Track record/result clicks
document.addEventListener('click', function(event) {
const clickedElement = event.target;

// Check if click is on a record link (has matomo-record-id data attribute)
const recordLink = clickedElement.closest('[data-matomo-record-id]');
if (recordLink && window.matomoTracker) {
const recordId = recordLink.getAttribute('data-matomo-record-id');
const recordTitle = recordLink.getAttribute('data-matomo-record-title');
window.matomoTracker.trackRecordClick(recordTitle || 'unknown', recordId || '');
}
}, true); // Use capture phase to ensure we catch the event


235 changes: 235 additions & 0 deletions app/javascript/matomo_events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/**
* Matomo Events Tracking Utility
*
* This module provides a unified interface for tracking events with Matomo,
* supporting both Matomo Tag Manager (_mtm) and Legacy Matomo (_paq).
*
* Usage:
* window.matomoTracker.trackSearch({ keyword: 'biology', filters: ['peer-reviewed'] })
* window.matomoTracker.trackTabClick('primo')
* window.matomoTracker.trackFilterClick('language', 'English')
* window.matomoTracker.trackPagination(2)
* window.matomoTracker.trackRecordClick('title-of-record')
*/

const MatomoTracker = (() => {
/**
* Detect which Matomo mode is active
* @returns {string} 'tagmanager' | 'legacy' | 'none'
*/
const detectMode = () => {
if (typeof window._mtm !== 'undefined') {
return 'tagmanager';
}
if (typeof window._paq !== 'undefined') {
return 'legacy';
}
return 'none';
};

/**
* Push event to Matomo Tag Manager via _mtm data layer
* Events pushed to _mtm are processed by Matomo Tag Manager container triggers
*
* @param {string} eventName - Event name (e.g., 'search', 'tab_click')
* @param {object} eventData - Key-value pairs for event properties
*/
const pushToTagManager = (eventName, eventData = {}) => {
if (typeof window._mtm === 'undefined') return;

const event = {
event: `matomo_${eventName}`,
...eventData,
};

window._mtm.push(event);

// Log for debugging in browser console
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
console.log(`[Matomo Tag Manager] Event pushed:`, event);
}
};

/**
* Push event to Legacy Matomo via _paq
* Uses trackEvent() method for custom events
*
* @param {string} category - Event category (e.g., 'search')
* @param {string} action - Event action (e.g., 'submit')
* @param {string} name - Event name (e.g., 'biology')
* @param {number} value - Optional numeric value
*/
const pushToLegacy = (category, action, name, value) => {
if (typeof window._paq === 'undefined') return;

const args = ['trackEvent', category, action, name];
if (value !== undefined && value !== null) {
args.push(value);
}

window._paq.push(args);

// Log for debugging in browser console
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
console.log(`[Matomo Legacy] Event pushed:`, { category, action, name, value });
}
};

/**
* Generic push method that routes to the appropriate Matomo mode
* For Tag Manager: sends as named event with object data
* For Legacy: sends as category/action/name tuple
*
* @param {object} config - Configuration object
* - eventName: string (e.g., 'search_submit')
* - category: string for legacy (e.g., 'Search')
* - action: string for legacy (e.g., 'Submit')
* - value: string/number for legacy (e.g., 'biology')
* - data: object for tag manager (e.g., { keyword: 'biology', filters: [...] })
*/
const push = (config) => {
const mode = detectMode();

if (mode === 'tagmanager') {
pushToTagManager(config.eventName, config.data);
} else if (mode === 'legacy') {
pushToLegacy(config.category, config.action, config.value);
}
};

// Public API
return {
/**
* Track a search submission
* @param {object} options
* - keyword: string (search query)
* - filters: array of applied filter strings (optional)
*/
trackSearch: (options = {}) => {
push({
eventName: 'search_submit',
category: 'Search',
action: 'Submit',
value: options.keyword || '',
data: {
keyword: options.keyword || '',
filters: options.filters || [],
filterCount: (options.filters || []).length,
},
});
},

/**
* Track a tab switch event
* @param {string} tabName - Name of the tab ('primo', 'timdex', 'all')
*/
trackTabClick: (tabName) => {
push({
eventName: 'tab_click',
category: 'Navigation',
action: 'Tab Switch',
value: tabName || '',
data: {
tab: tabName || '',
},
});
},

/**
* Track a filter interaction
* @param {string} category - Filter category (e.g., 'language', 'content_type')
* @param {string} term - Filter term (e.g., 'English')
* @param {string} action - 'add' or 'remove'
*/
trackFilterClick: (category, term, action = 'add') => {
push({
eventName: 'filter_click',
category: 'Filters',
action: `${action}_${category}`,
value: term || '',
data: {
filterCategory: category || '',
filterTerm: term || '',
filterAction: action || 'add',
},
});
},

/**
* Track pagination interaction
* @param {number} pageNumber - Page number (or offset)
* @param {string} direction - 'first' | 'previous' | 'next' | 'direct'
*/
trackPagination: (pageNumber, direction = 'direct') => {
push({
eventName: 'pagination_click',
category: 'Navigation',
action: 'Pagination',
value: String(pageNumber),
data: {
page: pageNumber,
direction: direction,
},
});
},

/**
* Track a record (result) click
* @param {string} recordTitle - Title of the record/result clicked
* @param {string} recordId - Unique identifier (if available)
*/
trackRecordClick: (recordTitle, recordId = '') => {
push({
eventName: 'record_click',
category: 'Results',
action: 'Click',
value: recordTitle || '',
data: {
recordTitle: recordTitle || '',
recordId: recordId || '',
},
});
},

/**
* Track advanced search panel toggle
* @param {string} panelName - Name of the panel ('advanced', 'geobox', 'geodistance')
* @param {boolean} isOpen - Whether panel is now open
*/
trackAdvancedSearchToggle: (panelName, isOpen) => {
push({
eventName: 'search_panel_toggle',
category: 'Search',
action: `Toggle ${panelName}`,
value: isOpen ? 'open' : 'closed',
data: {
panel: panelName,
state: isOpen ? 'open' : 'closed',
},
});
},

/**
* Track a virtual page view (for SPA navigation in Tag Manager mode)
* This supplements or replaces trackPageView for Hotwire navigation
* @param {string} url - Page URL
* @param {string} title - Page title
*/
trackPageView: (url, title) => {
// For Tag Manager, push a virtual pageview event
pushToTagManager('page_view', {
pageUrl: url || window.location.href,
pageTitle: title || document.title,
});

// Legacy Matomo handles this via turbo:load listener in _head.html.erb
},

mode: detectMode,
};
})();

// Expose globally for use in inline event handlers and other scripts
window.matomoTracker = MatomoTracker;

export default MatomoTracker;
33 changes: 32 additions & 1 deletion app/javascript/search_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ function togglePanelState(currentPanel) {

// Finally, enable or disable the search type of the current panel, based on whether it is open or not.
toggleSearch(currentPanel);

// Track panel toggle
if (window.matomoTracker) {
const panelName = currentPanel.id
.replace('-search-panel', '')
.replace('-', '_');
const isOpen = currentPanel.open || false;
window.matomoTracker.trackAdvancedSearchToggle(panelName, isOpen);
}
}

function toggleRequiredFieldset(panel) {
Expand Down Expand Up @@ -77,4 +86,26 @@ if (Array.from(allPanels).includes(geoboxPanel && geodistancePanel)) {
});
}

console.log('search_form.js loaded');
// Track search form submission
document.addEventListener('submit', function(event) {
const form = event.target;

// Check if this is the search form (has the basic-search-main field)
if (form.querySelector('input[name="q"]') && window.matomoTracker) {
const keyword = form.querySelector('input[name="q"]').value || '';

// Collect applied filters from the URL if available
const filters = [];
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of urlParams.entries()) {
if (key.startsWith('filter_') && value) {
filters.push(`${key}=${value}`);
}
}

window.matomoTracker.trackSearch({
keyword: keyword,
filters: filters
});
}
}, true); // Use capture phase to catch events before form submission
Loading
Loading