Version: 1.2.1
Last Updated: 2025
Table of Contents
- What Are Custom Events?
- Why Use Custom Events?
- Implementation Methods
- Event Structure & Best Practices
- The Two-Push Pattern
- Working with Session Data
- Consent Integration
- Advanced Patterns
- Testing Custom Events
- Common Use Cases
- Troubleshooting
What Are Custom Events?
Custom events are user-defined tracking events that extend ADT’s 40+ built-in events. They allow you to track specific business actions, unique interactions, or proprietary features that aren’t covered by the standard event set.
Built-in Events vs Custom Events:
Built-in Events (Automatic):
- form_submit
- video_progress
- scroll_depth
- add_to_cart
- session_engagement_summary
Custom Events (You Define):
- demo_requested
- calculator_used
- tier_selected
- comparison_tool_opened
- custom_cta_clicked
Why Use Custom Events?
Business-Specific Tracking Track interactions unique to your business model, industry, or product features that no standard event covers.
Micro-Conversions Monitor intermediate conversion steps that predict final conversion but aren’t standard form submissions or purchases.
Feature Adoption Measure which custom features users engage with most frequently.
A/B Testing Track variant exposure and interaction for custom experiments.
Custom Workflows Monitor multi-step processes unique to your application.
Implementation Methods
Method 1: Direct DataLayer Push (Standard)
The simplest approach for firing custom events:
// Basic custom event
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'demo_requested',
demo_type: 'product_walkthrough',
timestamp: Date.now()
});
How It Works:
- Your code pushes the event object to the dataLayer
- ADT intercepts it via the centralized dispatcher
- Consent is checked automatically
- Event is enriched with session data (if available)
- Event is pushed to GTM for processing
Method 2: Using adtPush (Recommended)
ADT provides window.adtPush() which adds debug logging:
// Using ADT's helper function
window.adtPush({
event: 'calculator_completed',
calculation_type: 'roi',
result_value: 12500,
input_count: 5
});
Benefits:
- Automatic debug logging when debug mode is enabled
- Consistent with ADT patterns
- Better visibility in browser console during development
Method 3: Deduped Push (For Rapid-Fire Events)
Use window.adtPushDeduped() for events that might fire multiple times in quick succession:
// Prevent duplicate events within 1500ms
window.adtPushDeduped(
{
event: 'filter_applied',
filter_type: 'price_range',
min_price: 100,
max_price: 500
},
'filter_price_range_100_500', // Unique key for deduplication
1500 // TTL in milliseconds (default: 1500ms)
);
When to Use:
- Slider interactions that fire continuously
- Real-time filtering that updates rapidly
- Scroll-triggered events with high frequency
- Any event that could fire multiple times per second
Method 4: jQuery Event Binding
For sites using jQuery, bind custom events to user interactions:
jQuery(document).ready(function($) {
// Track calculator button click
$('.roi-calculator-submit').on('click', function(e) {
const formData = $(this).closest('form').serializeArray();
window.adtPush({
event: 'calculator_submitted',
calculator_type: 'roi',
field_count: formData.length,
session_id: window.ADTSession?.id() || null
});
});
// Track tier selection
$('.pricing-tier').on('click', function(e) {
const tierName = $(this).data('tier');
const tierPrice = $(this).data('price');
window.adtPush({
event: 'pricing_tier_selected',
tier_name: tierName,
tier_price: tierPrice,
session_id: window.ADTSession?.id() || null
});
});
});
Method 5: WordPress Action Hooks (PHP)
Fire custom events from server-side WordPress actions:
<?php
/**
* Track custom post type submission
*/
add_action('save_post_job_application', function($post_id, $post, $update) {
// Only track new applications (not updates)
if ($update) return;
// Get application data
$applicant_email = get_post_meta($post_id, '_applicant_email', true);
$position = get_post_meta($post_id, '_position', true);
// Enqueue tracking script in footer
add_action('wp_footer', function() use ($position) {
?>
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'job_application_submitted',
position: '<?php echo esc_js($position); ?>',
application_method: 'online_form',
timestamp: Date.now(),
session_id: window.ADTSession?.id() || null
});
</script>
<?php
});
}, 10, 3);
Event Structure & Best Practices
Naming Conventions
Event Names:
- Use snake_case (lowercase with underscores)
- Be descriptive and specific
- Include action verb when possible
- Keep under 50 characters
Good Examples:
✅ demo_requested
✅ pricing_calculator_completed
✅ comparison_tool_opened
✅ tier_upgrade_clicked
✅ newsletter_signup_completed
Bad Examples:
❌ DemoRequested // camelCase
❌ demo // Too vague
❌ evt1 // Meaningless
❌ user-clicked-btn // Hyphens instead of underscores
❌ really_long_event_name_that_describes_everything_in_detail // Too long
Parameter Structure
Standard Parameters to Include:
window.adtPush({
// REQUIRED: Event name
event: 'custom_event_name',
// HIGHLY RECOMMENDED: Session tracking
session_id: window.ADTSession?.id() || null,
session_number: window.ADTSession?.number() || null,
// RECOMMENDED: Timestamp
timestamp: Date.now(),
// CUSTOM: Your business parameters
custom_param_1: 'value',
custom_param_2: 123,
custom_param_3: true
});
Parameter Types:
Use consistent data types for parameters:
{
event: 'product_configured',
// Strings for identifiers and categories
product_id: 'PROD-12345',
product_category: 'electronics',
// Numbers for quantities and amounts
quantity: 3,
price: 299.99,
discount_percent: 15,
// Booleans for flags
has_warranty: true,
is_in_stock: false,
// Arrays for multiple values
selected_features: ['wifi', 'bluetooth', 'nfc'],
// Objects for nested data
configuration: {
color: 'blue',
size: 'large',
material: 'aluminum'
}
}
Privacy & PII Protection
NEVER include personally identifiable information:
// ❌ BAD - Contains PII
window.adtPush({
event: 'form_completed',
email: 'user@example.com', // ❌ NO
phone: '555-1234', // ❌ NO
full_name: 'John Smith', // ❌ NO
address: '123 Main St' // ❌ NO
});
// ✅ GOOD - No PII
window.adtPush({
event: 'form_completed',
form_type: 'contact',
field_count: 6,
has_email: true, // ✅ Flag only
has_phone: true, // ✅ Flag only
zip_code: '12345', // ✅ OK if needed for targeting
session_id: window.ADTSession?.id() || null
});
The Two-Push Pattern
ADT uses a “two-push” pattern to ensure events are visible in GTM Preview Mode. Understanding this pattern is crucial for advanced implementations.
Why Two Pushes?
GTM Preview Mode requires events to have unique identifiers to display properly. The two-push pattern:
- First push: Sets up the event container
- Second push: Fires the actual event with data
How It Works Internally
When you call window.dataLayer.push(), ADT’s dispatcher:
// Your code:
window.dataLayer.push({
event: 'custom_event',
param1: 'value1'
});
// What ADT does internally:
// 1. Intercepts the push
// 2. Adds GTM unique event ID
// 3. Enriches with session data
// 4. Processes through consent check
// 5. Pushes to real dataLayer
// 6. Dispatches custom event for listeners
The actual implementation in adt-utils-lite.js:
window._adtDispatchEvent = function(eventData) {
// Check consent first
const hasConsent = window.hasConsent('analytics');
if (!hasConsent) {
// Block or queue the event
return;
}
// Process event (add unique ID, enrich data)
const processedEvent = window._adtProcessEvent(eventData);
// Push to dataLayer (FIRST PUSH)
const result = window.dataLayer.__originalPush(processedEvent);
// Notify listeners (SECOND PUSH - custom event)
window.dispatchEvent(new CustomEvent('adt_datalayer_push', {
detail: processedEvent
}));
return result;
};
You Don’t Need to Implement It
The two-push pattern is handled automatically by ADT. You only need to push once:
// You only need to do this:
window.dataLayer.push({
event: 'my_custom_event',
data: 'value'
});
// ADT handles the rest automatically
Working with Session Data
Accessing Session Information
If Session Manager is enabled (Premium feature), include session data in your custom events:
// Check if Session Manager is available
if (window.ADTSession) {
window.adtPush({
event: 'custom_action',
action_type: 'feature_used',
// Include session context
session_id: window.ADTSession.id(),
session_number: window.ADTSession.number(),
tab_id: window.ADTSession.tabId(),
// Custom parameters
feature_name: 'advanced_calculator'
});
}
Session Data Properties
Available session methods:
window.ADTSession.id() // Current session ID
window.ADTSession.number() // Session count for user
window.ADTSession.tabId() // Unique tab identifier
window.ADTSession.startTime() // Session start timestamp
window.ADTSession.pageCount() // Pages viewed in session
Safe Session Access
Always check if Session Manager exists before using it:
// Safe access pattern
const sessionData = {
session_id: window.ADTSession?.id() || null,
session_number: window.ADTSession?.number() || null,
tab_id: window.ADTSession?.tabId() || null
};
window.adtPush({
event: 'custom_event',
...sessionData,
custom_param: 'value'
});
Consent Integration
ADT automatically respects user consent preferences. All custom events are subject to consent checks.
How Consent Blocking Works
// You fire an event:
window.dataLayer.push({
event: 'custom_event'
});
// ADT checks consent automatically:
// 1. Is analytics consent granted?
// 2. If YES: Event fires immediately
// 3. If NO: Event is blocked or queued
Manual Consent Check
Check consent status before firing events:
// Check if consent is granted
if (window.hasConsent('analytics')) {
window.adtPush({
event: 'custom_feature_used',
feature_name: 'calculator'
});
} else {
console.log('Event blocked - no analytics consent');
}
Consent-Critical Events
For events that must fire regardless of consent (like consent acceptance itself):
// Consent events bypass consent checks
window.dataLayer.push({
event: 'consent_updated', // Special handling
consent_type: 'analytics',
consent_value: 'granted'
});
Event Queueing
ADT queues events when consent is pending:
// Events fired before consent is determined are queued
window.ADTEventQueue.push({
event: 'early_interaction',
interaction_type: 'scroll'
});
// When consent is granted, queued events flush automatically
window.addEventListener('adt_consent_granted', function() {
console.log('Consent granted - flushing queued events');
});
Advanced Patterns
Pattern 1: Multi-Step Process Tracking
Track user progress through a multi-step workflow:
// Initialize process tracking
const processId = 'process_' + Date.now();
// Step 1: Started
window.adtPush({
event: 'onboarding_started',
process_id: processId,
step_number: 1,
step_name: 'account_creation',
session_id: window.ADTSession?.id() || null
});
// Step 2: In progress
window.adtPush({
event: 'onboarding_step_completed',
process_id: processId,
step_number: 2,
step_name: 'profile_setup',
time_spent_seconds: 45,
session_id: window.ADTSession?.id() || null
});
// Step 3: Completed
window.adtPush({
event: 'onboarding_completed',
process_id: processId,
total_steps: 3,
total_time_seconds: 180,
completion_rate: 100,
session_id: window.ADTSession?.id() || null
});
Pattern 2: Feature Flag Tracking
Track which feature variants users see:
// When feature flag is evaluated
function trackFeatureFlag(flagName, variant, context) {
window.adtPush({
event: 'feature_flag_evaluated',
flag_name: flagName,
variant: variant,
context: context,
user_id_hash: window.ADTSession?.id() || null,
timestamp: Date.now()
});
}
// Usage
trackFeatureFlag('new_checkout_flow', 'variant_b', 'checkout_page');
Pattern 3: Error Tracking
Monitor custom errors and exceptions:
// Track application errors
function trackCustomError(errorType, errorMessage, context) {
window.adtPush({
event: 'application_error',
error_type: errorType,
error_category: categorizeError(errorType),
context: context,
page_path: window.location.pathname,
session_id: window.ADTSession?.id() || null,
timestamp: Date.now()
});
}
// Usage
try {
// Your code
processPayment();
} catch (error) {
trackCustomError('payment_failed', error.message, 'checkout');
}
Pattern 4: Performance Monitoring
Track custom performance metrics:
// Track custom timing
function trackCustomTiming(metricName, duration, context) {
window.adtPush({
event: 'custom_timing',
metric_name: metricName,
duration_ms: duration,
context: context,
page_path: window.location.pathname,
session_id: window.ADTSession?.id() || null
});
}
// Measure API call performance
const startTime = Date.now();
fetch('/api/data')
.then(response => {
const duration = Date.now() - startTime;
trackCustomTiming('api_call', duration, 'data_fetch');
});
Pattern 5: Progressive Disclosure
Track how users expand content:
// Track content expansion
$('.expand-button').on('click', function() {
const sectionName = $(this).data('section');
const expansionCount = $(this).data('expansions') || 0;
$(this).data('expansions', expansionCount + 1);
window.adtPush({
event: 'content_expanded',
section_name: sectionName,
expansion_count: expansionCount + 1,
content_type: $(this).data('content-type'),
session_id: window.ADTSession?.id() || null
});
});
Pattern 6: Dynamic Content Tracking
Track lazy-loaded or dynamic content:
// Track when dynamic content loads
const contentObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const contentId = entry.target.dataset.contentId;
const contentType = entry.target.dataset.contentType;
window.adtPush({
event: 'dynamic_content_viewed',
content_id: contentId,
content_type: contentType,
load_method: 'lazy',
viewport_position: Math.round(entry.intersectionRatio * 100),
session_id: window.ADTSession?.id() || null
});
// Stop observing after first view
contentObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
// Observe all dynamic content
document.querySelectorAll('[data-track-dynamic]').forEach(el => {
contentObserver.observe(el);
});
Testing Custom Events
Method 1: Browser Console
// Fire test event in console
window.dataLayer.push({
event: 'test_custom_event',
test_param: 'test_value',
timestamp: Date.now()
});
// Check if it was added
console.log(window.dataLayer);
// Filter for your event
window.dataLayer.filter(e => e.event === 'test_custom_event');
Method 2: ADT Debug Overlay (Premium)
Enable the Debug Overlay in ADT Settings:
- Go to ADT Settings → Debug
- Enable “Show Debug Overlay”
- Refresh page
- Click the ADT icon in bottom right
- Fire your custom event
- See it appear in real-time
Method 3: GTM Preview Mode
- Open Google Tag Manager
- Click “Preview”
- Enter your site URL
- Fire your custom event
- Check GTM Preview’s “Data Layer” tab
- Verify event appears with correct parameters
Method 4: Automated Testing
// Create test suite for custom events
function testCustomEvent(eventName, expectedParams) {
const before = window.dataLayer.length;
// Fire event
window.adtPush({
event: eventName,
...expectedParams
});
const after = window.dataLayer.length;
if (after > before) {
console.log(`✅ ${eventName} fired successfully`);
// Verify parameters
const firedEvent = window.dataLayer[window.dataLayer.length - 1];
console.log('Event data:', firedEvent);
return true;
} else {
console.error(`❌ ${eventName} failed to fire`);
return false;
}
}
// Run tests
testCustomEvent('demo_requested', { demo_type: 'product' });
testCustomEvent('calculator_used', { calc_type: 'roi' });
Method 5: Network Tab Verification
If sending events to GA4 or other platforms:
- Open DevTools → Network tab
- Filter for “google-analytics” or your endpoint
- Fire custom event
- Look for outgoing request
- Check payload contains your event data
Common Use Cases
Use Case 1: Lead Scoring Events
// Track lead score-worthy actions
function trackLeadAction(actionType, actionValue) {
window.adtPush({
event: 'lead_scoring_action',
action_type: actionType,
action_value: actionValue,
page_path: window.location.pathname,
session_id: window.ADTSession?.id() || null,
timestamp: Date.now()
});
}
// Usage
trackLeadAction('pricing_page_viewed', 10);
trackLeadAction('case_study_downloaded', 25);
trackLeadAction('demo_requested', 50);
Use Case 2: Product Configurator
// Track product configuration steps
const configuratorState = {
configId: 'config_' + Date.now(),
steps: []
};
function trackConfigStep(stepName, selections) {
configuratorState.steps.push({
step: stepName,
selections: selections,
timestamp: Date.now()
});
window.adtPush({
event: 'product_configured',
config_id: configuratorState.configId,
step_name: stepName,
step_number: configuratorState.steps.length,
selections: selections,
session_id: window.ADTSession?.id() || null
});
}
// When configuration is completed
function trackConfigComplete() {
window.adtPush({
event: 'configuration_completed',
config_id: configuratorState.configId,
total_steps: configuratorState.steps.length,
configuration_data: configuratorState.steps,
session_id: window.ADTSession?.id() || null
});
}
Use Case 3: Content Engagement Scoring
// Track content engagement signals
const engagementScore = {
score: 0,
signals: []
};
function addEngagementSignal(signalType, points) {
engagementScore.score += points;
engagementScore.signals.push(signalType);
window.adtPush({
event: 'engagement_signal',
signal_type: signalType,
points: points,
total_score: engagementScore.score,
session_id: window.ADTSession?.id() || null
});
// Fire milestone events
if (engagementScore.score >= 50 && !engagementScore.highEngagement) {
engagementScore.highEngagement = true;
window.adtPush({
event: 'high_engagement_reached',
final_score: engagementScore.score,
signals: engagementScore.signals,
session_id: window.ADTSession?.id() || null
});
}
}
// Track various engagement signals
addEngagementSignal('video_watched', 15);
addEngagementSignal('article_read', 10);
addEngagementSignal('comment_posted', 25);
Use Case 4: Custom CTA Tracking
// Track custom CTA performance
jQuery('.custom-cta').on('click', function(e) {
const ctaData = {
cta_text: $(this).text().trim(),
cta_type: $(this).data('cta-type'),
cta_location: $(this).data('location'),
cta_variant: $(this).data('variant'),
destination_url: $(this).attr('href')
};
window.adtPush({
event: 'custom_cta_clicked',
...ctaData,
page_path: window.location.pathname,
session_id: window.ADTSession?.id() || null,
timestamp: Date.now()
});
});
Use Case 5: Comparison Tool
// Track product comparisons
const comparisonState = {
products: [],
startTime: Date.now()
};
function addToComparison(productId, productName) {
comparisonState.products.push({ id: productId, name: productName });
window.adtPush({
event: 'product_added_to_comparison',
product_id: productId,
product_name: productName,
comparison_count: comparisonState.products.length,
session_id: window.ADTSession?.id() || null
});
}
function viewComparison() {
const duration = Date.now() - comparisonState.startTime;
window.adtPush({
event: 'comparison_viewed',
product_count: comparisonState.products.length,
product_ids: comparisonState.products.map(p => p.id),
time_to_compare_ms: duration,
session_id: window.ADTSession?.id() || null
});
}
Use Case 6: Quiz/Survey Tracking
// Track quiz/survey completion
const quizState = {
quizId: 'quiz_' + Date.now(),
answers: {},
startTime: Date.now()
};
function trackQuizAnswer(questionId, answer) {
quizState.answers[questionId] = answer;
window.adtPush({
event: 'quiz_question_answered',
quiz_id: quizState.quizId,
question_id: questionId,
answer: answer,
question_number: Object.keys(quizState.answers).length,
session_id: window.ADTSession?.id() || null
});
}
function trackQuizComplete(result) {
const duration = Date.now() - quizState.startTime;
window.adtPush({
event: 'quiz_completed',
quiz_id: quizState.quizId,
total_questions: Object.keys(quizState.answers).length,
quiz_result: result,
completion_time_ms: duration,
session_id: window.ADTSession?.id() || null
});
}
Troubleshooting
Events Not Appearing in DataLayer
Check 1: Verify DataLayer Exists
console.log(window.dataLayer);
// Should return: Array with events
Check 2: Verify ADT is Loaded
console.log(window.ADTData);
// Should return: Object with ADT configuration
Check 3: Check Console for Errors Look for JavaScript errors that might prevent events from firing.
Check 4: Verify Event Syntax
// ✅ Correct
window.dataLayer.push({
event: 'my_event',
param: 'value'
});
// ❌ Incorrect - missing event property
window.dataLayer.push({
param: 'value'
});
Events Blocked by Consent
Check Consent Status:
console.log('Has consent:', window.hasConsent('analytics'));
Solution 1: Grant Consent (Testing)
// Temporarily grant consent for testing
window.ADTConsent = { analytics: true };
Solution 2: Check CMP Integration Ensure your consent management platform is properly integrated with ADT.
Solution 3: Enable Fallback Tracking (Testing Only) In ADT Settings → Consent → Enable “Fallback Track Without CMP” for testing.
Events Not Appearing in GTM Preview
Check 1: Verify GTM Container is Loading
console.log(google_tag_manager);
// Should return: Object
Check 2: Verify Event Name Matches GTM Trigger Event names are case-sensitive and must match exactly.
Check 3: Check for GTM Errors In GTM Preview, check the Errors tab for issues.
Check 4: Verify Two-Push Pattern ADT handles this automatically, but verify events have gtm.uniqueEventId:
// Check last event in dataLayer
console.log(window.dataLayer[window.dataLayer.length - 1]);
// Should have gtm.uniqueEventId property
Events Firing Multiple Times
Solution 1: Use Deduplication
// Instead of:
window.dataLayer.push({ event: 'my_event' });
// Use:
window.adtPushDeduped(
{ event: 'my_event' },
'unique_key_for_this_event',
1500
);
Solution 2: Add Guard Clauses
let eventFired = false;
function fireOnce() {
if (eventFired) return;
eventFired = true;
window.adtPush({
event: 'one_time_event'
});
}
Solution 3: Unbind Event Listeners After First Fire
$('.one-time-button').one('click', function() {
window.adtPush({
event: 'button_clicked_once'
});
});
Parameters Not Showing in GA4
Check 1: Verify GTM Variable Setup In GTM, ensure you’ve created Data Layer Variables for your custom parameters.
Check 2: Check GA4 Custom Dimensions Custom parameters must be registered as custom dimensions in GA4.
Check 3: Verify Parameter Names GA4 has naming restrictions (no spaces, must start with letter, max 40 chars).
Check 4: Check Data Layer Structure
// ✅ Correct - flat structure
{
event: 'my_event',
param1: 'value1',
param2: 'value2'
}
// ⚠️ May need flattening in GTM
{
event: 'my_event',
eventData: {
param1: 'value1',
param2: 'value2'
}
}
Debug Mode Not Showing Events
Check 1: Verify Debug Mode is Enabled
console.log('Debug enabled:', window.adtIsDebugEnabled());
Check 2: Enable Debug in Settings Go to ADT Settings → Debug → Enable Debug Mode
Check 3: Check Debug Level
console.log('Debug level:', window.ADTData?.debug_level);
// Should be: 'verbose' or 'normal'
Check 4: Bypass Debug Filters Some events are filtered in normal mode. Try verbose mode:
window.ADTData.debug_level = 'verbose';
Additional Resources
Related Documentation:
- Complete Event Guide
- GTM Setup Guide
- Engagement Tracking Guide
- Session Management Guide
- Consent Management Guide
Support:
- Plugin Settings:
WordPress Admin → ADT Settings - Debug Tools: Enable in Settings → Debug
- GTM Export: Settings → GTM Export
Summary
Custom events extend ADT’s capabilities to track your unique business actions. Key takeaways:
Implementation:
- Use
window.dataLayer.push()orwindow.adtPush()for standard events - Use
window.adtPushDeduped()for rapid-fire events - Include session data when available
- Follow naming conventions (snake_case)
Best Practices:
- Never include PII in events
- Be descriptive with event names
- Include context parameters
- Test thoroughly before production
- Document your custom events
Integration:
- Consent is checked automatically
- Session data is available via
window.ADTSession - Two-push pattern is handled automatically
- Events work with GTM, GA4, and pixels
Testing:
- Use browser console for quick tests
- Enable Debug Overlay for real-time monitoring
- Use GTM Preview Mode for integration testing
- Monitor Network tab for server-side verification
Custom events are powerful tools for tracking your unique business metrics. When combined with ADT’s built-in events, you have comprehensive visibility into every user interaction.