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;
}