Skip to content

Commit fc3981b

Browse files
fix(googleTagManager): push consent as arguments to dataLayer (#771)
Co-authored-by: motion-work <11538632+motion-work@users.noreply.github.com>
1 parent 727e025 commit fc3981b

2 files changed

Lines changed: 70 additions & 14 deletions

File tree

‎packages/script/src/runtime/registry/google-tag-manager.ts‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export { GoogleTagManagerOptions }
8181
exporttypeGoogleTagManagerInput=RegistryScriptInput<typeofGoogleTagManagerOptions>
8282

8383
exportinterfaceGoogleTagManagerConsent{
84-
/** Push `['consent','update', state]` onto dataLayer with GCMv2 partial state. */
84+
/** Send `gtag('consent','update', state)` so the dataLayer receives consent command (GCMv2 partial state). */
8585
update: (state: ConsentState)=>void
8686
}
8787

@@ -97,6 +97,8 @@ export function useScriptGoogleTagManager<T extends GoogleTagManagerApi>(
9797
onBeforeGtmStart?: (gtag: DataLayerPush)=>void
9898
},
9999
): UseScriptContext<UseFunctionType<NuxtUseScriptOptions<T>,T>,GoogleTagManagerConsent>{
100+
constconsentDataLayerName=options?.l??options?.dataLayer??'dataLayer'
101+
100102
constinstance=useRegistryScript<T,typeofGoogleTagManagerOptions>(
101103
options?.key||'googleTagManager',
102104
(opts)=>{
@@ -132,10 +134,13 @@ export function useScriptGoogleTagManager<T extends GoogleTagManagerApi>(
132134
// Initialize dataLayer if it doesn't exist
133135
(windowasany)[dataLayerName]=(windowasany)[dataLayerName]||[]
134136

135-
// Create gtag function
136-
functiongtag(...args: any[]){
137-
// Pushing arguments to dataLayer is necessary for GTM to process events
138-
(windowasany)[dataLayerName].push(args)
137+
// Create gtag function (must push the real `arguments` object —
138+
// not a spread array — so GTM processes consent and
139+
// other commands like the official snippet)
140+
functiongtag(..._args: any[]){
141+
// Rest params satisfy TypeScript call sites; gtm expects `arguments` on the queue.
142+
// eslint-disable-next-line prefer-rest-params
143+
(windowasany)[dataLayerName].push(arguments)
139144
}
140145

141146
// Assign gtag to window for global access
@@ -176,7 +181,15 @@ export function useScriptGoogleTagManager<T extends GoogleTagManagerApi>(
176181
if(import.meta.client&&!typed.consent){
177182
typed.consent={
178183
update: (state: ConsentState)=>{
179-
;((typed.proxyasunknownasGoogleTagManagerApi).dataLayerasany).push(['consent','update',state])
184+
constdl=(windowasany)[consentDataLayerName]=(windowasany)[consentDataLayerName]||[]
185+
// Must push the real `arguments` object — not a
186+
// spread array — so GTM processes consent and
187+
// other commands like the official snippet
188+
;(function(..._args: any[]){
189+
// Rest params satisfy TypeScript call sites; gtm expects `arguments` on the queue.
190+
// eslint-disable-next-line prefer-rest-params
191+
dl.push(arguments)
192+
})('consent','update',state)
180193
},
181194
}
182195
}

‎test/nuxt-runtime/consent-default.nuxt.test.ts‎

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ vi.mock('../../packages/script/src/runtime/composables/useScriptEventPage', () =
5151
useScriptEventPage: vi.fn(),
5252
}))
5353

54+
/** `gtag()` queues the Arguments object; `dataLayer.push([...])` uses a real Array. */
55+
functiongtmConsentCommandParts(e: any): [string,string,any?]|null{
56+
if(e==null||typeofe!=='object')
57+
returnnull
58+
if(!Array.isArray(e)&&!('length'ine&&typeof(easany).length==='number'))
59+
returnnull
60+
constrow=Array.isArray(e) ? e : Array.from(easArrayLike<any>)
61+
if(row[0]!=='consent')
62+
returnnull
63+
return[row[0]asstring,row[1]asstring,row[2]]
64+
}
65+
5466
describe('consent defaults — clientInit ordering',()=>{
5567
beforeEach(()=>{
5668
delete(windowasany).dataLayer
@@ -72,13 +84,14 @@ describe('consent defaults — clientInit ordering', () => {
7284
constdl=(windowasany).dataLayerasany[]
7385
expect(Array.isArray(dl)).toBe(true)
7486

75-
constconsentIdx=dl.findIndex(e=>Array.isArray(e)&&e[0]==='consent'&&e[1]==='default')
87+
constconsentIdx=dl.findIndex(e=>gtmConsentCommandParts(e)?.[1]==='default')
7688
conststartIdx=dl.findIndex(e=>e&&typeofe==='object'&&!Array.isArray(e)&&e.event==='gtm.js')
7789

7890
expect(consentIdx).toBeGreaterThanOrEqual(0)
7991
expect(startIdx).toBeGreaterThanOrEqual(0)
8092
expect(consentIdx).toBeLessThan(startIdx)
81-
expect(dl[consentIdx][2]).toMatchObject({analytics_storage: 'denied',ad_storage: 'denied'})
93+
constconsentRow=gtmConsentCommandParts(dl[consentIdx])
94+
expect(consentRow?.[2]).toMatchObject({analytics_storage: 'denied',ad_storage: 'denied'})
8295
})
8396

8497
it('gtm: array form fires multiple ["consent","default",…] entries in input order, all before gtm.js',async()=>{
@@ -98,14 +111,14 @@ describe('consent defaults — clientInit ordering', () => {
98111

99112
constconsentEntries=dl
100113
.map((e,i)=>({ e, i }))
101-
.filter(({ e })=>Array.isArray(e)&&e[0]==='consent'&&e[1]==='default')
114+
.filter(({ e })=>gtmConsentCommandParts(e)?.[1]==='default')
102115
conststartIdx=dl.findIndex(e=>e&&typeofe==='object'&&!Array.isArray(e)&&e.event==='gtm.js')
103116

104117
expect(consentEntries).toHaveLength(2)
105118
expect(startIdx).toBeGreaterThanOrEqual(0)
106119
for(const{ i }ofconsentEntries)expect(i).toBeLessThan(startIdx)
107-
expect(consentEntries[0].e[2]).toMatchObject({analytics_storage: 'denied',region: ['ES','US-AK'],wait_for_update: 500})
108-
expect(consentEntries[1].e[2]).toMatchObject({ad_storage: 'denied'})
120+
expect(gtmConsentCommandParts(consentEntries[0].e)?.[2]).toMatchObject({analytics_storage: 'denied',region: ['ES','US-AK'],wait_for_update: 500})
121+
expect(gtmConsentCommandParts(consentEntries[1].e)?.[2]).toMatchObject({ad_storage: 'denied'})
109122
})
110123

111124
it('gtm: single-entry array is observationally equivalent to a bare object',async()=>{
@@ -120,6 +133,14 @@ describe('consent defaults — clientInit ordering', () => {
120133
// Slice up to (but not including) gtm.js — the entry carries a Date.now() timestamp
121134
// that differs between calls. Ordering relative to gtm.js is locked by the sibling test.
122135
returndl.slice(0,startIdx)
136+
// `gtag()` pushes the Arguments object onto dataLayer, so each run yields distinct exotic
137+
// objects. Normalize array-like rows to real arrays so `toEqual` compares values only,
138+
// not Arguments identity or matcher quirks across two separate `clientInit` runs.
139+
.map(e=>
140+
e!=null&&typeofe==='object'&&!Array.isArray(e)&&'length'ine&&typeof(easany).length==='number'
141+
? Array.from(easArrayLike<any>)
142+
: e,
143+
)
123144
}
124145

125146
constfromObject=runWith({ad_storage: 'denied'})
@@ -135,13 +156,32 @@ describe('consent defaults — clientInit ordering', () => {
135156
result._opts.clientInit()
136157

137158
constdl=(windowasany).dataLayerasany[]
138-
constconsentEntries=dl.filter(e=>Array.isArray(e)&&e[0]==='consent'&&e[1]==='default')
159+
constconsentEntries=dl.filter(e=>gtmConsentCommandParts(e)?.[1]==='default')
139160
conststartIdx=dl.findIndex(e=>e&&typeofe==='object'&&!Array.isArray(e)&&e.event==='gtm.js')
140161

141162
expect(consentEntries).toHaveLength(0)
142163
expect(startIdx).toBeGreaterThanOrEqual(0)
143164
})
144165

166+
it('gtm: defaultConsent via gtag queues Arguments, not a plain Array (gtag.js contract)',async()=>{
167+
const{ useScriptGoogleTagManager }=awaitimport('../../packages/script/src/runtime/registry/google-tag-manager')
168+
constresult: any=useScriptGoogleTagManager({
169+
id: 'GTM-XXXX',
170+
defaultConsent: {analytics_storage: 'denied'},
171+
})
172+
173+
result._opts.clientInit()
174+
175+
constdl=(windowasany).dataLayerasany[]
176+
constentry=dl.find(e=>gtmConsentCommandParts(e)?.[1]==='default')
177+
expect(entry).toBeDefined()
178+
expect(Array.isArray(entry)).toBe(false)
179+
constparts=Array.from(entryasArrayLike<any>)
180+
expect(parts[0]).toBe('consent')
181+
expect(parts[1]).toBe('default')
182+
expect(parts[2]).toMatchObject({analytics_storage: 'denied'})
183+
})
184+
145185
it('matomo: "required" pushes requireConsent before setSiteId',async()=>{
146186
;(windowasany)._paq=[]
147187
const{ useScriptMatomoAnalytics }=awaitimport('../../packages/script/src/runtime/registry/matomo-analytics')
@@ -339,14 +379,17 @@ describe('per-script consent object', () => {
339379
expect(updateArgs?.[2]).toEqual({ad_storage: 'granted'})
340380
})
341381

342-
it('gtm: consent.update() pushes ["consent","update", state] onto dataLayer',async()=>{
382+
it('gtm: consent.update() queues Arguments(consent, update, state) to dataLayer',async()=>{
343383
;(windowasany).dataLayer=[]
344384
const{ useScriptGoogleTagManager }=awaitimport('../../packages/script/src/runtime/registry/google-tag-manager')
345385
constresult: any=useScriptGoogleTagManager({id: 'GTM-XXXX'})
346386
result._opts.clientInit()
347387
result.consent.update({analytics_storage: 'granted'})
348388
constdl=(windowasany).dataLayerasany[]
349-
expect(dl).toContainEqual(['consent','update',{analytics_storage: 'granted'}])
389+
constentry=dl.find(e=>gtmConsentCommandParts(e)?.[1]==='update')
390+
expect(entry).toBeDefined()
391+
expect(Array.isArray(entry)).toBe(false)
392+
expect(gtmConsentCommandParts(entry)).toEqual(['consent','update',{analytics_storage: 'granted'}])
350393
})
351394

352395
it('meta: consent.grant()/revoke() queue fbq(\'consent\', ...) calls',async()=>{

0 commit comments

Comments
 (0)