TL;DR
Using multiple GTM containers has been officially supported since 2015. However, all containers must share the same dataLayer — using different dataLayer names causes trigger issues and data loss. Modern GTM implementations require Consent Mode v2 (GDPR/CCPA), Server-side tagging, and GA4 event schema as essential components.
| Topic | 2019 Approach | 2025+ Current State |
|---|---|---|
| dataLayer | Different name per GTM | Single dataLayer required |
| Instantiation | dataLayer = [{...}] | window.dataLayer = window.dataLayer || [] |
| Analytics | Universal Analytics | GA4 (UA deprecated) |
| Privacy | Optional | Consent Mode v2 required |
| Tagging | Client-side only | Server-side preferred |
Why Multiple GTM Is Needed?
In most cases, a single GTM container is the best solution. However, multiple containers may be unavoidable in these scenarios:
- Different teams/agencies: Ad agency wants to manage their own tags
- Access control: Only certain people should have access to specific tags
- Multi-tenant structures: Different business units need independent tracking on the same domain
- Vendor requirements: Third-party software mandates their own GTM
Important: When an agency or vendor requests to install their own GTM container, the preferred approach is to grant them access to your GTM first. This ensures full visibility over changes.
dataLayer: Legacy vs Current Approach
Legacy Approach (2019)
2019 recommendation: Different dataLayer names could be used for each GTM container. For example, first container uses
dataLayer, second container usesnewLayer.
<!-- 2019: Different dataLayer names (NO LONGER RECOMMENDED) -->
<script>(...,'dataLayer','GTM-XXXX');</script>
<script>(...,'newLayer','GTM-YYYY');</script>
Current Approach (2025+)
Google’s current documentation clearly states:
“Using more than one data layer can cause some triggers to stop working and could have other implications.” — Google Tag Manager Developer Guide
All containers must share the same dataLayer:
<!-- Correct: Single dataLayer, multiple containers -->
<script>
window.dataLayer = window.dataLayer || [];
</script>
<!-- GTM Container 1 -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXX');</script>
<!-- GTM Container 2 -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-YYYY');</script>
dataLayer Instantiation
Legacy method (problematic):
dataLayer = [{'pageTitle': 'Home'}];This usage completely overwrites the existing dataLayer.
Correct method:
// Safe instantiation - preserves existing data
var dataLayer = window.dataLayer = window.dataLayer || [];
// Always add data using push()
dataLayer.push({
'pageTitle': 'Home',
'pageType': 'home'
});
Key Considerations for Multiple Containers
1. Shared dataLayer = Shared Events
Since all containers share the same dataLayer, events pushed by one container are visible to other containers. This means:
Advantage: Ensures data consistency Risk: Unintended tag triggers may occur
Solution: Keep event names specific. Use unique names like agency_remarketing_click instead of generic gaEvent.
2. Event Naming Convention
// ❌ Wrong: Generic event name
dataLayer.push({ 'event': 'click' });
// ✅ Correct: Specific and namespaced
dataLayer.push({ 'event': 'brand_cta_click' });
dataLayer.push({ 'event': 'agency_form_submit' });
3. PII (Personal Data) Risk
All data pushed to the shared dataLayer is accessible by all containers. If one container pushes PII (name, email, phone), other containers can also access this data.
// ⚠️ Warning: This data is visible to ALL containers
dataLayer.push({
'event': 'form_submit',
'userEmail': 'user@example.com', // PII!
'userName': 'John Doe' // PII!
});
Consent Mode v2 Integration
Consent Mode v2 is now mandatory for GDPR, CCPA, and other privacy regulations.
Basic Setup
// BEFORE the GTM snippet
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Default consent state (before user permission)
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied'
});
// Update after user grants permission
gtag('consent', 'update', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
'analytics_storage': 'granted'
});
Regional Consent Settings
// Default denied for EU users
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied',
'region': ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE',
'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV',
'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK',
'SI', 'ES', 'SE']
});
// Granted for other regions
gtag('consent', 'default', {
'ad_storage': 'granted',
'analytics_storage': 'granted'
});
GA4 Event Schema (Replacing Enhanced Ecommerce)
Legacy approach (Universal Analytics Enhanced Ecommerce):
dataLayer.push({ 'event': 'addToCart', 'ecommerce': { 'currencyCode': 'USD', 'add': { 'products': [{ 'id': '123', 'name': 'Product', 'price': 99.90 }] } } });
GA4 Ecommerce Event Schema:
dataLayer.push({ ecommerce: null }); // Clear previous ecommerce data
dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'currency': 'USD',
'value': 99.90,
'items': [{
'item_id': '123',
'item_name': 'Product Name',
'item_brand': 'Brand',
'item_category': 'Category',
'price': 99.90,
'quantity': 1
}]
}
});
GA4 Standard Events
| Event | Description |
|---|---|
view_item | Product detail page view |
add_to_cart | Add to cart |
remove_from_cart | Remove from cart |
begin_checkout | Start checkout |
add_payment_info | Payment info entry |
add_shipping_info | Shipping info entry |
purchase | Purchase completion |
Server-Side GTM
Server-side GTM has become the standard for enterprise projects in 2025.
Advantages
- Performance: Reduces client-side script load
- Privacy: Data processing in first-party context
- Ad-blocker bypass: Server-side requests aren’t blocked
- Data control: PII masking and filtering
Basic Architecture
[Browser] → [Client-side GTM] → [Server-side GTM Container] → [GA4, Ads, Meta...]
↓
[First-party domain]
Platform Integrations: Shopify
Shopify runs GTM as a Custom Pixel in a sandbox environment. This provides security but introduces some limitations.
Important: GTM setup on Shopify requires advanced JavaScript knowledge. Custom pixels are not supported by Shopify — troubleshooting is your responsibility.
Shopify Sandbox Limitations
- Limited direct DOM access
- No
document.cookieaccess - Limited
localStorage/sessionStorage - Third-party scripts run inside the sandbox
GTM Custom Pixel Setup
// dataLayer and gtag definition
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// GTM snippet (without HTML tags)
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer', 'GTM-XXXXXXX');
// Consent Mode v2
gtag('consent', 'update', {
'ad_storage': 'granted',
'analytics_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
});
Forwarding Shopify Events to GTM
Use Shopify’s analytics.subscribe() API to listen for standard events and push to dataLayer:
// Page view
analytics.subscribe("page_viewed", (event) => {
window.dataLayer.push({
event: "page_viewed",
timestamp: event.timestamp,
client_id: event.clientId,
url: event.context.document.location.href,
page_title: event.context.document.title,
});
});
// Product view
analytics.subscribe("product_viewed", (event) => {
window.dataLayer.push({
event: "product_viewed",
timestamp: event.timestamp,
client_id: event.clientId,
url: event.context.document.location.href,
product_id: event.data?.productVariant?.product?.id,
product_title: event.data?.productVariant?.title,
product_sku: event.data?.productVariant?.sku,
});
});
// Add to cart
analytics.subscribe("product_added_to_cart", (event) => {
window.dataLayer.push({
event: "product_added_to_cart",
timestamp: event.timestamp,
client_id: event.clientId,
url: event.context.document.location.href,
price: event.data?.cartLine?.merchandise?.price?.amount,
product_title: event.data?.cartLine?.merchandise?.product?.title,
quantity: event.data?.cartLine?.quantity,
total_cost: event.data?.cartLine?.cost?.totalAmount?.amount,
});
});
// Checkout complete
analytics.subscribe("checkout_completed", (event) => {
window.dataLayer.push({
event: "checkout_completed",
timestamp: event.timestamp,
token: event.data?.checkout?.token,
client_id: event.clientId,
email: event.data?.checkout?.email,
orderId: event.data?.checkout?.order?.id,
currency: event.data?.checkout?.currencyCode,
value: event.data?.checkout?.totalPrice?.amount,
tax: event.data?.checkout?.totalTax?.amount,
shipping: event.data?.checkout?.shippingLine?.price?.amount,
});
});
Shopify → GA4 Event Mapping
| Shopify Event | GTM Trigger | GA4 Event |
|---|---|---|
page_viewed | Custom Event | page_view |
product_viewed | Custom Event | view_item |
product_added_to_cart | Custom Event | add_to_cart |
product_removed_from_cart | Custom Event | remove_from_cart |
cart_viewed | Custom Event | view_cart |
checkout_started | Custom Event | begin_checkout |
checkout_address_info_submitted | Custom Event | add_shipping_info |
payment_info_submitted | Custom Event | add_payment_info |
checkout_completed | Custom Event | purchase |
Migrating Legacy dataLayer.push Code
Legacy method (in theme.liquid):
<script> dataLayer.push({ event: 'email_signup', email: customer.email }); </script>
New method (using Shopify.analytics.publish):
<!-- In theme.liquid -->
<script>
Shopify.analytics.publish('email_signup', { email: customer.email });
</script>
// In custom pixel
analytics.subscribe("email_signup", (event) => {
window.dataLayer.push({
'event': 'email_signup',
'email': event.customData.email,
});
});
GTM Trigger Setup
Create a Custom Event trigger in GTM for each Shopify event:
- Triggers → New → Custom Event
- Event name:
checkout_completed(or relevant event) - This trigger fires on: All Custom Events
- Connect the tag to this trigger
GA4 Tag Configuration
Tag Type: Google Analytics: GA4 Event
Measurement ID: G-XXXXXXXX
Event Name: purchase (or relevant GA4 event)
Event Parameters:
- transaction_id: {{DLV - orderId}}
- value: {{DLV - value}}
- currency: {{DLV - currency}}
- items: {{DLV - items}}
Testing: Use Shopify Pixel Helper and Google Tag Assistant together. Tag Assistant’s “Troubleshoot tag” feature won’t detect pixels inside the sandbox — check dataLayer manually.
noscript Tags
Each GTM container requires a noscript tag:
<!-- GTM Container 1 noscript -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- GTM Container 2 noscript -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-YYYY"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
Testing and Debugging
GTM Preview Mode
Preview Mode should be activated separately for each container:
- Start Preview for GTM-XXXX
- Start Preview for GTM-YYYY in a separate tab
- Check both debug panels on the target page
Tag Assistant
Use Google Tag Assistant to check:
- Which tags are firing
- dataLayer contents
- Consent state
Debug with Container ID
// Check specific container's state
console.log(window.google_tag_manager['GTM-XXXX']);
console.log(window.google_tag_manager['GTM-YYYY']);
Common Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
| Triggers not working | Different dataLayer names | Use single dataLayer |
| Data loss | Using dataLayer = [] | Use window.dataLayer = window.dataLayer || [] |
| Duplicate events | Generic event names | Use namespaced names |
| PII leakage | Sensitive data in shared dataLayer | Process data server-side |
| Consent violation | Missing Consent Mode | Integrate Consent Mode v2 |
FAQ
Can I use multiple GTM containers?
Yes, officially supported since 2015. However, all containers must share the same dataLayer.
Should I use different dataLayers for each GTM?
No. Google’s current recommendation is for all containers to use a single dataLayer. Different dataLayers can cause trigger issues and data loss.
Can I rename the dataLayer?
Technically possible but not recommended. Renaming should only be considered for very specific edge-cases.
Is Consent Mode v2 mandatory?
For sites subject to GDPR, CCPA, and similar regulations, yes. Also required for Google Ads conversion tracking.
When should I use Server-side GTM?
Recommended for performance-critical projects, when PII processing is needed, and for high-traffic sites affected by ad-blockers.
How do I migrate Universal Analytics events to GA4?
Convert Enhanced Ecommerce schema to GA4 event schema. Mappings like addToCart → add_to_cart, products → items are required.
How do I install GTM on Shopify?
Install as a Custom Pixel. Go to Settings → Customer events → Add custom pixel. Add GTM code without HTML tags and use analytics.subscribe() to listen to Shopify events and push to dataLayer.
What are Shopify sandbox limitations?
Limited direct DOM access, no document.cookie access, restricted localStorage/sessionStorage. Some GTM features (scroll tracking, click tracking) may not work inside the sandbox.
Key Takeaways
- Use single dataLayer — All GTM containers must share the same dataLayer
- Safe instantiation — Use
window.dataLayer = window.dataLayer || [] - Specific event names — Prefer namespaced, unique event names
- Consent Mode v2 — Mandatory for GDPR/CCPA compliance
- GA4 schema — Universal Analytics deprecated, migrate to GA4 events
- Consider server-side — Evaluate server-side GTM for performance and privacy
- Test, test, test — Validate every change with Preview Mode and Tag Assistant