Skip to content

Commit 0ac1caa

Browse files
authored
Issue LIF-Initiative#853: Add GA4 analytics instrumentation to Advisor and MDR frontends (LIF-Initiative#875)
##### Description of Change Adds Google Analytics 4 (GA4) instrumentation to both the Advisor and MDR frontends to provide visibility into who is visiting each app and how features are being used. **What problem does this solve?** Neither frontend has any analytics — there is no visibility into visitor traffic or feature usage. **What is the solution?** - A shared `analytics.ts` utility per frontend that conditionally loads gtag.js when `VITE_GA_MEASUREMENT_ID` is set at build time - Core event tracking calls added to key user actions in each app - Dockerfile updates to accept the measurement ID as a build arg **Advisor events (5):** `login`, `login_failed`, `conversation_started`, `message_sent`, `logout` **MDR events (5 + auto page_view):** `login`, `login_failed`, `model_selected`, `search_performed`, `mapping_opened` **Side effects / limitations:** - No analytics traffic in local dev (env var unset = complete no-op) - GA4 account/property/data streams must be created manually before deploy - No PII is sent in any event parameter **How should reviewers test this?** - Set `VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX` in a `.env.local` file and run either frontend - Verify gtag script loads in the browser network tab - Verify events appear in GA4 DebugView (Realtime > DebugView) - Confirm no gtag script or network requests when the env var is unset ##### Related Issues Closes LIF-Initiative#853 Closes LIF-Initiative#867 ##### Type of Change - [x] New feature (non-breaking change which adds functionality) - [x] Infrastructure/deployment change ##### Project Area(s) Affected - [x] frontends/ --- ##### Checklist - [x] commit message follows commit guidelines (see commitlint.config.mjs) - [x] documentation is changed or added (in /docs directory) - [x] pre-commit hooks have been run successfully ##### Testing - [ ] Manual testing performed - [ ] Automated tests added/updated - [ ] Integration testing completed ##### Additional Notes **Remaining manual steps before deploy:** 1. Create GA4 account, property, and two web data streams (Advisor + MDR) 2. Add `GA_MEASUREMENT_ID` build arg to CI/CD or Docker build commands 3. Share GA4 property access (Unicon = Editor, client = Viewer) See `docs/ga4-implementation-notes.md` and `docs/proposals/ga-analytics-instrumentation.md` for the full design doc and implementation notes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 94c9e9e + d212787 commit 0ac1caa

File tree

18 files changed

+146
-4
lines changed

18 files changed

+146
-4
lines changed

.github/workflows/lif_advisor_app.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ jobs:
5757
ecr-repository: ${{ env.ECR_REPOSITORY }}
5858
build-args: |
5959
LIF_ADVISOR_API_URL=https://advisor-api.dev.lif.unicon.net
60+
GA_MEASUREMENT_ID=G-WCYGBK4XHK
6061
6162
- name: Update the lif-advisor-app ECS Service
6263
uses: ./.github/actions/update-cluster

.github/workflows/lif_mdr_frontend.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
run: |
4848
npm ci
4949
echo "VITE_API_URL=https://mdr-api.${{ env.ENV_NAME }}.${{ env.DOMAIN }}" > .env
50+
echo "VITE_GA_MEASUREMENT_ID=G-VZ515ZL70E" >> .env
5051
npm run build
5152
5253
- name: Sync to S3

frontends/lif_advisor_app/Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Stage 1: build the vite application
22
FROM node:20-alpine AS build
33

4-
# Accept a build-time argument for the API URL
4+
# Accept build-time arguments
55
ARG LIF_ADVISOR_API_URL
66
ENV LIF_ADVISOR_API_URL=${LIF_ADVISOR_API_URL}
7+
ARG GA_MEASUREMENT_ID
8+
ENV GA_MEASUREMENT_ID=${GA_MEASUREMENT_ID}
79

810
# Set working directory
911
WORKDIR /app
@@ -15,8 +17,9 @@ RUN apk update && apk upgrade && npm ci
1517
# Copy the rest of the application
1618
COPY . .
1719

18-
# Inject Vite environment variable and build
20+
# Inject Vite environment variables and build
1921
RUN echo "VITE_LIF_ADVISOR_API_URL=${LIF_ADVISOR_API_URL}" > .env && \
22+
echo "VITE_GA_MEASUREMENT_ID=${GA_MEASUREMENT_ID}" >> .env && \
2023
npm run build
2124

2225
# Stage 2: serve the built app with nginx

frontends/lif_advisor_app/src/components/ChatInterface.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import MessageInput from './MessageInput';
44
import { useChat } from '../hooks/useChat';
55
import { BarChart3, Search, LogOut } from 'lucide-react';
66
import { UserDetails } from '../types';
7+
import { trackLogout } from '../utils/analytics';
78

89
interface ChatInterfaceProps {
910
onLogout: () => void;
@@ -21,6 +22,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onLogout, user }) => {
2122
const handleLogout = async () => {
2223
if (isUserActionsDisabled) return;
2324
setLoggingOut(true);
25+
trackLogout();
2426
displayLoggingOutMessage();
2527
await onLogout();
2628
};

frontends/lif_advisor_app/src/components/LoginPanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import axios from 'axios';
33
import axiosInstance from '../utils/axios';
44
import { KeyRound, User } from 'lucide-react';
55
import { UserDetails } from '../types';
6+
import { trackLogin, trackLoginFailed } from '../utils/analytics';
67

78
interface LoginPanelProps {
89
onLoginSuccess: (user: UserDetails) => void;
@@ -40,10 +41,12 @@ const LoginPanel: React.FC<LoginPanelProps> = ({ onLoginSuccess }) => {
4041
if (response.data.success) {
4142
localStorage.setItem('token', response.data.access_token);
4243
localStorage.setItem('refreshToken', response.data.refresh_token);
44+
trackLogin('password');
4345
onLoginSuccess(response.data.user);
4446
}
4547
} catch (error) {
4648
if (!axios.isCancel(error)) {
49+
trackLoginFailed('password');
4750
setError('Invalid credentials. Please try again.');
4851
}
4952
} finally {

frontends/lif_advisor_app/src/hooks/useChat.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Message } from '../types';
33
import { generateId, delay } from '../utils/helpers';
44
import axios from 'axios';
55
import axiosInstance from '../utils/axios';
6+
import { trackEvent } from '../utils/analytics';
67

78
export const useChat = () => {
89
const [messages, setMessages] = useState<Message[]>([]);
@@ -81,6 +82,7 @@ export const useChat = () => {
8182
if (isMounted) {
8283
setIsTyping(false);
8384
setIsInitializing(false);
85+
trackEvent('conversation_started');
8486
}
8587
}
8688
};
@@ -104,6 +106,7 @@ export const useChat = () => {
104106
};
105107

106108
setMessages(prev => [...prev, userMessage]);
109+
trackEvent('message_sent');
107110

108111
const typingMessage: Message = {
109112
id: generateId(),

frontends/lif_advisor_app/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createRoot } from 'react-dom/client';
2+
import './utils/analytics';
23
import App from './App.tsx';
34
import './index.css';
45

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* GA4 analytics utility. All calls are no-ops when gtag is not loaded
3+
* (i.e., when VITE_GA_MEASUREMENT_ID is not set).
4+
*/
5+
6+
type GtagEventParams = Record<string, string | number | boolean>;
7+
8+
const MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID as string | undefined;
9+
10+
function loadGtag(): void {
11+
if (!MEASUREMENT_ID) return;
12+
13+
const script = document.createElement('script');
14+
script.async = true;
15+
script.src = `https://www.googletagmanager.com/gtag/js?id=${MEASUREMENT_ID}`;
16+
document.head.appendChild(script);
17+
18+
window.dataLayer = window.dataLayer || [];
19+
window.gtag = function gtag(...args: unknown[]) {
20+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21+
(window.dataLayer as any[]).push(args);
22+
};
23+
window.gtag('js', new Date());
24+
window.gtag('config', MEASUREMENT_ID, { anonymize_ip: true });
25+
}
26+
27+
// Initialize on module load
28+
loadGtag();
29+
30+
function gtag(...args: unknown[]): void {
31+
if (typeof window.gtag === 'function') {
32+
window.gtag(...args);
33+
}
34+
}
35+
36+
export function trackEvent(eventName: string, params?: GtagEventParams): void {
37+
gtag('event', eventName, params);
38+
}
39+
40+
export function trackLogin(method: string): void {
41+
trackEvent('login', { method });
42+
}
43+
44+
export function trackLoginFailed(method: string): void {
45+
trackEvent('login_failed', { method });
46+
}
47+
48+
export function trackLogout(): void {
49+
trackEvent('logout');
50+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
/// <reference types="vite/client" />
2+
3+
interface Window {
4+
gtag: (...args: unknown[]) => void;
5+
dataLayer: unknown[];
6+
}

frontends/mdr-frontend/Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Stage 1: build the vite application
22
FROM node:20-alpine AS build
33

4-
# Accept a build-time argument for the API URL
4+
# Accept build-time arguments
55
ARG LIF_MDR_API_URL
66
ENV LIF_MDR_API_URL=${LIF_MDR_API_URL}
7+
ARG GA_MEASUREMENT_ID
8+
ENV GA_MEASUREMENT_ID=${GA_MEASUREMENT_ID}
79

810
# Set working directory
911
WORKDIR /app
@@ -15,8 +17,9 @@ RUN apk update && apk upgrade && npm ci
1517
# Copy the rest of the application
1618
COPY . .
1719

18-
# Inject Vite environment variable and build
20+
# Inject Vite environment variables and build
1921
RUN echo "VITE_API_URL=${LIF_MDR_API_URL}" > .env && \
22+
echo "VITE_GA_MEASUREMENT_ID=${GA_MEASUREMENT_ID}" >> .env && \
2023
npm run build
2124

2225
# Stage 2: serve the built app with nginx

0 commit comments

Comments
 (0)