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:

  1. Event Queue (Consent-Aware) - Holds events until consent is granted, then flushes them
  2. 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:

  1. Checking consent status before pushing events
  2. Queuing events if consent not yet granted
  3. Automatically flushing queue when consent received
  4. 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:

  1. 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)
  2. Event Cost:
    • High-value events (purchases) need longer TTL
    • Low-cost events (scrolls) can use shorter TTL
  3. 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:

  1. Consent status:
console.log('Has consent:', window.hasConsent?.('analytics'));
console.log('Queue consent flag:', window.ADTEventQueue.consentGranted);
  1. Queue contents:
console.log('Queued events:', window.ADTEventQueue.queue.length);
console.log('Queue:', window.ADTEventQueue.queue);
  1. 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:

  1. No deduplication key:
// ❌ Problem
window.ADTEventQueue.push(payload); // No dedupKey

// ✅ Solution
window.ADTEventQueue.push(payload, 'unique_key', 1500);
  1. Non-unique key:
// ❌ Problem: Key is too generic
key: 'scroll'

// ✅ Solution: Make key specific
key: `scroll_${percent}`
  1. 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