From 630602858ec23b5a6ec4388098aaf5070b598c4d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 14 May 2026 16:50:01 +0700 Subject: [PATCH] feat: gtm --- .gitignore | 3 ++ apps/frontend/package.json | 2 + apps/frontend/scripts/fetch-gtm.mjs | 50 +++++++++++++++++++ apps/frontend/src/app/(app)/layout.tsx | 2 + .../src/components/layout/gtm.component.tsx | 21 ++++++++ .../src/components/onboarding/onboarding.tsx | 11 ++++ 6 files changed, 89 insertions(+) create mode 100644 apps/frontend/scripts/fetch-gtm.mjs create mode 100644 apps/frontend/src/components/layout/gtm.component.tsx diff --git a/.gitignore b/.gitignore index 0d7684f2..5cb60826 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ Thumbs.db .secrets/ libraries/plugins/src/plugins.ts i18n.cache + +# Generated by apps/frontend/scripts/fetch-gtm.mjs on install +apps/frontend/public/g.js diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 85f5f408..4e92b3ca 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -5,6 +5,8 @@ "type": "module", "scripts": { "dev": "dotenv -e ../../.env -- next dev -p 4200", + "fetch-gtm": "node scripts/fetch-gtm.mjs", + "postinstall": "node scripts/fetch-gtm.mjs", "build": "next build", "build:sentry": "dotenv -e ../../.env -- next build", "start": "dotenv -e ../../.env -- next start -p 4200", diff --git a/apps/frontend/scripts/fetch-gtm.mjs b/apps/frontend/scripts/fetch-gtm.mjs new file mode 100644 index 00000000..d1465c1b --- /dev/null +++ b/apps/frontend/scripts/fetch-gtm.mjs @@ -0,0 +1,50 @@ +import { writeFile, mkdir, readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const envPath = resolve(__dirname, '..', '..', '..', '.env'); +const outPath = resolve(__dirname, '..', 'public', 'g.js'); + +if (!process.env.NEXT_PUBLIC_GTM_ID && existsSync(envPath)) { + const content = await readFile(envPath, 'utf8'); + for (const raw of content.split('\n')) { + const line = raw.trim(); + if (!line || line.startsWith('#')) continue; + const eq = line.indexOf('='); + if (eq === -1) continue; + const key = line.slice(0, eq).trim(); + let value = line.slice(eq + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + if (!process.env[key]) process.env[key] = value; + } +} + +const id = process.env.NEXT_PUBLIC_GTM_ID; +if (!id) { + console.log('[fetch-gtm] NEXT_PUBLIC_GTM_ID not set, skipping'); + process.exit(0); +} + +const url = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(id)}`; +try { + console.log(`[fetch-gtm] fetching ${url}`); + const res = await fetch(url); + if (!res.ok) { + console.warn(`[fetch-gtm] non-OK response ${res.status}, skipping`); + process.exit(0); + } + const body = await res.text(); + await mkdir(dirname(outPath), { recursive: true }); + await writeFile(outPath, body, 'utf8'); + console.log(`[fetch-gtm] wrote ${outPath} (${body.length} bytes)`); +} catch (err) { + console.warn(`[fetch-gtm] failed: ${err?.message || err}, skipping`); + process.exit(0); +} diff --git a/apps/frontend/src/app/(app)/layout.tsx b/apps/frontend/src/app/(app)/layout.tsx index dedbbf09..eaa4aebe 100644 --- a/apps/frontend/src/app/(app)/layout.tsx +++ b/apps/frontend/src/app/(app)/layout.tsx @@ -15,6 +15,7 @@ import { PHProvider } from '@gitroom/react/helpers/posthog'; import UtmSaver from '@gitroom/helpers/utils/utm.saver'; import { DubAnalytics } from '@gitroom/frontend/components/layout/dubAnalytics'; import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.component'; +import { GoogleTagManagerComponent } from '@gitroom/frontend/components/layout/gtm.component'; import { cookies } from 'next/headers'; import { cookieName, @@ -96,6 +97,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { + diff --git a/apps/frontend/src/components/layout/gtm.component.tsx b/apps/frontend/src/components/layout/gtm.component.tsx new file mode 100644 index 00000000..900228e9 --- /dev/null +++ b/apps/frontend/src/components/layout/gtm.component.tsx @@ -0,0 +1,21 @@ +'use client'; + +import Script from 'next/script'; +import { FC } from 'react'; + +export const GoogleTagManagerComponent: FC<{ gtmId?: string }> = ({ + gtmId, +}) => { + if (!gtmId) { + return null; + } + return ( + + ); +}; diff --git a/apps/frontend/src/components/onboarding/onboarding.tsx b/apps/frontend/src/components/onboarding/onboarding.tsx index fe6d39c3..3e7da8f7 100644 --- a/apps/frontend/src/components/onboarding/onboarding.tsx +++ b/apps/frontend/src/components/onboarding/onboarding.tsx @@ -27,6 +27,17 @@ export const Onboarding: FC = () => { } return; } + if (typeof window !== 'undefined') { + const check = query.get('check') || 'unknown'; + const key = `gtm_start_trial_${check}`; + if (!sessionStorage.getItem(key)) { + sessionStorage.setItem(key, '1'); + // @ts-ignore + window.dataLayer = window.dataLayer || []; + // @ts-ignore + window.dataLayer.push({ event: 'start_trial', check }); + } + } if (modalOpen.current) { return; }