Widget tracking allows you to monitor user interactions, performance metrics, and business insights from Sportradar widgets integrated into your website. With comprehensive tracking, you can:
The tracking system provides real-time event data through the onTrack callback function, enabling seamless integration with your analytics infrastructure.
This tutorial is intended for engineers involved in integration of Sportradar widgets into their online infrastructure, who are building custom tracking of how Sportradar widgets are used inside their online environment.
In this tutorial you will learn:
To get through this tutorial, you will need:
Understanding how widget tracking works helps you implement effective monitoring:
onTrack function receives the eventKey Benefits:
The onTrack function is a callback property available on every widget's API. This function is invoked whenever any trackable event occurs within the widget.
Function Signature:
function onTrack(eventType: string, eventData: object): voidParameters:
| Parameter | Type | Description |
|---|---|---|
| eventType | string | The type of event that occurred |
| eventData | object | Contextual data about the event |
Triggered when: Widget receives new data or data updates
Use cases:
Event Data Structure:
{
// Widget metadata
scheme: 'https',
solution: 'sir',
component: 'match.scoreboard',
version: '2.1.0',
language: 'en',
channel: 'web',
// Content identifiers
sportId: 1,
categoryId: 123,
tournamentId: 456,
seasonId: 789,
matchId: 12345,
// Team information (when applicable)
team1uid: 'team_123',
team2uid: 'team_456',
// Location data (when applicable)
continentId: 1,
venueId: 789,
// User context
user: 'anonymous_user_id',
// Error information (when error occurs)
error: null | {
message: 'Error description',
code: 'ERROR_CODE',
details: {}
}
}Example Implementation:
const onTrack = (eventType, data) => {
if (eventType === 'data_change') {
if (data.error) {
console.error('Widget failed to load:', data.error);
// Hide widget or show error message
document.getElementById('widget-container').style.display = 'none';
} else {
console.log('Widget loaded successfully:', data.component);
// Analytics tracking
analytics.track('widget_loaded', {
widget_type: data.component,
match_id: data.matchId,
sport_id: data.sportId
});
}
}
};Let's build a comprehensive tracking system step by step:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Widget Tracking Implementation</title>
<style>
.analytics-dashboard {
background: #f5f5f5;
padding: 20px;
margin: 10px 0;
border-radius: 8px;
}
.event-log {
background: #fff;
border: 1px solid #ddd;
padding: 10px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
}
</style>
</head>
<body>
<div class="analytics-dashboard">
<h3>Live Analytics Dashboard</h3>
<div id="event-log" class="event-log"></div>
</div>
<div id="sr-widget"></div>
<script>
// Analytics tracking system
const analytics = {
events: [],
track: function(eventName, properties) {
const event = {
name: eventName,
properties: properties,
timestamp: new Date().toISOString()
};
this.events.push(event);
this.displayEvent(event);
// Send to your analytics service
this.sendToAnalytics(event);
},
displayEvent: function(event) {
const log = document.getElementById('event-log');
const entry = document.createElement('div');
entry.textContent = `${event.timestamp}: ${event.name} - ${JSON.stringify(event.properties)}`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
},
sendToAnalytics: function(event) {
// Integration with your analytics platform
console.log('Sending to analytics:', event);
}
};
// Comprehensive tracking callback
const trackingCallback = (eventType, data) => {
switch(eventType) {
case "data_change":
if (data.error) {
analytics.track('widget_error', {
component: data.component,
error_message: data.error.message,
match_id: data.matchId
});
} else {
analytics.track('widget_loaded', {
component: data.component,
match_id: data.matchId,
sport_id: data.sportId,
load_time: Date.now() - startTime
});
}
break;
case "odds_click":
analytics.track('odds_engagement', {
odds_value: data.odds?.value,
market_type: data.odds?.market,
is_visible: data.visible
});
break;
case "social_share":
analytics.track('content_shared', {
platform: data.target,
widget_source: data.source
});
break;
case "license_error":
analytics.track('license_error', {
error_code: data.errorCode,
component: data.component
});
break;
default:
analytics.track('unknown_event', {
event_type: eventType,
data: data
});
}
};
// Track widget load start time
const startTime = Date.now();
// Initialize Sportradar widgets
(function(a,b,c,d,e,f,g,h,i){a[e]||(i=a[e]=function(){(a[e].q=a[e].q||[]).push(arguments)},i.l=1*new Date,i.o=f,
g=b.createElement(c),h=b.getElementsByTagName(c)[0],g.async=1,g.src=d,g.setAttribute("n",e),h.parentNode.insertBefore(g,h)
)})(window,document,"script","https://widgets.sir.sportradar.com/sportradar/widgetloader","SIR", {
language: 'en'
});
// Load widget with tracking
SIR('addWidget', '#sr-widget', 'match.scoreboard', {
matchId: 12345,
onTrack: trackingCallback
});
</script>
</body>
</html>Real-world examples for popular analytics platforms:
// Google Analytics 4 integration
const trackingCallback = (eventType, data) => {
switch(eventType) {
case "data_change":
if (!data.error) {
gtag('event', 'widget_loaded', {
event_category: 'Widget',
event_label: data.component,
custom_parameters: {
match_id: data.matchId,
sport_id: data.sportId,
widget_version: data.version
}
});
} else {
gtag('event', 'widget_error', {
event_category: 'Widget',
event_label: data.component,
custom_parameters: {
error_message: data.error.message,
error_code: data.error.code
}
});
}
break;
case "odds_click":
gtag('event', 'odds_interaction', {
event_category: 'Engagement',
event_label: 'odds_click',
value: data.odds?.value || 0,
custom_parameters: {
market_type: data.odds?.market,
selection: data.odds?.selection,
match_id: data.odds?.matchId
}
});
break;
case "social_share":
gtag('event', 'share', {
method: data.target,
content_type: 'widget',
content_id: data.source
});
break;
}
};For high-traffic applications, implement intelligent event filtering:
// Advanced event filtering system
class EventFilter {
constructor() {
this.filters = new Map();
this.rateLimits = new Map();
}
// Add filter conditions
addFilter(eventType, filterFn) {
if (!this.filters.has(eventType)) {
this.filters.set(eventType, []);
}
this.filters.get(eventType).push(filterFn);
}
// Add rate limiting
addRateLimit(eventType, maxEventsPerMinute) {
this.rateLimits.set(eventType, {
max: maxEventsPerMinute,
events: [],
lastCleanup: Date.now()
});
}
// Check if event should be tracked
shouldTrack(eventType, data) {
// Apply filters
const filters = this.filters.get(eventType) || [];
for (const filter of filters) {
if (!filter(data)) {
return false;
}
}
// Apply rate limiting
if (this.rateLimits.has(eventType)) {
return this.checkRateLimit(eventType);
}
return true;
}
checkRateLimit(eventType) {
const limit = this.rateLimits.get(eventType);
const now = Date.now();
// Clean old events (older than 1 minute)
if (now - limit.lastCleanup > 60000) {
limit.events = limit.events.filter(time => now - time < 60000);
limit.lastCleanup = now;
}
if (limit.events.length >= limit.max) {
return false;
}
limit.events.push(now);
return true;
}
}
// Usage example
const eventFilter = new EventFilter();
// Only track odds clicks for high-value odds
eventFilter.addFilter('odds_click', (data) => {
return data.odds?.value > 2.0;
});
// Limit data_change events to 10 per minute
eventFilter.addRateLimit('data_change', 10);
// Enhanced tracking callback with filtering
const trackingCallback = (eventType, data) => {
if (!eventFilter.shouldTrack(eventType, data)) {
return; // Skip tracking this event
}
// Proceed with normal tracking
processEvent(eventType, data);
};Implement tracking that respects user privacy and complies with regulations:
// Privacy-compliant tracking system
class PrivacyTracker {
constructor() {
this.hasConsent = false;
this.consentTypes = new Set();
this.queuedEvents = [];
this.anonymousMode = true;
}
// Set user consent status
setConsent(consentGiven, consentTypes = []) {
this.hasConsent = consentGiven;
this.consentTypes = new Set(consentTypes);
this.anonymousMode = !consentGiven;
if (consentGiven) {
// Process queued events
this.processQueuedEvents();
} else {
// Clear any stored data
this.clearStoredData();
}
}
// Track event with privacy checks
track(eventType, data) {
// Always allow essential tracking
if (this.isEssentialEvent(eventType)) {
return this.processEvent(eventType, this.sanitizeData(data));
}
// Check consent for non-essential tracking
if (!this.hasConsent) {
// Queue for later or discard
this.queuedEvents.push({ eventType, data, timestamp: Date.now() });
return;
}
// Check specific consent types
if (!this.hasConsentForEvent(eventType)) {
return; // Skip tracking
}
this.processEvent(eventType, data);
}
isEssentialEvent(eventType) {
// Define which events are essential for functionality
const essentialEvents = ['license_error', 'data_change'];
return essentialEvents.includes(eventType);
}
hasConsentForEvent(eventType) {
const eventConsentMap = {
'odds_click': 'analytics',
'social_share': 'marketing',
'data_change': 'functional'
};
const requiredConsent = eventConsentMap[eventType];
return !requiredConsent || this.consentTypes.has(requiredConsent);
}
sanitizeData(data) {
if (this.anonymousMode) {
// Remove or hash personally identifiable information
const sanitized = { ...data };
delete sanitized.user;
delete sanitized.userId;
delete sanitized.email;
delete sanitized.ipAddress;
return sanitized;
}
return data;
}
processQueuedEvents() {
const events = [...this.queuedEvents];
this.queuedEvents = [];
events.forEach(event => {
this.track(event.eventType, event.data);
});
}
clearStoredData() {
// Clear any tracking data stored locally
localStorage.removeItem('tracking_data');
sessionStorage.removeItem('session_events');
this.queuedEvents = [];
}
processEvent(eventType, data) {
// Your actual tracking implementation
console.log('Tracking event:', eventType, data);
}
}
// Usage with consent management
const privacyTracker = new PrivacyTracker();
// Check for existing consent (from cookie banner, etc.)
const savedConsent = localStorage.getItem('user_consent');
if (savedConsent) {
const consent = JSON.parse(savedConsent);
privacyTracker.setConsent(consent.given, consent.types);
}
// Update consent when user makes choice
function updateConsent(consentGiven, consentTypes) {
privacyTracker.setConsent(consentGiven, consentTypes);
localStorage.setItem('user_consent', JSON.stringify({
given: consentGiven,
types: consentTypes,
timestamp: Date.now()
}));
}
// Privacy-compliant tracking callback
const trackingCallback = (eventType, data) => {
privacyTracker.track(eventType, data);
};Events not firing
Possible causes:
onTrack function signatureSolutions:
// Debug tracking implementation
function debugTrackingCallback(eventType, data) {
console.log('🔍 Tracking Debug:', {
eventType,
data,
timestamp: new Date().toISOString()
});
// Validate parameters
if (typeof eventType !== 'string') {
console.error('❌ eventType should be string, got:', typeof eventType);
return;
}
if (typeof data !== 'object') {
console.error('❌ data should be object, got:', typeof data);
return;
}
// Call your actual tracking
try {
yourTrackingFunction(eventType, data);
console.log('✅ Event tracked successfully');
} catch (error) {
console.error('❌ Tracking failed:', error);
}
}Missing event data
Possible causes:
Solutions:
// Validate and enrich event data
function validateEventData(eventType, data) {
const requiredFields = {
'data_change': ['component', 'version'],
'odds_click': ['visible'],
'social_share': ['source', 'target'],
'license_error': ['error', 'component']
};
const required = requiredFields[eventType] || [];
const missing = required.filter(field => !data.hasOwnProperty(field));
if (missing.length > 0) {
console.warn(`Missing required fields for ${eventType}:`, missing);
}
// Enrich with fallback data
return {
...data,
_timestamp: Date.now(),
_url: window.location.href,
_eventType: eventType
};
}Performance impact
Possible causes:
Solutions:
// Performance monitoring
class PerformanceTracker {
constructor() {
this.metrics = [];
}
measureTracking(eventType, trackingFn) {
const start = performance.now();
try {
trackingFn();
const duration = performance.now() - start;
this.metrics.push({
eventType,
duration,
timestamp: Date.now()
});
if (duration > 50) { // Warn if tracking takes >50ms
console.warn(`Slow tracking for ${eventType}: ${duration.toFixed(2)}ms`);
}
} catch (error) {
console.error('Tracking performance measurement failed:', error);
}
}
getAverageTime(eventType) {
const events = this.metrics.filter(m => m.eventType === eventType);
if (events.length === 0) return 0;
const total = events.reduce((sum, event) => sum + event.duration, 0);
return total / events.length;
}
}// Development-only event inspector
class TrackingInspector {
constructor() {
this.events = [];
this.isEnabled = window.location.search.includes('debug=tracking');
if (this.isEnabled) {
this.createInspectorUI();
}
}
inspect(eventType, data) {
if (!this.isEnabled) return;
const event = {
type: eventType,
data: data,
timestamp: new Date().toISOString(),
id: Math.random().toString(36).substr(2, 9)
};
this.events.push(event);
this.updateUI(event);
}
createInspectorUI() {
const inspector = document.createElement('div');
inspector.id = 'tracking-inspector';
inspector.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
width: 300px;
max-height: 400px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
z-index: 10000;
overflow: hidden;
`;
inspector.innerHTML = `
<div style="background: #333; color: white; padding: 8px;">
🔍 Tracking Inspector
<button onclick="this.parentNode.parentNode.remove()" style="float: right;">×</button>
</div>
<div id="inspector-events" style="max-height: 350px; overflow-y: auto; padding: 8px;"></div>
`;
document.body.appendChild(inspector);
}
updateUI(event) {
const container = document.getElementById('inspector-events');
if (!container) return;
const eventEl = document.createElement('div');
eventEl.style.cssText = `
border-bottom: 1px solid #ddd;
padding: 4px 0;
margin-bottom: 4px;
`;
eventEl.innerHTML = `
<strong style="color: #007acc;">${event.type}</strong>
<span style="color: #666; font-size: 10px;">${event.timestamp}</span>
<details>
<summary>Data</summary>
<pre style="font-size: 10px; max-height: 100px; overflow: auto;">${JSON.stringify(event.data, null, 2)}</pre>
</details>
`;
container.insertBefore(eventEl, container.firstChild);
// Keep only last 20 events
while (container.children.length > 20) {
container.removeChild(container.lastChild);
}
}
exportEvents() {
const dataStr = JSON.stringify(this.events, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `tracking-events-${Date.now()}.json`;
link.click();
URL.revokeObjectURL(url);
}
}
// Usage: Add ?debug=tracking to URL to enable
const inspector = new TrackingInspector();
const debugTrackingCallback = (eventType, data) => {
inspector.inspect(eventType, data);
// Your normal tracking
yourTrackingFunction(eventType, data);
};Performance & Privacy:
Reliability:
Compliance: