Skip to content

feat: ga analytics support#25

Merged
rongquan1 merged 4 commits intomainfrom
feat/ga-analytics-support
Sep 12, 2025
Merged

feat: ga analytics support#25
rongquan1 merged 4 commits intomainfrom
feat/ga-analytics-support

Conversation

@Moiz47
Copy link
Contributor

@Moiz47 Moiz47 commented Sep 12, 2025

Summary

Added changes to support GA tracking using GTM

Summary by CodeRabbit

  • New Features

    • Integrated Google Tag Manager for app-wide analytics.
    • Added event tracking for Expand Preview and Document Status interactions (captures title, schema, and status).
  • Chores

    • CI updated to pass GTM ID during builds.
    • .env and .env.local added to .gitignore.
    • Added GTM packages and initialize analytics on app start.
    • Updated TypeScript config and added a GA event type.

@netlify
Copy link

netlify bot commented Sep 12, 2025

Deploy Preview for tradetrust-gallery ready!

Name Link
🔨 Latest commit e0996b1
🔍 Latest deploy log https://app.netlify.com/projects/tradetrust-gallery/deploys/68c3e46ff012d90008595dee
😎 Deploy Preview https://deploy-preview-25--tradetrust-gallery.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

Walkthrough

Adds Google Tag Manager integration: new GTM service and type, initializes on app startup, sends events from DocumentPreview and DocumentStatus, adds GTM deps, ignores .env files, passes VITE_GTM_ID in CI build env, and updates tsconfig for Vite types.

Changes

Cohort / File(s) Summary
CI environment
.github/workflows/ci.yml
Passes VITE_GTM_ID from GitHub Secrets into the Build step environment so the Vite build can access the GTM ID.
Ignore env files
.gitignore
Adds .env and .env.local to .gitignore.
Dependencies for GTM
package.json
Adds react-gtm-module and @types/react-gtm-module.
GTM service and types
src/lib/gtmService.ts, src/types/types.ts
Adds src/lib/gtmService.ts which imports vite env, initializes TagManager on import if VITE_GTM_ID is present, and exports sendGAEvent(eventData: GADataLayerEvent); adds GADataLayerEvent type.
App bootstrap
src/main.tsx
Imports the GTM service to trigger initialization during module load.
Component analytics hooks
src/components/content/DocumentPreview.tsx, src/components/content/DocumentStatus.tsx
Calls sendGAEvent on preview expand and on document status link click; adds id="qr-code-icon" to the QR image element.
TS config for Vite
tsconfig.json
Adds compilerOptions.types = ["vite/client"] for Vite type resolution.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant UI as React UI
  participant GTM as gtmService
  participant TM as TagManager

  Note over UI,GTM: App startup (module eval)
  UI->>GTM: import gtmService
  alt VITE_GTM_ID present
    GTM->>TM: initialize({ gtmId: VITE_GTM_ID })
  end

  Note over UI: User interactions
  U->>UI: Click "Expand Preview"
  UI->>GTM: sendGAEvent({ event: "document_preview", preview_title })
  alt GTM configured
    GTM->>TM: dataLayer({ dataLayer: eventData })
  end

  U->>UI: Click document status link
  UI->>GTM: sendGAEvent({ event: "document_type", document_title, document_schema, document_status })
  alt GTM configured
    GTM->>TM: dataLayer({ dataLayer: eventData })
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The current PR description contains only a one-line summary and does not follow the repository's required template; it is missing the Changes, Issues, and Releases sections and lacks crucial detail such as which files were modified, the introduction of the VITE_GTM_ID env var and CI change, added dependencies, and the GTM initialization behavior, so it is incomplete for reviewers. Please update the PR description to match the template: expand the Summary with background, add a Changes section that lists key file-level edits (for example src/lib/gtmService.ts initialization and sendGAEvent export, import in src/main.tsx, event calls in src/components/content/DocumentPreview.tsx and DocumentStatus.tsx, package.json dependency additions, tsconfig change, .gitignore entries, and the CI workflow change to pass VITE_GTM_ID), state related issue numbers or "None", and fill the Releases section with the target channel and ETA so reviewers and release coordinators have the necessary context.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: ga analytics support" is concise and directly reflects the primary change in the changeset—adding Google Analytics tracking via GTM (new gtmService, event calls, deps, and CI/env updates)—so it accurately summarizes the main intent and is clear for history scanning.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

I thump my paws—events take flight,
A tag is set, the data right.
I hop to preview, nudge a link,
Push to dataLayer in a blink.
Env files hidden, GTM awake—metrics bright. 🐇✨

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ga-analytics-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Moiz47 Moiz47 changed the title Feat/ga analytics support feat/ga analytics support Sep 12, 2025
@Moiz47 Moiz47 changed the title feat/ga analytics support feat: ga analytics support Sep 12, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (12)
.gitignore (1)

13-14: Broaden env ignores and keep a tracked sample.

Ignoring only .env and .env.local is easy to miss other variants. Recommend ignoring all env files and committing a .env.example.

-.env
-.env.local
+.env*
+!.env.example
.github/workflows/ci.yml (1)

24-25: Avoid injecting secrets on PR builds; inject only on pushes to main.

Prevents analytics IDs from being present in non-prod builds while keeping PR builds working.

-      - name: Build
-        run: npm run build
-        env:
-          VITE_GTM_ID: ${{ secrets.VITE_GTM_ID }}
+      - name: Build (PR)
+        if: ${{ github.event_name == 'pull_request' }}
+        run: npm run build
+
+      - name: Build (main)
+        if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+        run: npm run build
+        env:
+          VITE_GTM_ID: ${{ secrets.VITE_GTM_ID }}
package.json (1)

24-24: Move type-only package to devDependencies.

@types/react-gtm-module is compile-time only; keep prod deps lean.

   "dependencies": {
-    "@types/react-gtm-module": "^2.0.4",
     "react-gtm-module": "^2.0.11",
   },
   "devDependencies": {
+    "@types/react-gtm-module": "^2.0.4",
   }

Also applies to: 31-31

tsconfig.json (1)

14-16: Include Node types to avoid accidental loss due to explicit “types”.

Specifying types restricts ambient types. Add node if any Node globals/types are used (e.g., Buffer in polyfills).

-    "types": ["vite/client"]
+    "types": ["vite/client", "node"]
src/types/types.ts (1)

31-34: Loosen event payload value types for future-proofing.

GA/GTM dataLayer commonly carries numbers/booleans too.

 export type GADataLayerEvent = {
   event: string;
-} & Record<string, string>;
+} & Record<string, string | number | boolean>;
src/components/content/DocumentPreview.tsx (1)

5-5: Only emit analytics in production and lazy-load GTM.

Prevents dev/preview noise and avoids loading GTM when not needed.

-import { sendGAEvent } from '../../lib/gtmService';
+import { sendGAEvent } from '../../lib/gtmService';
@@
-          onClick={() => {
-            sendGAEvent({
+          onClick={() => {
+            sendGAEvent({
               event: 'document_preview',
               preview_title: name,
             });
             setExpandPreview(true);
           }}

Additionally, consider gating initialization in gtmService:

// src/lib/gtmService.ts
const gtmId = import.meta.env.VITE_GTM_ID;
if (import.meta.env.PROD && gtmId && typeof window !== 'undefined') {
  TagManager.initialize({ gtmId });
}

export const sendGAEvent = (eventData: GADataLayerEvent) => {
  if (import.meta.env.PROD && gtmId && typeof window !== 'undefined') {
    TagManager.dataLayer({ dataLayer: eventData });
  }
};

Also applies to: 21-27

src/main.tsx (1)

5-5: Conditionally import GTM in production.

Reduces bundle impact and avoids analytics in non-prod.

-import './lib/gtmService'; // Import triggers GTM initialization
+if (import.meta.env.PROD && import.meta.env.VITE_GTM_ID) {
+  import('./lib/gtmService'); // Lazy init GTM only in prod with ID
+}
src/components/content/DocumentStatus.tsx (1)

7-7: Track QR-tile clicks as well; verify no PII in event fields.

You’re tracking the link click; consider also capturing the QR tile interaction for parity. Please confirm title/schema carry no PII per your analytics policy.

Suggested QR-tile tracking:

-                      <div
+                      <div
                         className="inline-flex items-center justify-center p-2 rounded-xl border border-[#e2e3e7] cursor-pointer"
-                        onClick={() =>
-                          onStatusClick({
+                        onClick={() => {
+                          sendGAEvent({
+                            event: 'document_qr_click',
+                            document_title: title,
+                            document_schema: selectedSchema,
+                            document_status: status.label,
+                          });
+                          onStatusClick({
                             url: status.url[selectedSchema],
                             documentId: id,
                             label: status.label,
                             documentTitle: title,
-                          })
-                        }
+                          });
+                        }}
                       >

Also applies to: 34-34, 47-54

src/lib/gtmService.ts (4)

4-10: Avoid import-time side effects; gate by browser/prod and make init idempotent.

Prevents SSR/test crashes, accidental dev tracking, and duplicate script injection during HMR.

Apply:

-// Initialize GTM immediately when this module is imported
-const gtmId = import.meta.env.VITE_GTM_ID;
-if (gtmId) {
-  TagManager.initialize({
-    gtmId,
-  });
-}
+const gtmId = import.meta.env.VITE_GTM_ID as string | undefined;
+const isBrowser = typeof window !== 'undefined';
+const isProd = import.meta.env.PROD;
+let gtmInitialized = false;
+
+export const initGTM = () => {
+  if (!gtmInitialized && isBrowser && isProd && gtmId) {
+    TagManager.initialize({ gtmId });
+    gtmInitialized = true;
+  }
+};

Outside this file (example in src/main.tsx):

import { initGTM } from 'lib/gtmService';
initGTM();

12-18: Guard sends with init/browser checks and ensure event name is present.

Prevents no-op pushes before init and malformed events.

-export const sendGAEvent = (eventData: GADataLayerEvent) => {
-  if (gtmId) {
-    TagManager.dataLayer({
-      dataLayer: eventData,
-    });
-  }
-};
+export const sendGAEvent = (eventData: GADataLayerEvent) => {
+  if (!isBrowser || !gtmId) return;
+  if (!('event' in eventData) || !eventData.event) return;
+  // Optional: if you adopted initGTM above, also guard on gtmInitialized
+  // if (!gtmInitialized) return;
+  TagManager.dataLayer({ dataLayer: eventData });
+};

Additionally (in src/types/types.ts), future-proof GA payloads:

export type GADataLayerEvent = {
  event: string;
} & Record<string, string | number | boolean>;

1-18: Optional: validate GTM ID format to catch misconfig at startup.

Lightweight guard to fail fast in dev without affecting prod.

-const gtmId = import.meta.env.VITE_GTM_ID as string | undefined;
+const gtmId = import.meta.env.VITE_GTM_ID as string | undefined;
+const isValidGtmId = !gtmId || /^GTM-[A-Z0-9]+$/.test(gtmId);
+if (isBrowser && !isValidGtmId && import.meta.env.DEV) {
+  // eslint-disable-next-line no-console
+  console.warn('[GTM] Invalid VITE_GTM_ID:', gtmId);
+}

1-18: PII guardrails for events (doc only).

Confirm that event payloads exclude PII and align with your GA4 taxonomy; consider a small allowlist of keys if needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be67b67 and f080782.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • .github/workflows/ci.yml (1 hunks)
  • .gitignore (1 hunks)
  • package.json (1 hunks)
  • src/components/content/DocumentPreview.tsx (2 hunks)
  • src/components/content/DocumentStatus.tsx (3 hunks)
  • src/lib/gtmService.ts (1 hunks)
  • src/main.tsx (1 hunks)
  • src/types/types.ts (1 hunks)
  • tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/content/DocumentPreview.tsx (1)
src/lib/gtmService.ts (1)
  • sendGAEvent (12-18)
src/components/content/DocumentStatus.tsx (1)
src/lib/gtmService.ts (1)
  • sendGAEvent (12-18)
src/lib/gtmService.ts (1)
src/types/types.ts (1)
  • GADataLayerEvent (32-34)
🔇 Additional comments (1)
src/lib/gtmService.ts (1)

1-18: Overall approach LGTM.

Thin wrapper over react-gtm-module, environment-driven enablement, and a single entrypoint is a clean integration surface.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/lib/gtmService.ts (1)

2-2: Fix non-relative import: breaks Vite resolution

import { GADataLayerEvent } from 'types/types'; relies on TS baseUrl and will fail in Vite unless aliases/plugins are aligned. Switch to a relative import (simplest) or add matching aliases (vite + tsconfig).

-import { GADataLayerEvent } from 'types/types';
+import { GADataLayerEvent } from '../types/types';
🧹 Nitpick comments (4)
src/lib/gtmService.ts (4)

5-11: SSR safety and quieter prod logs

Initialize GTM only in browser contexts and avoid noisy prod logs.

 const gtmId = import.meta.env.VITE_GTM_ID;
-if (gtmId) {
-  console.log('Initializing GTM for analytics');
-  TagManager.initialize({
-    gtmId,
-  });
-}
+const isBrowser = typeof window !== 'undefined';
+if (isBrowser && gtmId) {
+  if (import.meta.env.DEV) console.debug('GTM: initializing');
+  TagManager.initialize({ gtmId });
+}

If the app ever runs under SSR (or tests that simulate it), this prevents “window is not defined.” Can you confirm whether SSR is in use?


13-19: Return success flag and guard dataLayer push

Make the API easier to consume and safer in non-browser contexts.

-export const sendGAEvent = (eventData: GADataLayerEvent) => {
-  if (gtmId) {
-    TagManager.dataLayer({
-      dataLayer: eventData,
-    });
-  }
-};
+export const sendGAEvent = (eventData: GADataLayerEvent): boolean => {
+  if (!isBrowser || !gtmId) return false;
+  try {
+    TagManager.dataLayer({ dataLayer: eventData });
+    return true;
+  } catch (err) {
+    if (import.meta.env.DEV) console.debug('GTM dataLayer push failed', err);
+    return false;
+  }
+};

5-11: Gate initialization on user consent (GDPR/CPRA) and env kill-switch

Initialize GTM only after user consent via your CMP, and consider a VITE_ENABLE_GTM=false kill-switch to disable tracking quickly across environments. This prevents accidental tracking without consent and eases ops rollbacks.


13-19: Event payload flexibility (cross-file: src/types/types.ts lines 31-33)

GADataLayerEvent restricts values to string; GA4 often uses number/boolean. Consider Record<string, string | number | boolean> to avoid unnecessary stringification.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f080782 and 42dbc31.

📒 Files selected for processing (1)
  • src/lib/gtmService.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/gtmService.ts (1)
src/types/types.ts (1)
  • GADataLayerEvent (32-34)

@Moiz47 Moiz47 requested a review from rongquan1 September 12, 2025 09:14
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/lib/gtmService.ts (5)

1-2: Use a type-only import for the event payload.

Slight perf/treeshake nit: import the type with import type to avoid emitting runtime code.

-import { GADataLayerEvent } from '../types/types';
+import type { GADataLayerEvent } from '../types/types';

4-11: Guard for SSR, initialize once, and keep logs dev-only.

Avoid window access on SSR/test runs and double-initialization on HMR; also silence prod logs.

 // Initialize GTM immediately when this module is imported
-const gtmId = import.meta.env.VITE_GTM_ID;
-if (gtmId) {
-  console.log('Initializing GTM for analytics');
-  TagManager.initialize({
-    gtmId,
-  });
-}
+const gtmId = import.meta.env.VITE_GTM_ID;
+const isClient = typeof window !== 'undefined';
+let initialized = false;
+if (isClient && gtmId && !initialized) {
+  if (import.meta.env.DEV) console.debug('[GTM] initialize');
+  TagManager.initialize({ gtmId });
+  initialized = true;
+}

Optional: export an initGTM() and call it from main.tsx to remove import side-effects entirely.


13-19: Validate payload and signal success/failure from sender.

Add a minimal runtime check and return a boolean for callers to branch on.

-export const sendGAEvent = (eventData: GADataLayerEvent) => {
-  if (gtmId) {
-    TagManager.dataLayer({
-      dataLayer: eventData,
-    });
-  }
-};
+export const sendGAEvent = (eventData: GADataLayerEvent): boolean => {
+  if (!isClient || !gtmId || !eventData?.event) return false;
+  TagManager.dataLayer({ dataLayer: eventData });
+  return true;
+};

13-19: Broaden GA payload value types (numbers/booleans are common).

Current type restricts values to string; GA/GTM events often carry numeric/boolean fields (e.g., value, debug_mode).

Apply in src/types/types.ts:

 export type GADataLayerEvent = {
   event: string;
-} & Record<string, string>;
+} & Record<string, string | number | boolean | null | undefined>;

4-11: Consent-aware initialization (if applicable).

If you have a CMP, defer initialize until consent is granted (and disable in jurisdictions where required) to avoid compliance issues.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42dbc31 and e0996b1.

📒 Files selected for processing (1)
  • src/lib/gtmService.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/gtmService.ts (1)
src/types/types.ts (1)
  • GADataLayerEvent (32-34)

@rongquan1 rongquan1 merged commit 03ac17f into main Sep 12, 2025
6 checks passed
@rongquan1 rongquan1 deleted the feat/ga-analytics-support branch September 12, 2025 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants