Skip to main content

Analytics & Push Notifications

CivicPulse integrates Google Analytics 4 (GA4) for usage analytics and Web Push Notifications so users can opt in to receive real-time civic alerts.


Google Analytics 4​

How It Works​

CivicPulse uses GA4 with the gtag.js library. The Measurement ID is injected at build time via a Vite environment variable so no secret is exposed (GA Measurement IDs are inherently public).

Events tracked automatically:

  • Page views on every route change
  • Report Issue button clicks
  • Issue category selections
  • Admin portal login events (no PII)

Setup​

1. Create a GA4 property at analytics.google.com and copy your Measurement ID (format: G-XXXXXXXXXX).

2. Set the environment variable:

# frontend/.env.production
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX

Leave this unset in .env.development to avoid polluting analytics with local traffic.

3. The frontend initialises GA automatically in src/analytics.js:

// Initialised once on app load when VITE_GA_MEASUREMENT_ID is set
export function initGA() {
const id = import.meta.env.VITE_GA_MEASUREMENT_ID;
if (!id) return;
// Loads gtag.js and configures GA4
window.dataLayer = window.dataLayer || [];
window.gtag = function () { dataLayer.push(arguments); };
gtag('js', new Date());
gtag('config', id, { anonymize_ip: true });
}

export function trackEvent(name, params = {}) {
if (typeof window.gtag !== 'function') return;
window.gtag('event', name, params);
}

Tracking Custom Events​

import { trackEvent } from './analytics';

// Track a "Report Issue" button click
trackEvent('report_issue_click', { category: 'Roads', city: 'Mumbai' });

// Track admin login
trackEvent('admin_login', { role: 'admin' });

Privacy Considerations​

  • anonymize_ip: true is set by default β€” IP addresses are anonymised before storage
  • No PII (names, emails, UUIDs) is sent to GA
  • Complies with Indian IT Act data-handling guidelines
  • Add a cookie banner if expanding to EU users (GDPR)

Web Push Notifications​

Citizens can opt in to receive browser push notifications for:

  • New issues filed in their city
  • Status updates on issues they've upvoted
  • Civic alerts and announcements from admins

Architecture​

Browser                       Backend
| |
|-- subscribe (VAPID) -------> POST /api/notifications/subscribe
| | (saves PushSubscription to DynamoDB)
| |
|<-- push event -------------- POST /api/notifications/send
| (via Web Push Protocol) | (triggered by admin or issue event)
| |
sw.js handles the event |
shows notification |

Environment Variables​

# backend/.env
VAPID_PUBLIC_KEY=<generate below>
VAPID_PRIVATE_KEY=<generate below>
VAPID_MAILTO=mailto:admin@civicpulse.in

Generate VAPID keys:

cd backend
node -e "const wp = require('web-push'); const keys = wp.generateVAPIDKeys(); console.log(keys);"

The public key must also be exposed to the frontend:

# frontend/.env.production
VITE_VAPID_PUBLIC_KEY=<same VAPID public key>

Service Worker (public/sw.js)​

The service worker handles incoming push events and displays notifications:

self.addEventListener('push', event => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title || 'CivicPulse', {
body: data.body || 'New civic update',
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url || '/' },
})
);
});

self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});

Subscribing Users​

// src/hooks/usePushNotifications.js
const { subscribe, unsubscribe, isSubscribed } = usePushNotifications();

// Ask user for permission and subscribe
await subscribe(); // registers SW, calls /api/notifications/subscribe

Backend API​

MethodPathAuthDescription
POST/api/notifications/subscribeUser JWTSave a push subscription
DELETE/api/notifications/unsubscribeUser JWTRemove subscription
POST/api/notifications/sendAdmin JWTBroadcast a push notification
POST/api/notifications/send-to-cityAdmin JWTSend to subscribers in a city

Sending Notifications from Admin​

From the admin portal (Notifications view):

  1. Enter a title and body message
  2. Optionally target a city
  3. Click Send notification

Alternatively, via API:

curl -X POST https://api.civicpulse.in/api/notifications/send \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "New water shortage report",
"body": "12 reports filed in Andheri West today",
"url": "/issues?city=Mumbai&category=Water"
}'

DynamoDB Table β€” civicpulse-push-subscriptions​

AttributeTypeDescription
endpointString (PK)Push endpoint URL (unique per browser/device)
userIdStringAssociated user ID (if logged in)
cityString (opt.)User's city preference for targeting
keysMap{ p256dh, auth } from PushSubscription
createdAtStringISO 8601 timestamp

Create via AWS CLI:

aws dynamodb create-table \
--table-name civicpulse-push-subscriptions \
--attribute-definitions AttributeName=endpoint,AttributeType=S \
--key-schema AttributeName=endpoint,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region ap-south-1

Browser Support​

BrowserSupport
Chrome (desktop + Android)βœ… Full
Firefoxβœ… Full
Edgeβœ… Full
Safari (macOS 13+, iOS 16.4+)βœ… Full (with limitations)
Samsung Internetβœ… Full

iOS note: Web Push on iOS requires Safari 16.4+ and the user must add the site to their Home Screen for notifications to work without opening the browser.