Event Queue & Deduplication Guide
- Overview - Event Queue System - Consent-Aware Queuing - Queue Lifecycle
Migrated from WordPress knowledge base. Runtime API names (e.g.
adtPush) are unchanged.
Table of Contents
- Overview
- Event Queue System
- Consent-Aware Queuing
- Queue Lifecycle
- Queue API
- Deduplication System
- How Deduplication Works
- Deduplication Keys
- TTL Configuration
- Deduplication API
- Event Dispatcher
- Push Functions
- Implementation Examples
- Best Practices
- Troubleshooting
Overview
The Event Queue & Deduplication system provides two critical functions:
- Event Queue (Consent-Aware) - Holds events until consent is granted, then flushes them
- Deduplication System - Prevents duplicate events from firing in rapid succession
Why These Systems Matter
Traditional dataLayer problems:
- ❌ Events fire before consent is given (GDPR violation)
- ❌ Duplicate events from rapid interactions (double-clicks, page reloads)
- ❌ Event flooding from scroll/resize handlers
- ❌ Race conditions between modules
DataLayer Tracker solutions:
- ✅ Consent-aware queue holds events until permission granted
- ✅ Time-based deduplication prevents duplicates
- ✅ Configurable TTL per event type
- ✅ Memory-efficient cleanup
Event Queue System
File: adt-event-queue.js
Purpose: Hold events until consent granted, then release them
Consent-Aware Queuing
The event queue ensures GDPR/CCPA compliance by:
- Checking consent status before pushing events
- Queuing events if consent not yet granted
- Automatically flushing queue when consent received
- Bypassing queue if consent already exists
Queue Architecture
window.ADTEventQueue = {
queue: [], // Array of queued events
consentGranted: false, // Current consent status
initialized: false, // Initialization flag
init() { ... }, // Initialize queue
push(payload, dedupKey, dedupTime) { ... }, // Add event to queue or send
send(payload, dedupKey, dedupTime) { ... }, // Send event to dataLayer
flush() { ... } // Release all queued events
}
Queue Lifecycle
1. Initialization
// Automatically runs on page load
window.ADTEventQueue.init();
// What happens:
// - Check if consent already restored from cookies (100ms delay)
// - If consent exists AND queue has events → flush immediately
// - Register listener for consent changes
2. Event Push (No Consent)
// User interacts before giving consent
// Module tries to push event
window.ADTEventQueue.push({
event: 'scroll_depth',
percent: 25,
timestamp: '2025-01-09T14:30:00.000Z'
}, 'scroll_25', 5000);
// What happens:
// 1. Check consent: window.hasConsent('analytics') → false
// 2. Add to queue: queue.push({ payload, dedupKey, dedupTime })
// 3. Log: "[DataLayer Tracker EventQueue] Queued: scroll_depth"
// 4. Event NOT sent to dataLayer yet
Queue State:
window.ADTEventQueue.queue = [
{
payload: { event: 'scroll_depth', percent: 25, ... },
dedupKey: 'scroll_25',
dedupTime: 5000
}
];
3. Consent Granted
// User accepts consent
// Consent manager fires event
window.dispatchEvent(new Event('adt_consent_granted'));
// What happens:
// 1. Event listener catches 'adt_consent_granted'
// 2. Update flag: consentGranted = true
// 3. Call flush()
// 4. All queued events sent to dataLayer
// 5. Queue cleared: queue = []
Console Output:
[DataLayer Tracker EventQueue] Flushing 3 queued events
[DataLayer Tracker-lite] PUSHED: scroll_depth {...}
[DataLayer Tracker-lite] PUSHED: time_on_page {...}
[DataLayer Tracker-lite] PUSHED: form_start {...}
4. Event Push (With Consent)
// User already gave consent
// Module tries to push event
window.ADTEventQueue.push({
event: 'scroll_depth',
percent: 50,
timestamp: '2025-01-09T14:32:00.000Z'
}, 'scroll_50', 5000);
// What happens:
// 1. Check consent: window.hasConsent('analytics') → true
// 2. Update flag: consentGranted = true (if needed)
// 3. Send immediately: this.send(payload, dedupKey, dedupTime)
// 4. Event sent directly to dataLayer (NO queueing)
Push Method Logic
push(payload, dedupKey, dedupTime) {
// ALWAYS check LIVE consent status (don't rely on cached flag)
const hasConsentNow = window.hasConsent?.('analytics') || false;
if (hasConsentNow) {
// Consent granted - send immediately, DON'T queue
this.consentGranted = true; // Update cached flag
this.send(payload, dedupKey, dedupTime);
} else {
// No consent - queue for later
this.queue.push({ payload, dedupKey, dedupTime });
console.log(`[DataLayer Tracker EventQueue] Queued: ${payload.event}`);
}
}
Key Design Decision: Always check LIVE consent status via window.hasConsent(), not the cached consentGranted flag. This prevents race conditions where consent changes between checks.
Send Method
send(payload, dedupKey, dedupTime) {
if (typeof window.adtPushDeduped === 'function' && dedupKey) {
// Use deduped push if key provided
window.adtPushDeduped(payload, dedupKey, dedupTime);
} else {
// Direct push to dataLayer
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(payload);
}
}
Flush Method
flush() {
if (this.queue.length > 0) {
console.log(`[DataLayer Tracker EventQueue] Flushing ${this.queue.length} queued events`);
// Copy queue and clear original
const toFlush = [...this.queue];
this.queue = [];
// Send each event
toFlush.forEach(item => {
this.send(item.payload, item.dedupKey, item.dedupTime);
});
}
}
Why Copy Queue? If send() triggers additional events during flush, we don't want them in the flush batch. The copy prevents infinite loops.
Queue API
Check Queue Status
// How many events in queue?
console.log('Queued events:', window.ADTEventQueue.queue.length);
// Consent status
console.log('Consent granted:', window.ADTEventQueue.consentGranted);
// View queued events
console.log('Queue:', window.ADTEventQueue.queue);
Manually Trigger Flush
// Force flush (for testing or special cases)
window.ADTEventQueue.flush();
Clear Queue Without Sending
// Emergency clear (not recommended in production)
window.ADTEventQueue.queue = [];
Consent Integration
The queue listens for the custom event adt_consent_granted:
// How consent systems trigger flush
window.addEventListener('adt_consent_granted', () => {
this.consentGranted = true;
this.flush();
});
From Consent Manager:
// When user accepts consent
function grantConsent(category) {
// Update consent state
window.ADTConsent[category] = true;
// Fire event to trigger queue flush
window.dispatchEvent(new Event('adt_consent_granted'));
}
Early Consent Check
The queue checks for pre-existing consent 100ms after initialization:
init() {
// ...
// Check if consent was already restored from storage
setTimeout(() => {
this.consentGranted = window.hasConsent?.('analytics') || false;
if (this.consentGranted && this.queue.length > 0) {
console.log(`[DataLayer Tracker EventQueue] Consent already granted, flushing ${this.queue.length} queued events`);
this.flush();
}
}, 100); // Small delay to let consent manager restore from cookies
}
Why 100ms delay? Consent managers often restore consent from cookies asynchronously. The delay ensures window.hasConsent() is available before checking.
Deduplication System
File: adt-utils-lite.js
Function: window.adtPushDeduped()
Purpose: Prevent duplicate events within a time window
How Deduplication Works
Basic Concept
// User double-clicks a button
Button clicked → Event pushed (key: btn_123)
Button clicked (50ms later) → BLOCKED (duplicate within 1500ms)
Button clicked (2s later) → Event pushed (TTL expired, fresh event)
Deduplication Map
// In-memory storage
window._adtDedupe = {
'scroll_25': 1760034620287, // Key → Timestamp
'scroll_50': 1760034630450,
'btn_cta_click': 1760034635123,
// ... etc
};
// How it works:
// 1. Event fired with key 'scroll_25'
// 2. Check: Does 'scroll_25' exist in map?
// 3. If yes: Is (now - stored_time) < TTL?
// - If yes: BLOCK (duplicate)
// - If no: ALLOW (TTL expired)
// 4. If no: ALLOW (first occurrence)
// 5. Update map: map['scroll_25'] = Date.now()
Deduplication Algorithm
function adtPushDeduped(payload, key, ttlMs = 1500) {
// Step 1: No key provided? Push directly without deduplication
if (!key) {
window._adtDispatchEvent(payload);
return;
}
// Step 2: Get current time and deduplication map
const now = Date.now();
const map = (window._adtDedupe = window._adtDedupe || {});
const hit = map[key];
// Step 3: Check if event is duplicate
if (hit && now - hit < ttlMs) {
// BLOCKED: Event fired within TTL window
console.log('[DataLayer Tracker-lite] BLOCKED (deduped):', payload.event, 'within', ttlMs, 'ms');
return;
}
// Step 4: ALLOWED - Update timestamp and push event
map[key] = now;
window._adtDispatchEvent(payload);
console.log('[DataLayer Tracker-lite] PUSHED:', payload.event, payload);
}
Deduplication Keys
Keys should be unique per event instance, not per event type.
✅ Good Keys (Unique Identifiers)
// Scroll depth with milestone
key: 'scroll_25' // Unique per milestone
// Form with form ID
key: 'form_start_contact-form' // Unique per form
// Button with element ID + action
key: 'click_btn-cta-primary' // Unique per button
// Video with video ID + milestone
key: 'video_progress_abc123_50' // Unique per video per milestone
// Ecommerce with product IDs
key: 'add_to_cart_12345,67890_99.99' // Unique per cart action
❌ Bad Keys (Too Generic)
// Too broad - will block ALL scroll events
key: 'scroll'
// Too broad - will block ALL form events
key: 'form'
// No variation - will block rapid legitimate clicks
key: 'click'
Key Generation Examples
From Scroll Tracking:
function trackScrollDepth(percent) {
const payload = {
event: 'scroll_depth',
percent: percent,
timestamp: new Date().toISOString()
};
// Key includes milestone to allow 25%, 50%, 75%, 100%
const key = `scroll_${percent}`;
const ttl = 5000; // 5 seconds
window.adtPushDeduped(payload, key, ttl);
}
From Click Tracking:
function trackClick(element) {
const elementUrl = element.href || '';
const elementText = element.textContent?.trim() || '';
const elementClass = element.className || '';
const payload = {
event: 'click',
url: elementUrl,
text: elementText,
timestamp: new Date().toISOString()
};
// Key combines multiple unique identifiers
const key = `click_${elementUrl}_${elementText}_${elementClass}`;
const ttl = 1500; // 1.5 seconds
window.adtPushDeduped(payload, key, ttl);
}
From Ecommerce:
function trackAddToCart(items, value) {
// Generate key from item IDs and value
const itemIds = items.map(item => item.item_id || item.id).join(',');
const key = `add_to_cart_${itemIds}_${value}`;
const payload = {
event: 'add_to_cart',
ecommerce: { items, value }
};
const ttl = 2000; // 2 seconds
window.adtPushDeduped(payload, key, ttl);
}
TTL Configuration
TTL (Time To Live) = How long to block duplicate events (in milliseconds)
Default TTLs by Event Type
// From adt-ecommerce-lite.js
const TTL_CONFIG = {
'add_to_cart': 2000, // 2 seconds
'remove_from_cart': 2000, // 2 seconds
'view_item': 5000, // 5 seconds
'begin_checkout': 3000, // 3 seconds
'purchase': 10000, // 10 seconds (critical, longer window)
'scroll_depth': 5000, // 5 seconds
'click': 1500, // 1.5 seconds (default)
'form_start': 3000, // 3 seconds
'session_ping': 5000 // 5 seconds
};
Choosing TTL Values
Short TTL (500-1500ms): For rapid user actions
- Clicks (prevent double-clicks)
- Hover events
- Mouse movements
Medium TTL (2000-5000ms): For standard interactions
- Scroll events
- Form interactions
- Video progress
- Add to cart
Long TTL (5000-10000ms): For critical actions
- Purchase events
- Form submissions
- Account changes
- Downloads
Factors to Consider:
- User Behavior:
- Can users legitimately trigger this twice quickly?
- Example: User can scroll to 25% and 50% within 1 second (use unique keys, not longer TTL)
- Event Cost:
- High-value events (purchases) need longer TTL
- Low-cost events (scrolls) can use shorter TTL
- Technical Factors:
- Page reload time
- SPA navigation speed
- Network latency
Deduplication in Different Modules
Scroll Tracking
// From adt-engagement-tracking.js
function fireScrollEvent(percent) {
// ... build payload ...
// Dedupe by milestone
const key = `scroll_${percent}`;
window.adtPushDeduped(payload, key, 5000);
}
// Result:
// Scroll to 26% → fires scroll_25
// Scroll to 27% → BLOCKED (within 5s)
// Scroll to 51% → fires scroll_50 (different key)
Click Tracking
// From adt-click-tracking.js
const dedupKey = `${url || 'no-url'}_${text || 'no-text'}_${element.className}`;
// Local deduplication check FIRST
if (isDuplicate(dedupKey)) {
return; // Stop before even calling push
}
// Then push to dataLayer without dedupe key
window.dataLayer.push(payload);
Note: Click tracking uses its own in-memory deduplication (isDuplicate()) instead of adtPushDeduped(). This is for performance—clicks are high-frequency and local dedup is faster.
Session Manager
// From adt-session-manager.js
pushEvent(payload) {
// Use deduped push if available
if (typeof window.adt_push_deduped === 'function') {
const key = `${payload.event}_${this.state.sessionId}`;
window.adt_push_deduped(payload, key, { throttleMs: 5000 });
} else {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(payload);
}
}
Note: Session events (ping, exit) are deduplicated by session ID to prevent cross-tab duplicates.
Memory Management
The deduplication map grows over time. While there's no automatic cleanup in the core function, individual modules handle their own cleanup:
// Example: Click tracking cleanup
if (recentClicks.size > 100) {
const cutoff = now - config.dedupWindow;
for (const [key, time] of recentClicks.entries()) {
if (time < cutoff) {
recentClicks.delete(key);
}
}
}
For window._adtDedupe: Map persists for session lifetime. Old entries naturally expire as TTL passes. For high-volume sites, consider periodic cleanup:
// Custom cleanup (run periodically if needed)
function cleanupDedupeMap() {
const now = Date.now();
const maxAge = 60000; // 1 minute
Object.keys(window._adtDedupe).forEach(key => {
if (now - window._adtDedupe[key] > maxAge) {
delete window._adtDedupe[key];
}
});
}
// Run every 5 minutes
setInterval(cleanupDedupeMap, 300000);
Event Dispatcher
Function: window._adtDispatchEvent()
Purpose: Centralized event processing and dataLayer push
Dispatcher Architecture
window._adtDispatchEvent = function(event) {
// Step 1: Process event through registered processors
let processedEvent = { ...event };
window._adtEventProcessors?.forEach(processor => {
try {
processedEvent = processor(processedEvent) || processedEvent;
} catch (e) {
console.error('[DataLayer Tracker] Processor error:', e);
}
});
// Step 2: Push to dataLayer (original implementation)
const result = window.dataLayer.__originalPush(processedEvent);
// Step 3: Notify listeners (for debug overlay, etc.)
window.dispatchEvent(new CustomEvent('adt_datalayer_push', {
detail: processedEvent
}));
// Step 4: Notify server-side modules (Meta CAPI, etc.)
window.dispatchEvent(new CustomEvent('adt_track_server', {
detail: processedEvent
}));
return result;
};
Event Processors
Modules can register processors to transform events before they reach dataLayer:
// Register a processor
window.adtRegisterProcessor('myProcessor', function(event) {
// Modify event
if (event.event === 'click') {
event.processed_by = 'myProcessor';
event.timestamp_processed = Date.now();
}
// Return modified event (or original)
return event;
});
Processor Chain:
// Multiple processors run in sequence
Original Event
→ Processor 1 (adds session_id)
→ Processor 2 (adds user_id)
→ Processor 3 (enriches ecommerce data)
→ Final Event → dataLayer
dataLayer.push Override
The dispatcher overrides the native dataLayer.push():
// Save original push
window.dataLayer.__originalPush = window.dataLayer.push;
// Replace with centralized dispatcher
window.dataLayer.push = function(...args) {
if (args[0] && typeof args[0] === 'object') {
return window._adtDispatchEvent(args[0]);
}
return window.dataLayer.__originalPush.apply(window.dataLayer, args);
};
Why? This ensures ALL dataLayer pushes (even from external scripts) go through DataLayer Tracker's processing pipeline:
- Event processors can enrich data
- Debug overlay can capture events
- Server-side modules can track events
- Consistent logging/debugging
Push Functions
Standard Push Functions
window.dataLayer.push()
Native GTM push, now routes through DataLayer Tracker dispatcher.
window.dataLayer.push({
event: 'custom_event',
param1: 'value1'
});
window.adtPush()
Direct push with debug logging.
window.adtPush({
event: 'custom_event',
param1: 'value1'
});
// Debug output:
// [DataLayer Tracker-lite] push { event: 'custom_event', ... }
window.adtPushDeduped()
Push with time-based deduplication.
window.adtPushDeduped({
event: 'scroll_depth',
percent: 25
}, 'scroll_25', 5000);
// Debug output:
// [DataLayer Tracker] adtPushDeduped called: scroll_depth key: scroll_25
// [DataLayer Tracker-lite] Dedup check: scroll_25 hit: undefined now: 1760034620287 diff: N/A
// [DataLayer Tracker-lite] PUSHED: scroll_depth {...}
window.ADTEventQueue.push()
Consent-aware push with optional deduplication.
window.ADTEventQueue.push({
event: 'scroll_depth',
percent: 25
}, 'scroll_25', 5000);
// If no consent:
// [DataLayer Tracker EventQueue] Queued: scroll_depth
// If consent:
// [DataLayer Tracker-lite] PUSHED: scroll_depth {...}
Function Comparison
Function
Deduplication
Consent Check
Use Case
dataLayer.push()
No
No
Standard GTM events
adtPush()
No
No
Simple DataLayer Tracker events
adtPushDeduped()
✅ Yes
No
High-frequency events
ADTEventQueue.push()
✅ Yes (optional)
✅ Yes
All DataLayer Tracker modules
Recommended Usage
For DataLayer Tracker Modules:
// ✅ Recommended: Use queue for consent awareness
window.ADTEventQueue.push(payload, dedupKey, ttl);
For Custom Events:
// ✅ Good: Simple custom events
window.dataLayer.push({ event: 'custom_event', ... });
// ✅ Better: High-frequency custom events
window.adtPushDeduped({ event: 'custom_event', ... }, 'unique_key', 1500);
// ✅ Best: Consent-aware custom events
if (window.hasConsent('analytics')) {
window.adtPushDeduped({ event: 'custom_event', ... }, 'unique_key', 1500);
}
Implementation Examples
Example 1: Module Using Queue + Deduplication
// Custom scroll tracker with full DataLayer Tracker integration
(function() {
'use strict';
const THRESHOLDS = [25, 50, 75, 100];
const firedDepths = new Set();
function checkScrollDepth() {
const scrollPercent = Math.round(
(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
);
THRESHOLDS.forEach(threshold => {
if (scrollPercent >= threshold && !firedDepths.has(threshold)) {
firedDepths.add(threshold);
const payload = {
event: 'scroll_depth',
percent: threshold,
page_height: document.documentElement.scrollHeight,
viewport_height: window.innerHeight,
timestamp: new Date().toISOString()
};
// Add session context if available
if (window.ADTSession && !window.ADTSession._isStub) {
payload.session_id = window.ADTSession.id();
payload.tab_id = window.ADTSession.tabId();
}
// Use queue for consent awareness + deduplication
const dedupKey = `scroll_${threshold}`;
const ttl = 5000; // 5 seconds
window.ADTEventQueue.push(payload, dedupKey, ttl);
}
});
}
// Throttled scroll handler
let scrollTimeout;
window.addEventListener('scroll', () => {
if (scrollTimeout) return;
scrollTimeout = setTimeout(() => {
checkScrollDepth();
scrollTimeout = null;
}, 100);
}, { passive: true });
})();
Example 2: Testing Queue Behavior
// Test script to verify queue + consent flow
(function() {
console.log('=== TESTING EVENT QUEUE ===');
// Step 1: Check initial state
console.log('Queue length:', window.ADTEventQueue.queue.length);
console.log('Consent granted:', window.ADTEventQueue.consentGranted);
console.log('Has consent:', window.hasConsent?.('analytics'));
// Step 2: Push events WITHOUT consent
console.log('\n--- Pushing events without consent ---');
window.ADTEventQueue.push({
event: 'test_event_1',
test: true
}, 'test_1', 1000);
window.ADTEventQueue.push({
event: 'test_event_2',
test: true
}, 'test_2', 1000);
window.ADTEventQueue.push({
event: 'test_event_3',
test: true
}, 'test_3', 1000);
console.log('Queue length after pushing:', window.ADTEventQueue.queue.length);
console.log('Expected: 3 events queued (if no consent)');
// Step 3: Grant consent
console.log('\n--- Granting consent ---');
window.dispatchEvent(new Event('adt_consent_granted'));
// Step 4: Check after flush
setTimeout(() => {
console.log('\nQueue length after consent:', window.ADTEventQueue.queue.length);
console.log('Expected: 0 (all events flushed)');
console.log('\ndataLayer events:');
window.dataLayer.filter(e => e.event?.includes('test_event')).forEach(e => {
console.log(' -', e.event);
});
}, 200);
})();
Expected Console Output:
=== TESTING EVENT QUEUE ===
Queue length: 0
Consent granted: false
Has consent: false
--- Pushing events without consent ---
[DataLayer Tracker EventQueue] Queued: test_event_1
[DataLayer Tracker EventQueue] Queued: test_event_2
[DataLayer Tracker EventQueue] Queued: test_event_3
Queue length after pushing: 3
Expected: 3 events queued (if no consent)
--- Granting consent ---
[DataLayer Tracker EventQueue] Flushing 3 queued events
[DataLayer Tracker-lite] PUSHED: test_event_1 {...}
[DataLayer Tracker-lite] PUSHED: test_event_2 {...}
[DataLayer Tracker-lite] PUSHED: test_event_3 {...}
Queue length after consent: 0
Expected: 0 (all events flushed)
dataLayer events:
- test_event_1
- test_event_2
- test_event_3
Example 3: Testing Deduplication
// Test deduplication with rapid fires
(function() {
console.log('=== TESTING DEDUPLICATION ===');
const testEvent = {
event: 'test_dedup',
value: Math.random()
};
// Fire 5 times rapidly (50ms apart)
console.log('Firing same event 5 times with 50ms delay...');
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(`\nAttempt ${i + 1}:`);
window.adtPushDeduped(testEvent, 'test_dedup_key', 1000);
}, i * 50);
}
// Fire again after TTL expires
setTimeout(() => {
console.log('\n\nAfter 1500ms (TTL expired):');
window.adtPushDeduped(testEvent, 'test_dedup_key', 1000);
}, 1500);
})();
Expected Console Output:
=== TESTING DEDUPLICATION ===
Firing same event 5 times with 50ms delay...
Attempt 1:
[DataLayer Tracker] adtPushDeduped called: test_dedup key: test_dedup_key
[DataLayer Tracker-lite] Dedup check: test_dedup_key hit: undefined now: 1760034620287 diff: N/A
[DataLayer Tracker-lite] PUSHED: test_dedup {...}
Attempt 2:
[DataLayer Tracker] adtPushDeduped called: test_dedup key: test_dedup_key
[DataLayer Tracker-lite] Dedup check: test_dedup_key hit: 1760034620287 now: 1760034620337 diff: 50
[DataLayer Tracker-lite] BLOCKED (deduped): test_dedup within 1000 ms
Attempt 3:
[DataLayer Tracker-lite] BLOCKED (deduped): test_dedup within 1000 ms
Attempt 4:
[DataLayer Tracker-lite] BLOCKED (deduped): test_dedup within 1000 ms
Attempt 5:
[DataLayer Tracker-lite] BLOCKED (deduped): test_dedup within 1000 ms
After 1500ms (TTL expired):
[DataLayer Tracker] adtPushDeduped called: test_dedup key: test_dedup_key
[DataLayer Tracker-lite] Dedup check: test_dedup_key hit: 1760034620287 now: 1760034621787 diff: 1500
[DataLayer Tracker-lite] PUSHED: test_dedup {...}
Example 4: Custom Processor
// Register processor to add custom data to all events
window.adtRegisterProcessor('customEnrichment', function(event) {
// Add custom timestamp
event.custom_timestamp = Date.now();
// Add environment info
event.environment = window.location.hostname.includes('localhost') ? 'dev' : 'prod';
// Add device type
event.device_type = /mobile/i.test(navigator.userAgent) ? 'mobile' : 'desktop';
// Return enriched event
return event;
});
// Now all events will have these fields
window.dataLayer.push({ event: 'test' });
// Results in:
// {
// event: 'test',
// custom_timestamp: 1760034620287,
// environment: 'prod',
// device_type: 'desktop'
// }
Best Practices
1. Always Use Queue For Module Events
// ✅ Good: Consent-aware
window.ADTEventQueue.push(payload, dedupKey, ttl);
// ❌ Bad: Ignores consent
window.dataLayer.push(payload);
Why? Queue ensures GDPR/CCPA compliance automatically.
2. Use Unique Deduplication Keys
// ✅ Good: Unique per instance
key: `scroll_${percent}`
key: `form_start_${formId}`
key: `click_${url}_${text}`
// ❌ Bad: Too generic
key: 'scroll'
key: 'form'
key: 'click'
Why? Generic keys block legitimate events.
3. Choose Appropriate TTL
// ✅ Good: Matched to event frequency
'click': 1500, // Fast user action
'scroll_depth': 5000, // Medium frequency
'purchase': 10000 // Critical, rare event
// ❌ Bad: One-size-fits-all
'click': 10000, // Too long, blocks fast clicks
'purchase': 500 // Too short, allows duplicate purchases
4. Handle Consent Changes
// ✅ Good: React to consent changes
window.addEventListener('adt_consent_granted', () => {
// Resume tracking
startTrackingModule();
});
window.addEventListener('adt_consent_revoked', () => {
// Stop tracking
stopTrackingModule();
// Clear any stored data
clearTrackedData();
});
5. Test Queue Behavior
// ✅ Good: Test both scenarios
// 1. Events queue without consent
// 2. Events flush when consent granted
// 3. Events bypass queue with existing consent
// Test script
function testQueueFlow() {
// Clear consent
localStorage.removeItem('adt_consent');
// Reload page
location.reload();
// Push events (should queue)
// Grant consent (should flush)
// Push more events (should bypass queue)
}
6. Monitor Queue Size
// ✅ Good: Alert if queue grows too large
setInterval(() => {
const queueSize = window.ADTEventQueue.queue.length;
if (queueSize > 50) {
console.warn('[DataLayer Tracker] Queue unusually large:', queueSize, 'events');
// May indicate consent not granted or issue with flush
}
}, 5000);
7. Clean Up Deduplication Map
// ✅ Good: Periodic cleanup for high-volume sites
function cleanupDedupeMap() {
const now = Date.now();
const maxAge = 60000; // 1 minute
let cleaned = 0;
Object.keys(window._adtDedupe).forEach(key => {
if (now - window._adtDedupe[key] > maxAge) {
delete window._adtDedupe[key];
cleaned++;
}
});
if (cleaned > 0) {
console.log('[DataLayer Tracker] Cleaned', cleaned, 'expired dedup entries');
}
}
// Run every 5 minutes
setInterval(cleanupDedupeMap, 300000);
8. Use Event Processors Wisely
// ✅ Good: Light processing
window.adtRegisterProcessor('sessionContext', function(event) {
if (window.ADTSession && !event.session_id) {
event.session_id = window.ADTSession.id();
}
return event;
});
// ❌ Bad: Heavy processing (slows every event)
window.adtRegisterProcessor('heavyProcessing', function(event) {
// Don't do expensive API calls or DOM manipulation here
fetch('/api/enrich?event=' + event.event); // ❌ Bad
return event;
});
Troubleshooting
Events Not Firing (Stuck in Queue)
Problem: Events queued but never released
Check:
- Consent status:
console.log('Has consent:', window.hasConsent?.('analytics'));
console.log('Queue consent flag:', window.ADTEventQueue.consentGranted);
- Queue contents:
console.log('Queued events:', window.ADTEventQueue.queue.length);
console.log('Queue:', window.ADTEventQueue.queue);
- Consent event:
// Check if consent event fires
window.addEventListener('adt_consent_granted', () => {
console.log('✅ Consent event received');
});
// Grant consent to test
window.dispatchEvent(new Event('adt_consent_granted'));
Solution:
// Manual flush if needed
window.ADTEventQueue.flush();
// Or grant consent programmatically
if (window.ADTConsent) {
window.ADTConsent.analytics = true;
}
window.dispatchEvent(new Event('adt_consent_granted'));
Events Firing Multiple Times
Problem: Same event appears multiple times in dataLayer
Causes:
- No deduplication key:
// ❌ Problem
window.ADTEventQueue.push(payload); // No dedupKey
// ✅ Solution
window.ADTEventQueue.push(payload, 'unique_key', 1500);
- Non-unique key:
// ❌ Problem: Key is too generic
key: 'scroll'
// ✅ Solution: Make key specific
key: `scroll_${percent}`
- TTL too short:
// ❌ Problem: TTL expires before duplicate fires
ttl: 100 // Too short
// ✅ Solution: Increase TTL
ttl: 1500
Diagnostic:
// Check deduplication map
console.log('Dedupe map:', window._adtDedupe);
// Monitor event firing
window.addEventListener('adt_datalayer_push', (e) => {
console.log('Event pushed:', e.detail.event);
});
Queue Flushes Too Early
Problem: Events fire before user actually gives consent
Cause: False positive consent check
Check:
// Is consent function working correctly?
console.log('hasConsent function:', typeof window.hasConsent);
console.log('hasConsent result:', window.hasConsent?.('analytics'));
// Check consent state
console.log('Stored consent:', localStorage.getItem('adt_consent'));
Solution:
// Ensure consent manager is properly integrated
// Verify consent is only granted when user actually accepts
Deduplication Too Aggressive
Problem: Legitimate events being blocked
Symptoms:
User scrolls to 25% → fires ✅
User scrolls to 50% → BLOCKED ❌ (should fire)
Cause: Key too generic or TTL too long
Fix:
// ❌ Problem: Same key for all scroll events
key: 'scroll_depth'
// ✅ Solution: Unique key per milestone
key: `scroll_depth_${percent}`
Diagnostic:
// Enable debug mode
window.ADTData.debug_mode = '1';
// Check what's being blocked
// Look for: [DataLayer Tracker-lite] BLOCKED (deduped)
Memory Leak from Deduplication Map
Problem: window._adtDedupe grows too large
Check:
// How many entries?
console.log('Dedupe entries:', Object.keys(window._adtDedupe).length);
// View oldest entries
Object.entries(window._adtDedupe)
.sort((a, b) => a[1] - b[1])
.slice(0, 10)
.forEach(([key, time]) => {
const age = Date.now() - time;
console.log(key, '→', Math.round(age / 1000), 'seconds old');
});
Solution:
// Add periodic cleanup
function cleanupDedupeMap() {
const now = Date.now();
const maxAge = 300000; // 5 minutes
Object.keys(window._adtDedupe).forEach(key => {
if (now - window._adtDedupe[key] > maxAge) {
delete window._adtDedupe[key];
}
});
}
setInterval(cleanupDedupeMap, 60000); // Every minute
Debug Overlay Not Showing Events
Problem: Events fire but don't appear in overlay
Cause: Events dispatched before overlay hooks in
Check:
// Is overlay listening?
console.log('Overlay hooked:', window.ADTDebugOverlay?._dataLayerHooked);
// Are events dispatching custom event?
window.addEventListener('adt_datalayer_push', (e) => {
console.log('Overlay event:', e.detail.event);
});
Solution:
// Overlay automatically hooks via adt_datalayer_push event
// Fired by _adtDispatchEvent
// If not seeing events, check if overlay is enabled and loaded
Summary
The Event Queue & Deduplication system provides:
✅ Consent-Aware Queuing - Holds events until permission granted
✅ Automatic Flush - Releases queued events when consent received
✅ Time-Based Deduplication - Prevents duplicate events
✅ Configurable TTL - Different windows per event type
✅ Centralized Dispatcher - Single processing pipeline
✅ Event Processors - Extensible enrichment system
✅ Debug Support - Comprehensive logging
✅ Memory Efficient - Cleanup strategies available
Key Takeaway: The queue ensures GDPR compliance while deduplication prevents event flooding. Together, they provide clean, reliable dataLayer pushes that respect user consent and prevent duplicates.
Quick Reference
Check Queue Status
window.ADTEventQueue.queue.length // Number of queued events
window.ADTEventQueue.consentGranted // Consent flag
window.ADTEventQueue.queue // View queue contents
Push Functions
// Direct push
window.dataLayer.push(payload);
// Deduped push
window.adtPushDeduped(payload, key, ttl);
// Consent-aware + deduped
window.ADTEventQueue.push(payload, key, ttl);
Manual Operations
// Flush queue
window.ADTEventQueue.flush();
// Grant consent
window.dispatchEvent(new Event('adt_consent_granted'));
// Check dedup map
console.log(window._adtDedupe);
Debug
window.ADTData.debug_mode = '1';
// Then reload page to see all debug logs
This documentation covers the complete Event Queue & Deduplication system