Skip to content

Commit a6d5bf9

Browse files
Merge pull request #325 from CivicDataLab/dev
GA integration
2 parents 87aa057 + 356ad78 commit a6d5bf9

File tree

13 files changed

+483
-7
lines changed

13 files changed

+483
-7
lines changed

.env.local.example

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ END_SESSION_URL=https://keycloakdomain.com/auth/realms/keycloakrealm/protocol/op
88
REFRESH_TOKEN_URL=https://keycloakdomain.com/auth/realms/keycloakrealm/protocol/openid-connect/token
99

1010
# # Backend System variables
11-
NEXT_PUBLIC_BACKEND_URL= https://backendurl
12-
BACKEND_URL= https://backendurl
11+
NEXT_PUBLIC_BACKEND_URL= http://backendurl
12+
BACKEND_URL= http://backendurl
1313

14-
NEXT_PUBLIC_BACKEND_GRAPHQL_URL=https://backendurl/api/graphql
15-
BACKEND_GRAPHQL_URL=https://backendurl/api/graphql
14+
NEXT_PUBLIC_BACKEND_GRAPHQL_URL=http://backendurl/api/graphql
15+
BACKEND_GRAPHQL_URL=http://backendurl/api/graphql
1616

1717
NEXT_PUBLIC_ENABLE_ACCESSMODEL = 'false'
1818
NEXT_PUBLIC_ANALYTICS_URL ='https://analyticsurl'
@@ -27,3 +27,11 @@ SENTRY_ORG_NAME='orgname'
2727
SENTRY_PROJECT_NAME='projectname'
2828
NEXT_PUBLIC_PLATFORM_URL = 'https://platformurl'
2929

30+
# Google Analytics
31+
NEXT_PUBLIC_GA_ID='G-XXXXXXXXXX'
32+
33+
FEATURE_SITEMAPS='false'
34+
FEATURE_SITEMAP_BACKEND_BASE_URL= http://backendurl/api
35+
FEATURE_SITEMAP_ITEMS_PER_PAGE=5
36+
FEATURE_SITEMAP_CACHE_DURATION=3600
37+
FEATURE_SITEMAP_CHILD_CACHE_DURATION=21600

app/[locale]/(user)/datasets/[datasetIdentifier]/components/Details/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'opub-ui';
2121

2222
import { GraphQL } from '@/lib/api';
23+
import { useAnalytics } from '@/hooks/use-analytics';
2324
import { Icons } from '@/components/icons';
2425

2526
const DetailsQuery: any = graphql(`
@@ -52,12 +53,20 @@ const DetailsQuery: any = graphql(`
5253
const Details: React.FC = () => {
5354
const params = useParams();
5455
const chartRef = useRef<ReactECharts>(null);
56+
const { trackDataset } = useAnalytics();
5557

5658
const { data, isLoading }: { data: any; isLoading: any } = useQuery(
5759
[`chartDetails_${params.id}`],
5860
() => GraphQL(DetailsQuery, {}, { datasetId: params.datasetIdentifier })
5961
);
6062

63+
// Track dataset view when component mounts
64+
useEffect(() => {
65+
if (params.datasetIdentifier) {
66+
trackDataset(params.datasetIdentifier as string);
67+
}
68+
}, [params.datasetIdentifier, trackDataset]);
69+
6170
const renderChart = (item: any) => {
6271
if (item.chartType === 'ASSAM_DISTRICT' || item.chartType === 'ASSAM_RC') {
6372
// Register the map

app/[locale]/(user)/usecases/[useCaseSlug]/UsecaseDetailsClient.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import Image from 'next/image';
44
import Link from 'next/link';
55
import { useParams } from 'next/navigation';
6+
import { useEffect } from 'react';
67
import { graphql } from '@/gql';
78
import { TypeDataset, TypeUseCase } from '@/gql/generated/graphql';
89
import { useQuery } from '@tanstack/react-query';
910
import { Card, Text } from 'opub-ui';
1011

1112
import { GraphQLPublic } from '@/lib/api';
1213
import { formatDate, generateJsonLd } from '@/lib/utils';
14+
import { useAnalytics } from '@/hooks/use-analytics';
1315
import BreadCrumbs from '@/components/BreadCrumbs';
1416
import { Icons } from '@/components/icons';
1517
import JsonLd from '@/components/JsonLd';
@@ -141,6 +143,7 @@ const UseCasedetails = graphql(`
141143

142144
const UseCaseDetailClient = () => {
143145
const params = useParams();
146+
const { trackUsecase } = useAnalytics();
144147

145148
const {
146149
data: UseCaseDetails,
@@ -166,6 +169,14 @@ const UseCaseDetailClient = () => {
166169
},
167170
}
168171
);
172+
173+
// Track usecase view when data is loaded
174+
useEffect(() => {
175+
if (UseCaseDetails?.useCase) {
176+
trackUsecase(UseCaseDetails.useCase.id, UseCaseDetails.useCase.title || undefined);
177+
}
178+
}, [UseCaseDetails?.useCase, trackUsecase]);
179+
169180
const datasets = UseCaseDetails?.useCase?.datasets || []; // Fallback to an empty array
170181

171182
const hasSupportingOrganizations =

app/[locale]/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { notFound } from 'next/navigation';
44
import { NextIntlClientProvider } from 'next-intl';
55
import { unstable_setRequestLocale } from 'next-intl/server';
66

7-
import { siteConfig } from '@/config/site';
87
import Provider from '@/components/provider';
8+
import GoogleAnalytics from '@/components/GoogleAnalytics';
99
import locales from '../../config/locales';
1010

1111
const fontSans = FontSans({ subsets: ['latin'], display: 'swap' });
@@ -80,6 +80,7 @@ export default async function LocaleLayout({
8080
return (
8181
<html lang={locale}>
8282
<body className={fontSans.className}>
83+
<GoogleAnalytics />
8384
<NextIntlClientProvider locale={locale} messages={messages}>
8485
<Provider>{children}</Provider>
8586
</NextIntlClientProvider>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use client';
2+
3+
import { useEffect, Suspense } from 'react';
4+
import { usePathname, useSearchParams } from 'next/navigation';
5+
import Script from 'next/script';
6+
import { GA_TRACKING_ID, pageview, trackEvent } from '@/lib/gtag';
7+
8+
function GoogleAnalyticsInner() {
9+
const pathname = usePathname();
10+
const searchParams = useSearchParams();
11+
12+
useEffect(() => {
13+
if (GA_TRACKING_ID && pathname) {
14+
const url = pathname + (searchParams.toString() ? `?${searchParams.toString()}` : '');
15+
16+
// Track page view
17+
pageview(url);
18+
19+
// Track additional page metadata
20+
trackEvent('page_view_detailed', {
21+
page_path: pathname,
22+
page_location: url,
23+
page_title: document.title,
24+
// Extract route information
25+
route_type: getRouteType(pathname),
26+
locale: pathname.split('/')[1] || 'en',
27+
});
28+
}
29+
}, [pathname, searchParams]);
30+
31+
return null;
32+
}
33+
34+
export default function GoogleAnalytics() {
35+
if (!GA_TRACKING_ID) {
36+
return null;
37+
}
38+
39+
return (
40+
<>
41+
<Script
42+
strategy="afterInteractive"
43+
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
44+
/>
45+
<Script
46+
id="gtag-init"
47+
strategy="afterInteractive"
48+
dangerouslySetInnerHTML={{
49+
__html: `
50+
window.dataLayer = window.dataLayer || [];
51+
function gtag(){dataLayer.push(arguments);}
52+
gtag('js', new Date());
53+
gtag('config', '${GA_TRACKING_ID}', {
54+
page_path: window.location.pathname,
55+
send_page_view: false, // We handle page views manually
56+
});
57+
`,
58+
}}
59+
/>
60+
<Suspense fallback={null}>
61+
<GoogleAnalyticsInner />
62+
</Suspense>
63+
</>
64+
);
65+
}
66+
67+
// Helper function to categorize routes
68+
function getRouteType(pathname: string): string {
69+
if (pathname.includes('/datasets/')) return 'dataset_detail';
70+
if (pathname.includes('/datasets')) return 'dataset_list';
71+
if (pathname.includes('/usecases/')) return 'usecase_detail';
72+
if (pathname.includes('/usecases')) return 'usecase_list';
73+
if (pathname.includes('/dashboard')) return 'dashboard';
74+
if (pathname.includes('/login')) return 'auth';
75+
if (pathname === '/' || pathname.match(/^\/[a-z]{2}$/)) return 'home';
76+
return 'other';
77+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './GoogleAnalytics';

docs/GOOGLE_ANALYTICS.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Google Analytics Integration
2+
3+
This document describes how Google Analytics 4 (GA4) has been integrated into the DataSpace Frontend application.
4+
5+
## Setup
6+
7+
### 1. Environment Configuration
8+
9+
Add your Google Analytics tracking ID to your environment variables:
10+
11+
```bash
12+
# .env.local
13+
NEXT_PUBLIC_GA_ID='G-XXXXXXXXXX'
14+
```
15+
16+
Replace `G-XXXXXXXXXX` with your actual Google Analytics 4 tracking ID.
17+
18+
### 2. Files Added/Modified
19+
20+
- `lib/gtag.ts` - Core Google Analytics utilities and tracking functions
21+
- `components/GoogleAnalytics/` - React component for GA initialization
22+
- `hooks/use-analytics.ts` - Custom hook for easy analytics tracking
23+
- `types/index.d.ts` - TypeScript definitions for gtag
24+
- `env.ts` - Environment variable validation
25+
- `app/[locale]/layout.tsx` - GA component integration
26+
27+
## Usage
28+
29+
### Basic Tracking
30+
31+
The Google Analytics component is automatically loaded in the main layout and will track page views automatically.
32+
33+
### Custom Event Tracking
34+
35+
Use the `useAnalytics` hook for custom event tracking:
36+
37+
```tsx
38+
import { useAnalytics } from '@/hooks/use-analytics';
39+
40+
const MyComponent = () => {
41+
const { track, trackDataset, trackUsecase, trackSearch } = useAnalytics();
42+
43+
const handleClick = () => {
44+
track('button_click', { button_name: 'download' });
45+
};
46+
47+
const handleDatasetView = (datasetId: string, title?: string) => {
48+
trackDataset(datasetId, title);
49+
};
50+
51+
// ... rest of component
52+
};
53+
```
54+
55+
### Available Tracking Functions
56+
57+
#### From `lib/gtag.ts`:
58+
- `pageview(url)` - Track page views
59+
- `event({ action, category, label, value })` - Generic event tracking
60+
- `trackEvent(eventName, parameters)` - Custom event tracking
61+
- `trackPageView(pagePath, pageTitle)` - Page view tracking
62+
- `trackUserInteraction(action, element)` - User interaction tracking
63+
- `trackDatasetView(datasetId, datasetTitle)` - Dataset view tracking
64+
- `trackUsecaseView(usecaseId, usecaseTitle)` - Usecase view tracking
65+
- `trackSearch(query, resultCount)` - Search query tracking
66+
- `trackDownload(fileName, fileType)` - File download tracking
67+
68+
#### From `useAnalytics` hook:
69+
- `track(eventName, parameters)` - Generic event tracking
70+
- `trackPage(pagePath, pageTitle)` - Page view tracking
71+
- `trackInteraction(action, element)` - User interaction tracking
72+
- `trackDataset(datasetId, datasetTitle)` - Dataset view tracking
73+
- `trackUsecase(usecaseId, usecaseTitle)` - Usecase view tracking
74+
- `trackSearchQuery(query, resultCount)` - Search query tracking
75+
- `trackFileDownload(fileName, fileType)` - File download tracking
76+
77+
## Examples
78+
79+
### Track Dataset Views
80+
```tsx
81+
useEffect(() => {
82+
if (datasetId) {
83+
trackDataset(datasetId, datasetTitle);
84+
}
85+
}, [datasetId, datasetTitle, trackDataset]);
86+
```
87+
88+
### Track Search Queries
89+
```tsx
90+
const handleSearch = (query: string, results: any[]) => {
91+
trackSearchQuery(query, results.length);
92+
};
93+
```
94+
95+
### Track File Downloads
96+
```tsx
97+
const handleDownload = (fileName: string) => {
98+
trackFileDownload(fileName, fileName.split('.').pop());
99+
};
100+
```
101+
102+
### Track User Interactions
103+
```tsx
104+
const handleButtonClick = () => {
105+
trackInteraction('click', 'export_button');
106+
};
107+
```
108+
109+
## Implementation Details
110+
111+
### Automatic Page Tracking
112+
113+
The `GoogleAnalytics` component automatically tracks page views using Next.js router events. It's integrated into the main layout at `app/[locale]/layout.tsx`.
114+
115+
### Privacy Considerations
116+
117+
- Google Analytics only loads when `NEXT_PUBLIC_GA_ID` is provided
118+
- All tracking functions check for the presence of `window.gtag` before executing
119+
- The implementation follows Google's recommended practices for GA4
120+
121+
### TypeScript Support
122+
123+
Full TypeScript support is provided with proper type definitions for the `gtag` function and all tracking utilities.
124+
125+
## Testing
126+
127+
To test the implementation:
128+
129+
1. Set up a Google Analytics 4 property
130+
2. Add the tracking ID to your `.env.local` file
131+
3. Run the application in development mode
132+
4. Open browser developer tools and check the Network tab for GA requests
133+
5. Use Google Analytics Real-time reports to verify events are being tracked
134+
135+
## Troubleshooting
136+
137+
### GA not loading
138+
- Verify `NEXT_PUBLIC_GA_ID` is set correctly
139+
- Check browser console for any JavaScript errors
140+
- Ensure ad blockers are not interfering
141+
142+
### Events not tracking
143+
- Verify the `gtag` function is available (`window.gtag`)
144+
- Check that events are being called after GA initialization
145+
- Use browser developer tools to inspect network requests to Google Analytics
146+
147+
### Development vs Production
148+
- GA tracking works in both development and production
149+
- Use different GA properties for development and production environments
150+
- Consider using Google Analytics Debug mode for development testing

env.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ export const env = createEnv({
1111
END_SESSION_URL: z.string().url(),
1212
REFRESH_TOKEN_URL: z.string().url(),
1313
},
14-
client: {},
14+
client: {
15+
NEXT_PUBLIC_GA_ID: z.string().optional(),
16+
},
1517

16-
experimental__runtimeEnv: {},
18+
experimental__runtimeEnv: {
19+
NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
20+
},
1721
});

0 commit comments

Comments
 (0)