Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ END_SESSION_URL=https://keycloakdomain.com/auth/realms/keycloakrealm/protocol/op
REFRESH_TOKEN_URL=https://keycloakdomain.com/auth/realms/keycloakrealm/protocol/openid-connect/token

# # Backend System variables
NEXT_PUBLIC_BACKEND_URL= https://backendurl
BACKEND_URL= https://backendurl
NEXT_PUBLIC_BACKEND_URL= http://backendurl
BACKEND_URL= http://backendurl

NEXT_PUBLIC_BACKEND_GRAPHQL_URL=https://backendurl/api/graphql
BACKEND_GRAPHQL_URL=https://backendurl/api/graphql
NEXT_PUBLIC_BACKEND_GRAPHQL_URL=http://backendurl/api/graphql
BACKEND_GRAPHQL_URL=http://backendurl/api/graphql

NEXT_PUBLIC_ENABLE_ACCESSMODEL = 'false'
NEXT_PUBLIC_ANALYTICS_URL ='https://analyticsurl'
Expand All @@ -27,3 +27,11 @@ SENTRY_ORG_NAME='orgname'
SENTRY_PROJECT_NAME='projectname'
NEXT_PUBLIC_PLATFORM_URL = 'https://platformurl'

# Google Analytics
NEXT_PUBLIC_GA_ID='G-XXXXXXXXXX'

FEATURE_SITEMAPS='false'
FEATURE_SITEMAP_BACKEND_BASE_URL= http://backendurl/api
FEATURE_SITEMAP_ITEMS_PER_PAGE=5
FEATURE_SITEMAP_CACHE_DURATION=3600
FEATURE_SITEMAP_CHILD_CACHE_DURATION=21600
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from 'opub-ui';

import { GraphQL } from '@/lib/api';
import { useAnalytics } from '@/hooks/use-analytics';
import { Icons } from '@/components/icons';

const DetailsQuery: any = graphql(`
Expand Down Expand Up @@ -52,12 +53,20 @@ const DetailsQuery: any = graphql(`
const Details: React.FC = () => {
const params = useParams();
const chartRef = useRef<ReactECharts>(null);
const { trackDataset } = useAnalytics();

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

// Track dataset view when component mounts
useEffect(() => {
if (params.datasetIdentifier) {
trackDataset(params.datasetIdentifier as string);
}
}, [params.datasetIdentifier, trackDataset]);

const renderChart = (item: any) => {
if (item.chartType === 'ASSAM_DISTRICT' || item.chartType === 'ASSAM_RC') {
// Register the map
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import Image from 'next/image';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useEffect } from 'react';
import { graphql } from '@/gql';
import { TypeDataset, TypeUseCase } from '@/gql/generated/graphql';
import { useQuery } from '@tanstack/react-query';
import { Card, Text } from 'opub-ui';

import { GraphQLPublic } from '@/lib/api';
import { formatDate, generateJsonLd } from '@/lib/utils';
import { useAnalytics } from '@/hooks/use-analytics';
import BreadCrumbs from '@/components/BreadCrumbs';
import { Icons } from '@/components/icons';
import JsonLd from '@/components/JsonLd';
Expand Down Expand Up @@ -141,6 +143,7 @@ const UseCasedetails = graphql(`

const UseCaseDetailClient = () => {
const params = useParams();
const { trackUsecase } = useAnalytics();

const {
data: UseCaseDetails,
Expand All @@ -166,6 +169,14 @@ const UseCaseDetailClient = () => {
},
}
);

// Track usecase view when data is loaded
useEffect(() => {
if (UseCaseDetails?.useCase) {
trackUsecase(UseCaseDetails.useCase.id, UseCaseDetails.useCase.title || undefined);
}
}, [UseCaseDetails?.useCase, trackUsecase]);

const datasets = UseCaseDetails?.useCase?.datasets || []; // Fallback to an empty array

const hasSupportingOrganizations =
Expand Down
3 changes: 2 additions & 1 deletion app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
import { unstable_setRequestLocale } from 'next-intl/server';

import { siteConfig } from '@/config/site';
import Provider from '@/components/provider';
import GoogleAnalytics from '@/components/GoogleAnalytics';
import locales from '../../config/locales';

const fontSans = FontSans({ subsets: ['latin'], display: 'swap' });
Expand Down Expand Up @@ -80,6 +80,7 @@ export default async function LocaleLayout({
return (
<html lang={locale}>
<body className={fontSans.className}>
<GoogleAnalytics />
<NextIntlClientProvider locale={locale} messages={messages}>
<Provider>{children}</Provider>
</NextIntlClientProvider>
Expand Down
77 changes: 77 additions & 0 deletions components/GoogleAnalytics/GoogleAnalytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use client';

import { useEffect, Suspense } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import Script from 'next/script';
import { GA_TRACKING_ID, pageview, trackEvent } from '@/lib/gtag';

function GoogleAnalyticsInner() {
const pathname = usePathname();
const searchParams = useSearchParams();

useEffect(() => {
if (GA_TRACKING_ID && pathname) {
const url = pathname + (searchParams.toString() ? `?${searchParams.toString()}` : '');

// Track page view
pageview(url);

// Track additional page metadata
trackEvent('page_view_detailed', {
page_path: pathname,
page_location: url,
page_title: document.title,
// Extract route information
route_type: getRouteType(pathname),
locale: pathname.split('/')[1] || 'en',
});
}
}, [pathname, searchParams]);

return null;
}

export default function GoogleAnalytics() {
if (!GA_TRACKING_ID) {
return null;
}

return (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_TRACKING_ID}', {
page_path: window.location.pathname,
send_page_view: false, // We handle page views manually
});
`,
}}
/>
<Suspense fallback={null}>
<GoogleAnalyticsInner />
</Suspense>
</>
);
}

// Helper function to categorize routes
function getRouteType(pathname: string): string {
if (pathname.includes('/datasets/')) return 'dataset_detail';
if (pathname.includes('/datasets')) return 'dataset_list';
if (pathname.includes('/usecases/')) return 'usecase_detail';
if (pathname.includes('/usecases')) return 'usecase_list';
if (pathname.includes('/dashboard')) return 'dashboard';
if (pathname.includes('/login')) return 'auth';
if (pathname === '/' || pathname.match(/^\/[a-z]{2}$/)) return 'home';
return 'other';
}
1 change: 1 addition & 0 deletions components/GoogleAnalytics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './GoogleAnalytics';
150 changes: 150 additions & 0 deletions docs/GOOGLE_ANALYTICS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Google Analytics Integration

This document describes how Google Analytics 4 (GA4) has been integrated into the DataSpace Frontend application.

## Setup

### 1. Environment Configuration

Add your Google Analytics tracking ID to your environment variables:

```bash
# .env.local
NEXT_PUBLIC_GA_ID='G-XXXXXXXXXX'
```

Replace `G-XXXXXXXXXX` with your actual Google Analytics 4 tracking ID.

### 2. Files Added/Modified

- `lib/gtag.ts` - Core Google Analytics utilities and tracking functions
- `components/GoogleAnalytics/` - React component for GA initialization
- `hooks/use-analytics.ts` - Custom hook for easy analytics tracking
- `types/index.d.ts` - TypeScript definitions for gtag
- `env.ts` - Environment variable validation
- `app/[locale]/layout.tsx` - GA component integration

## Usage

### Basic Tracking

The Google Analytics component is automatically loaded in the main layout and will track page views automatically.

### Custom Event Tracking

Use the `useAnalytics` hook for custom event tracking:

```tsx
import { useAnalytics } from '@/hooks/use-analytics';

const MyComponent = () => {
const { track, trackDataset, trackUsecase, trackSearch } = useAnalytics();

const handleClick = () => {
track('button_click', { button_name: 'download' });
};

const handleDatasetView = (datasetId: string, title?: string) => {
trackDataset(datasetId, title);
};

// ... rest of component
};
```

### Available Tracking Functions

#### From `lib/gtag.ts`:
- `pageview(url)` - Track page views
- `event({ action, category, label, value })` - Generic event tracking
- `trackEvent(eventName, parameters)` - Custom event tracking
- `trackPageView(pagePath, pageTitle)` - Page view tracking
- `trackUserInteraction(action, element)` - User interaction tracking
- `trackDatasetView(datasetId, datasetTitle)` - Dataset view tracking
- `trackUsecaseView(usecaseId, usecaseTitle)` - Usecase view tracking
- `trackSearch(query, resultCount)` - Search query tracking
- `trackDownload(fileName, fileType)` - File download tracking

#### From `useAnalytics` hook:
- `track(eventName, parameters)` - Generic event tracking
- `trackPage(pagePath, pageTitle)` - Page view tracking
- `trackInteraction(action, element)` - User interaction tracking
- `trackDataset(datasetId, datasetTitle)` - Dataset view tracking
- `trackUsecase(usecaseId, usecaseTitle)` - Usecase view tracking
- `trackSearchQuery(query, resultCount)` - Search query tracking
- `trackFileDownload(fileName, fileType)` - File download tracking

## Examples

### Track Dataset Views
```tsx
useEffect(() => {
if (datasetId) {
trackDataset(datasetId, datasetTitle);
}
}, [datasetId, datasetTitle, trackDataset]);
```

### Track Search Queries
```tsx
const handleSearch = (query: string, results: any[]) => {
trackSearchQuery(query, results.length);
};
```

### Track File Downloads
```tsx
const handleDownload = (fileName: string) => {
trackFileDownload(fileName, fileName.split('.').pop());
};
```

### Track User Interactions
```tsx
const handleButtonClick = () => {
trackInteraction('click', 'export_button');
};
```

## Implementation Details

### Automatic Page Tracking

The `GoogleAnalytics` component automatically tracks page views using Next.js router events. It's integrated into the main layout at `app/[locale]/layout.tsx`.

### Privacy Considerations

- Google Analytics only loads when `NEXT_PUBLIC_GA_ID` is provided
- All tracking functions check for the presence of `window.gtag` before executing
- The implementation follows Google's recommended practices for GA4

### TypeScript Support

Full TypeScript support is provided with proper type definitions for the `gtag` function and all tracking utilities.

## Testing

To test the implementation:

1. Set up a Google Analytics 4 property
2. Add the tracking ID to your `.env.local` file
3. Run the application in development mode
4. Open browser developer tools and check the Network tab for GA requests
5. Use Google Analytics Real-time reports to verify events are being tracked

## Troubleshooting

### GA not loading
- Verify `NEXT_PUBLIC_GA_ID` is set correctly
- Check browser console for any JavaScript errors
- Ensure ad blockers are not interfering

### Events not tracking
- Verify the `gtag` function is available (`window.gtag`)
- Check that events are being called after GA initialization
- Use browser developer tools to inspect network requests to Google Analytics

### Development vs Production
- GA tracking works in both development and production
- Use different GA properties for development and production environments
- Consider using Google Analytics Debug mode for development testing
8 changes: 6 additions & 2 deletions env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export const env = createEnv({
END_SESSION_URL: z.string().url(),
REFRESH_TOKEN_URL: z.string().url(),
},
client: {},
client: {
NEXT_PUBLIC_GA_ID: z.string().optional(),
},

experimental__runtimeEnv: {},
experimental__runtimeEnv: {
NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
},
});
Loading