From 98d5575463f306b153a605a6b575745cf41e505a Mon Sep 17 00:00:00 2001 From: dal4segno Date: Tue, 1 Sep 2026 08:56:20 +0900 Subject: [PATCH] fix: queue GA4 commands in the official format --- package.json | 2 +- src/lib/analytics/google-analytics.test.ts | 50 ++++++++++++++++++++++ src/lib/analytics/google-analytics.ts | 6 ++- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/lib/analytics/google-analytics.test.ts diff --git a/package.json b/package.json index 0ae367d..3c94a15 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "package:offline": "npm run build:offline && powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-offline.ps1", "preview": "vite preview --host 0.0.0.0", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "test:engine": "node --test src/lib/engine/*.test.ts", + "test:engine": "node --test src/lib/engine/*.test.ts src/lib/analytics/*.test.ts", "start": "node build", "data:sync": "node scripts/collect-holodoridb.mjs", "data:import-songs": "node scripts/import-legacy-song-data.mjs", diff --git a/src/lib/analytics/google-analytics.test.ts b/src/lib/analytics/google-analytics.test.ts new file mode 100644 index 0000000..db62876 --- /dev/null +++ b/src/lib/analytics/google-analytics.test.ts @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createGoogleAnalytics } from './google-analytics.ts'; + +test('Google 공식 형식의 arguments 객체를 dataLayer에 적재한다', () => { + const storage = new Map(); + const appendedScripts: Array<{ src?: string }> = []; + const originalWindow = globalThis.window; + const originalDocument = globalThis.document; + + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: { + localStorage: { + getItem: (key: string) => storage.get(key) ?? null, + setItem: (key: string, value: string) => storage.set(key, value) + }, + location: { origin: 'https://doricalc-holo.live' } + } + }); + Object.defineProperty(globalThis, 'document', { + configurable: true, + value: { + cookie: '', + title: '홀로도리 계산기', + createElement: () => ({ async: false, src: '', dataset: {} }), + head: { append: (script: { src?: string }) => appendedScripts.push(script) } + } + }); + + try { + const analytics = createGoogleAnalytics('G-TEST1234'); + analytics.grantConsent(); + analytics.trackPageView('/cards'); + + const dataLayer = (globalThis.window as unknown as GoogleTagWindow).dataLayer ?? []; + assert.equal(dataLayer.length, 4); + assert.equal(Array.isArray(dataLayer[0]), false); + assert.equal(dataLayer[0][0], 'consent'); + assert.equal(dataLayer[2][0], 'config'); + assert.equal(dataLayer[3][0], 'event'); + assert.equal(dataLayer[3][1], 'page_view'); + assert.equal(appendedScripts[0]?.src, 'https://www.googletagmanager.com/gtag/js?id=G-TEST1234'); + } finally { + Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow }); + Object.defineProperty(globalThis, 'document', { configurable: true, value: originalDocument }); + } +}); + +type GoogleTagWindow = Window & { dataLayer?: IArguments[] }; diff --git a/src/lib/analytics/google-analytics.ts b/src/lib/analytics/google-analytics.ts index 3d25633..3961e8d 100644 --- a/src/lib/analytics/google-analytics.ts +++ b/src/lib/analytics/google-analytics.ts @@ -4,7 +4,7 @@ const CONSENT_STORAGE_KEY = 'holodori:analytics-consent'; const MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]+$/i; type GoogleTagWindow = Window & { - dataLayer?: unknown[][]; + dataLayer?: IArguments[]; gtag?: (...args: unknown[]) => void; }; @@ -56,7 +56,9 @@ export function createGoogleAnalytics(rawMeasurementId: string | undefined): Ana if (!available || !currentWindow || loaded) return; currentWindow.dataLayer ??= []; - currentWindow.gtag ??= (...args: unknown[]) => currentWindow.dataLayer?.push(args); + currentWindow.gtag ??= function googleTag(..._args: unknown[]) { + currentWindow.dataLayer?.push(arguments); + }; currentWindow.gtag('consent', 'default', { analytics_storage: 'granted', ad_storage: 'denied',