Context
We are launching Google Ads campaigns on kestra.io and need Google Consent Mode v2 properly implemented. Currently, GTM does not load at all until the user accepts cookies in Europe, which means Google cannot use conversion modeling to estimate conversions from users who decline cookies — causing ~10-30% underreporting of conversions in European markets.
Current Implementation (source: cookie-consent.ts)
Library: vanilla-cookieconsent (open-source)
Detection: Europe detection via Intl.DateTimeFormat().resolvedOptions().timeZone
Current behavior:
- Non-Europe users: GTM + PostHog + marketing load immediately (no consent required) ✅
- Europe users: GTM is not loaded at all until user clicks "Accept". The
enabledAnalytics() function injects the GTM script tag only after consent is granted via the onConsent callback.
Two consent categories exist:
analytics → triggers enabledAnalytics() (loads GTM + PostHog)
marketing → triggers enabledMarketing() (pushes enable_marketing event to dataLayer)
The problem: Google Consent Mode v2 requires GTM to be loaded before consent, with all consent parameters set to denied. This allows Google to send anonymous pings for conversion modeling. Currently, GTM is not present on the page at all before consent, so Google receives zero signals.
Technical Details
- GTM container:
GTM-T4F85WRF
- GA4 property:
G-EYVNS03HHR
- Google Ads tag:
AW-18075963910
- File to modify:
cookie-consent.ts (or equivalent in the Astro project)
Required Changes
Step 1: Always load GTM (even before consent)
Move the GTM script injection outside of enabledAnalytics() so it loads on every page for European users too. But before loading GTM, set the consent defaults to denied.
Before GTM loads (for European users):
// Set consent defaults BEFORE loading GTM
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'wait_for_update': 500
});
// Now load GTM
window.dataLayer.push({
'gtm.start': new Date().getTime(),
event: 'gtm.js'
});
const s = document.createElement('script');
s.async = true;
s.src = `https://www.googletagmanager.com/gtm.js?id=${GTM_ID}`;
document.head.appendChild(s);
Step 2: Update consent when user accepts
In the onConsent callback, after checking categories, push the consent update:
onConsent: ({ cookie }) => {
let consentCategories = cookie.categories;
if (consentCategories.includes('analytics')) {
gtag('consent', 'update', {
'analytics_storage': 'granted'
});
// Keep existing PostHog init logic here
// But REMOVE the GTM loading code (it's already loaded in Step 1)
}
if (consentCategories.includes('marketing')) {
gtag('consent', 'update', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted'
});
enabledMarketing(); // Keep existing behavior
}
}
Step 3: Handle non-Europe users
For non-European users, set consent to granted by default (before GTM loads):
if (!isEurope) {
gtag('consent', 'default', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
'analytics_storage': 'granted'
});
// Then load GTM + PostHog + marketing as before
}
Step 4: Handle decline / only partial consent
- If user only accepts
analytics: analytics_storage → granted, ad params remain denied
- If user rejects all: all parameters remain
denied, Google tags fire in cookieless ping mode
Suggested Refactored Structure
window.addEventListener('DOMContentLoaded', () => {
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
if (!isEurope) {
// Non-Europe: grant all, load everything
gtag('consent', 'default', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
'analytics_storage': 'granted'
});
loadGTM();
initPostHog();
enabledMarketing();
return;
}
// Europe: deny all by default, load GTM anyway
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'wait_for_update': 500
});
loadGTM(); // GTM loads but tags wait for consent
// Show cookie banner
cookieConsent.run({
mode: 'opt-in',
// ... existing config ...
onConsent: ({ cookie }) => {
if (cookie.categories.includes('analytics')) {
gtag('consent', 'update', {
'analytics_storage': 'granted'
});
initPostHog(); // PostHog only after analytics consent
}
if (cookie.categories.includes('marketing')) {
gtag('consent', 'update', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted'
});
enabledMarketing();
}
},
// ... rest of existing config ...
});
});
Key Architectural Change
| Aspect |
Current |
Required |
| GTM loading (Europe) |
Only after consent |
Always (with denied defaults) |
| Consent signals to Google |
None |
denied by default → granted on accept |
| PostHog loading |
After analytics consent |
After analytics consent (unchanged) |
| Marketing dataLayer push |
After marketing consent |
After marketing consent (unchanged) |
| Conversion modeling |
Not possible |
Enabled (Google receives anonymous pings) |
Acceptance Criteria
GTM Configuration Required After This Change
Once the consent signals are implemented in the code, the following GTM changes are needed (to be done by SEO/Marketing team):
- Each Google Ads tag needs its "Consent Settings" configured to require
ad_storage and ad_user_data
- The GA4 tag needs its "Consent Settings" configured to require
analytics_storage
Priority
High — Prerequisite for accurate Google Ads conversion reporting and full GDPR compliance. Blocking conversion modeling for European paid campaigns.
References
Context
We are launching Google Ads campaigns on kestra.io and need Google Consent Mode v2 properly implemented. Currently, GTM does not load at all until the user accepts cookies in Europe, which means Google cannot use conversion modeling to estimate conversions from users who decline cookies — causing ~10-30% underreporting of conversions in European markets.
Current Implementation (source:
cookie-consent.ts)Library:
vanilla-cookieconsent(open-source)Detection: Europe detection via
Intl.DateTimeFormat().resolvedOptions().timeZoneCurrent behavior:
enabledAnalytics()function injects the GTM script tag only after consent is granted via theonConsentcallback.Two consent categories exist:
analytics→ triggersenabledAnalytics()(loads GTM + PostHog)marketing→ triggersenabledMarketing()(pushesenable_marketingevent to dataLayer)The problem: Google Consent Mode v2 requires GTM to be loaded before consent, with all consent parameters set to
denied. This allows Google to send anonymous pings for conversion modeling. Currently, GTM is not present on the page at all before consent, so Google receives zero signals.Technical Details
GTM-T4F85WRFG-EYVNS03HHRAW-18075963910cookie-consent.ts(or equivalent in the Astro project)Required Changes
Step 1: Always load GTM (even before consent)
Move the GTM script injection outside of
enabledAnalytics()so it loads on every page for European users too. But before loading GTM, set the consent defaults todenied.Before GTM loads (for European users):
Step 2: Update consent when user accepts
In the
onConsentcallback, after checking categories, push the consent update:Step 3: Handle non-Europe users
For non-European users, set consent to
grantedby default (before GTM loads):Step 4: Handle decline / only partial consent
analytics:analytics_storage→granted, ad params remaindenieddenied, Google tags fire in cookieless ping modeSuggested Refactored Structure
Key Architectural Change
deniedby default →grantedon acceptAcceptance Criteria
ad_storage,analytics_storage,ad_user_data,ad_personalizationdenied(Europe users)grantedbased on accepted categoriesdeniedbut Google tags still fire in cookieless modegrantedenable_marketingdataLayer event still fires only after marketing consent (unchanged behavior)GTM Configuration Required After This Change
Once the consent signals are implemented in the code, the following GTM changes are needed (to be done by SEO/Marketing team):
ad_storageandad_user_dataanalytics_storagePriority
High — Prerequisite for accurate Google Ads conversion reporting and full GDPR compliance. Blocking conversion modeling for European paid campaigns.
References