Google Tag Manager Data Layer on HubSpot
The data layer is a JavaScript object that passes information from your HubSpot site to Google Tag Manager. This guide shows how to leverage HubSpot's unique features (HubL, CRM data, content properties) in your GTM data layer.
Prerequisites
- Install GTM on HubSpot
- Access to HubSpot Site Header HTML or template code
- Understanding of HubL templating language
- Super Admin or Website Editor permissions
Data Layer Basics
What is the Data Layer?
The data layer is a JavaScript array that holds information about:
- Page details (type, name, author)
- User/contact information (lifecycle stage, CRM data)
- Events (form submissions, clicks, interactions)
- Ecommerce data (products, transactions)
Basic Data Layer Structure
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'key': 'value',
'anotherKey': 'anotherValue'
});
Implementing HubSpot Data Layer
Method 1: Site Header HTML (Global Implementation)
Add this code to Settings → Website → Pages → Site Header HTML BEFORE your GTM container code:
<script>
// Initialize data layer
window.dataLayer = window.dataLayer || [];
// Push HubSpot data to data layer
dataLayer.push({
// Page Information
'pageType': '{{ content.type }}',
'pageId': '{{ content.id }}',
'pageName': '{{ content.name }}',
'pageUrl': '{{ content.absolute_url }}',
'pageLanguage': '{{ content.language }}',
{% if content.type == 'blog_post' %}
// Blog-specific data
'blogAuthor': '{{ content.blog_post_author.display_name }}',
'publishDate': '{{ content.publish_date }}',
'blogTags': [{% for tag in content.topic_list %}'{{ tag.name }}'{% if not loop.last %},{% endif %}{% endfor %}],
{% endif %}
{% if contact %}
// Contact/CRM Information (for known visitors)
'contactId': '{{ contact.vid }}',
'lifecycleStage': '{{ contact.lifecycle_stage }}',
'contactEmail': '{{ contact.email }}',
'leadSource': '{{ contact.hs_analytics_source }}',
'isContact': true,
{% if contact.associatedcompany %}
// Company information (if contact has associated company)
'companyName': '{{ contact.associatedcompany.name }}',
'companyIndustry': '{{ contact.associatedcompany.industry }}',
'companySize': '{{ contact.associatedcompany.numberofemployees }}',
{% endif %}
{% else %}
// Anonymous visitor
'isContact': false,
{% endif %}
// Portal/Domain Information
'portalId': '{{ portal_id }}',
'domain': '{{ request.domain }}'
});
</script>
<!-- Then your GTM container code -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');</script>
Method 2: Template-Level Implementation
For more control, add to specific templates in Design Tools:
{# Add before </head> in template #}
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'pageType': '{{ content.type }}',
'contentGroup': '{{ content.content_group_id }}',
{% if content.custom_property %}
'customProperty': '{{ content.custom_property }}',
{% endif %}
{% if contact %}
'contactId': '{{ contact.vid }}',
'lifecycleStage': '{{ contact.lifecycle_stage }}'
{% endif %}
});
</script>
HubSpot-Specific Data Layer Variables
Page & Content Variables
Standard Page Properties
dataLayer.push({
// Basic page info
'pageType': '{{ content.type }}', // page, landing_page, blog_post, etc.
'pageId': '{{ content.id }}',
'pageName': '{{ content.name }}',
'pageTitle': '{{ page_meta.html_title }}',
'metaDescription': '{{ page_meta.meta_description }}',
// URLs and paths
'pageUrl': '{{ content.absolute_url }}',
'canonicalUrl': '{{ content.canonical_url }}',
'pagePath': '{{ request.path }}',
// Dates
'publishDate': '{{ content.publish_date }}',
'lastModified': '{{ content.updated }}',
'createDate': '{{ content.created }}'
});
Blog Post Properties
{% if content.type == 'blog_post' %}
dataLayer.push({
'blogId': '{{ group.id }}',
'blogName': '{{ group.name }}',
'postAuthor': '{{ content.blog_post_author.display_name }}',
'authorEmail': '{{ content.blog_post_author.email }}',
'postCategory': '{{ content.category_id }}',
'postTags': [
{% for tag in content.topic_list %}
'{{ tag.name }}'{% if not loop.last %},{% endif %}
{% endfor %}
],
'commentCount': {{ content.comment_count|default(0) }},
'isFeatured': {{ content.featured_image != null }}
});
{% endif %}
Landing Page Properties
{% if content.type == 'landing_page' %}
dataLayer.push({
'landingPageCampaign': '{{ content.campaign }}',
'landingPageName': '{{ content.name }}',
'isGated': {% if content.has_form %}true{% else %}false{% endif %}
});
{% endif %}
Contact & CRM Variables
Contact Properties
{% if contact %}
dataLayer.push({
// Core contact info
'contactId': '{{ contact.vid }}',
'contactEmail': '{{ contact.email }}',
'firstName': '{{ contact.firstname }}',
'lastName': '{{ contact.lastname }}',
// Lifecycle and status
'lifecycleStage': '{{ contact.lifecycle_stage }}', // subscriber, lead, MQL, SQL, customer, etc.
'contactStatus': '{{ contact.hs_lead_status }}',
// Attribution
'originalSource': '{{ contact.hs_analytics_source }}',
'originalSourceData1': '{{ contact.hs_analytics_source_data_1 }}',
'originalSourceData2': '{{ contact.hs_analytics_source_data_2 }}',
// Engagement metrics
'emailOpens': {{ contact.hs_email_open|default(0) }},
'emailClicks': {{ contact.hs_email_click|default(0) }},
'pageViews': {{ contact.hs_analytics_num_page_views|default(0) }},
'visits': {{ contact.hs_analytics_num_visits|default(0) }},
// Dates
'createDate': '{{ contact.createdate }}',
'lastModifiedDate': '{{ contact.lastmodifieddate }}',
'lastContactedDate': '{{ contact.notes_last_contacted }}'
});
{% endif %}
Company Properties
{% if contact.associatedcompany %}
dataLayer.push({
'companyId': '{{ contact.associatedcompany.companyid }}',
'companyName': '{{ contact.associatedcompany.name }}',
'companyDomain': '{{ contact.associatedcompany.domain }}',
'companyIndustry': '{{ contact.associatedcompany.industry }}',
'companyType': '{{ contact.associatedcompany.type }}',
'companySize': '{{ contact.associatedcompany.numberofemployees }}',
'companyRevenue': '{{ contact.associatedcompany.annualrevenue }}',
'companyCity': '{{ contact.associatedcompany.city }}',
'companyState': '{{ contact.associatedcompany.state }}',
'companyCountry': '{{ contact.associatedcompany.country }}'
});
{% endif %}
Deal Properties
{% if contact %}
dataLayer.push({
'hasDeals': {% if contact.num_associated_deals > 0 %}true{% else %}false{% endif %},
'numberOfDeals': {{ contact.num_associated_deals|default(0) }},
'totalRevenue': {{ contact.total_revenue|default(0) }}
});
{% endif %}
Smart Content & Personalization Variables
Track which Smart Content variant is shown:
dataLayer.push({
'smartContentVariant': '{% if contact.lifecycle_stage == "customer" %}customer{% elif contact.lifecycle_stage == "lead" %}lead{% else %}default{% endif %}',
'personalizationEnabled': true
});
Custom HubSpot Properties
If you've created custom contact or company properties:
{% if contact %}
dataLayer.push({
'customPropertyName': '{{ contact.custom_property_name }}',
'industryVertical': '{{ contact.industry_vertical }}',
'productInterest': '{{ contact.product_interest }}'
});
{% endif %}
Event-Based Data Layer Pushes
Form Submission Events
Add to Site Header HTML:
<script>
// Listen for HubSpot form submissions
window.addEventListener('message', function(event) {
if (event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmitted') {
dataLayer.push({
'event': 'form_submission',
'formId': event.data.id,
'formType': 'hubspot_form',
'formName': event.data.data.formGuid || 'unknown',
'pageUrl': window.location.href,
{% if contact %}
'contactId': '{{ contact.vid }}',
'lifecycleStage': '{{ contact.lifecycle_stage }}'
{% endif %}
});
}
});
</script>
CTA Click Events
<script>
document.addEventListener('click', function(event) {
var ctaElement = event.target.closest('.cta_button, [class*="hs-cta"]');
if (ctaElement) {
dataLayer.push({
'event': 'cta_click',
'ctaId': ctaElement.getAttribute('data-hs-cta-id') || 'unknown',
'ctaText': ctaElement.textContent.trim(),
'ctaUrl': ctaElement.href || '',
'pageUrl': window.location.href
});
}
});
</script>
Meeting Booking Events
<script>
window.addEventListener('message', function(event) {
if (event.data.meetingBookSucceeded === true) {
dataLayer.push({
'event': 'meeting_booked',
'meetingType': event.data.meetingPayload.meetingType || 'unknown',
'meetingDuration': event.data.meetingPayload.duration || '',
{% if contact %}
'contactId': '{{ contact.vid }}'
{% endif %}
});
}
});
</script>
Chat Interaction Events
<script>
// Wait for HubSpot conversations to load
window.hsConversationsOnReady = [function() {
window.HubSpotConversations.on('conversationStarted', function() {
dataLayer.push({
'event': 'chat_started',
'chatType': 'hubspot_live_chat'
});
});
window.HubSpotConversations.on('conversationClosed', function() {
dataLayer.push({
'event': 'chat_closed'
});
});
}];
</script>
Creating GTM Variables from Data Layer
Step 1: Create Data Layer Variables in GTM
For each data layer variable you want to use:
- In GTM, go to Variables → New
- Variable Type: Data Layer Variable
- Data Layer Variable Name: Enter the exact key from your data layer (e.g.,
pageType) - Data Layer Version: Version 2
- Name the variable (e.g., "DL - Page Type")
- Save
Common HubSpot Variables to Create
Create these data layer variables in GTM:
| Variable Name | Data Layer Key | Use Case |
|---|---|---|
| DL - Page Type | pageType | Trigger tags based on page type |
| DL - Contact ID | contactId | Track identified users |
| DL - Lifecycle Stage | lifecycleStage | Segment by customer lifecycle |
| DL - Blog Author | blogAuthor | Track by content author |
| DL - Company Name | companyName | B2B attribution |
| DL - Form ID | formId | Track specific forms |
Step 2: Use Variables in Tags and Triggers
Example: Send Lifecycle Stage to GA4
- Edit your GA4 Configuration tag
- Fields to Set → Add Row:
- Field Name:
lifecycle_stage - Value:
\{\{DL - Lifecycle Stage\}\}
- Field Name:
- Save and Publish
Example: Trigger Tag Only for Blog Posts
- Create new trigger
- Type: Page View
- Fire on: Some Page Views
- Condition:
\{\{DL - Page Type\}\}equalsblog_post - Save
Advanced Data Layer Implementations
Conditional Data Based on Page Type
<script>
window.dataLayer = window.dataLayer || [];
var pageData = {
'pageType': '{{ content.type }}',
'pageId': '{{ content.id }}'
};
{% if content.type == 'blog_post' %}
pageData.blogAuthor = '{{ content.blog_post_author.display_name }}';
pageData.blogTags = [{% for tag in content.topic_list %}'{{ tag.name }}'{% if not loop.last %},{% endif %}{% endfor %}];
{% elif content.type == 'landing_page' %}
pageData.campaign = '{{ content.campaign }}';
pageData.hasForm = {{ content.has_form }};
{% endif %}
{% if contact %}
pageData.contactId = '{{ contact.vid }}';
pageData.lifecycleStage = '{{ contact.lifecycle_stage }}';
{% endif %}
dataLayer.push(pageData);
</script>
E-commerce Data Layer
For HubSpot Commerce or Payments:
{% if content.product_id %}
dataLayer.push({
'event': 'view_item',
'ecommerce': {
'currency': 'USD',
'value': {{ content.product_price }},
'items': [{
'item_id': '{{ content.product_id }}',
'item_name': '{{ content.product_name }}',
'item_category': '{{ content.product_category }}',
'price': {{ content.product_price }},
'quantity': 1
}]
}
});
{% endif %}
User ID Tracking
Pass HubSpot contact ID as GA4 User ID:
{% if contact %}
dataLayer.push({
'user_id': '{{ contact.vid }}', // HubSpot contact ID
'user_properties': {
'lifecycle_stage': '{{ contact.lifecycle_stage }}',
'customer_type': '{% if contact.lifecycle_stage == "customer" %}paying{% else %}free{% endif %}'
}
});
{% endif %}
Debugging the Data Layer
Method 1: Browser Console
// View entire data layer
console.log(dataLayer);
// View specific data layer push
console.log(dataLayer[0]); // First push
console.log(dataLayer[dataLayer.length - 1]); // Most recent push
// Find specific events
dataLayer.filter(item => item.event === 'form_submission');
Method 2: GTM Preview Mode
- Enable GTM Preview mode
- Visit your HubSpot page
- In Tag Assistant, click Variables
- Expand Data Layer
- See all values pushed to data layer
Method 3: Google Tag Assistant (Browser Extension)
- Install Google Tag Assistant browser extension
- Visit your HubSpot site
- Click extension icon
- View data layer values in real-time
Common Data Layer Issues
HubL Variables Return Empty
Problem: \{\{ contact.email \}\} returns empty string
Cause: Contact not identified yet, or HubL syntax error
Solution:
{% if contact.email %}
'contactEmail': '{{ contact.email }}'
{% else %}
'contactEmail': 'anonymous'
{% endif %}
Data Layer Pushed After GTM Loads
Problem: Variables undefined in GTM
Cause: Data layer push happens after GTM container loads
Fix: Always push data layer BEFORE GTM code:
<!-- Data layer first -->
<script>dataLayer.push({...});</script>
<!-- GTM container second -->
<script>(function(w,d,s,l,i){...})</script>
Variables Not Available in GTM
Problem: Data layer variable shows "undefined" in GTM
Debug:
- Check variable name matches exactly (case-sensitive)
- Verify data layer pushes before GTM loads
- Check HubL syntax is correct
- Use GTM Preview to inspect data layer
Testing Your Data Layer
1. Use GTM Preview Mode
- Enable Preview in GTM
- Visit HubSpot pages
- Check Variables tab in Tag Assistant
- Verify all expected variables populate
2. Test Across Page Types
Test data layer on different HubSpot content types:
- Standard pages
- Blog posts
- Landing pages
- Thank you pages
Verify page-specific variables populate correctly.
3. Test Logged In vs. Anonymous
- Visit as anonymous user → Contact variables should be empty/false
- Log in or identify yourself → Contact variables should populate
4. Verify Events Fire
Trigger events and check data layer:
- Submit form → Check
event: 'form_submission'pushed - Click CTA → Check
event: 'cta_click'pushed - Book meeting → Check
event: 'meeting_booked'pushed
Next Steps
- Set up GA4 via GTM - Use data layer in GA4
- Configure Event Tracking - Track HubSpot events
- Troubleshoot Events - Debug tracking issues
For general GTM data layer concepts, see GTM Data Layer Guide.