You can track user activity in a Replit-hosted web app by adding lightweight event logging to your frontend, storing events in Replit's built-in PostgreSQL database, and querying them through a simple analytics API. This tutorial covers setting up a logging endpoint, capturing clicks and page views, storing events with timestamps, and viewing aggregated data — all without third-party analytics services.
Track User Activity in Your Replit Web App with Event Logging
Understanding how users interact with your application is essential for improving the experience. Instead of integrating heavyweight analytics platforms, you can build a simple event logging system directly in your Replit app. This tutorial walks you through creating a backend endpoint that records user actions, a frontend utility that sends events automatically, and a database table that stores everything for later analysis.
Prerequisites
- A Replit account on Core or Pro plan (for PostgreSQL access)
- A running web application with an Express backend and React frontend
- Basic familiarity with SQL and JavaScript
- PostgreSQL database enabled in your Replit App (Cloud tab -> Database)
Step-by-step guide
Create the events table in PostgreSQL
Create the events table in PostgreSQL
Open your Replit App's database by clicking the Cloud tab (the plus icon next to Preview), then selecting Database. Use the SQL runner or Drizzle Studio to create a table that stores event data. Each row captures the event type (page_view, click, form_submit), the page or element involved, a session identifier, and a timestamp. The session_id helps group events from the same user visit without requiring authentication.
1CREATE TABLE IF NOT EXISTS user_events (2 id SERIAL PRIMARY KEY,3 event_type VARCHAR(50) NOT NULL,4 page VARCHAR(255),5 element VARCHAR(255),6 session_id VARCHAR(100),7 metadata JSONB DEFAULT '{}',8 created_at TIMESTAMP DEFAULT NOW()9);1011CREATE INDEX idx_events_type ON user_events(event_type);12CREATE INDEX idx_events_created ON user_events(created_at);Expected result: The user_events table is created in your PostgreSQL database with indexes for fast querying.
Build the logging API endpoint
Build the logging API endpoint
Add a POST endpoint to your Express server that receives event data from the frontend and inserts it into the database. Use parameterized queries to prevent SQL injection. The endpoint should validate that event_type is present and return a 201 status on success. Keep the endpoint lightweight with no authentication requirement so it does not slow down the user experience. Rate limiting is optional but recommended for production apps to prevent abuse.
1// server/routes/analytics.js2import { Router } from 'express';3import pg from 'pg';45const router = Router();6const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });78router.post('/api/events', async (req, res) => {9 const { event_type, page, element, session_id, metadata } = req.body;1011 if (!event_type) {12 return res.status(400).json({ error: 'event_type is required' });13 }1415 try {16 await pool.query(17 `INSERT INTO user_events (event_type, page, element, session_id, metadata)18 VALUES ($1, $2, $3, $4, $5)`,19 [event_type, page, element, session_id, JSON.stringify(metadata || {})]20 );21 res.status(201).json({ success: true });22 } catch (err) {23 console.error('Event logging error:', err.message);24 res.status(500).json({ error: 'Failed to log event' });25 }26});2728export default router;Expected result: POST requests to /api/events insert a row into user_events and return a 201 status.
Create a frontend tracking utility
Create a frontend tracking utility
Create a small JavaScript module that the frontend imports to send events. Generate a unique session ID on page load using crypto.randomUUID() and include it with every event. The trackEvent function sends a non-blocking fetch request to your logging endpoint. Use navigator.sendBeacon for page unload events to ensure the request completes even if the user navigates away. Wrap everything in a try-catch so tracking errors never break the application.
1// src/utils/tracker.ts2const SESSION_ID = crypto.randomUUID();3const API_URL = '/api/events';45export function trackEvent(6 eventType: string,7 element?: string,8 metadata?: Record<string, unknown>9) {10 try {11 const payload = {12 event_type: eventType,13 page: window.location.pathname,14 element,15 session_id: SESSION_ID,16 metadata17 };1819 fetch(API_URL, {20 method: 'POST',21 headers: { 'Content-Type': 'application/json' },22 body: JSON.stringify(payload)23 }).catch(() => {}); // Silently ignore tracking failures24 } catch {25 // Never let tracking break the app26 }27}2829export function trackPageView() {30 trackEvent('page_view');31}Expected result: Importing and calling trackEvent or trackPageView sends event data to your API without affecting app performance.
Add automatic page view tracking to your React app
Add automatic page view tracking to your React app
Import the trackPageView function into your main App component or layout and call it inside a useEffect hook. If you use React Router, listen for route changes to track every page navigation, not just the initial load. This gives you a complete picture of which pages users visit and in what order, with zero manual effort after the initial setup.
1// src/App.tsx2import { useEffect } from 'react';3import { useLocation } from 'react-router-dom';4import { trackPageView } from './utils/tracker';56function App() {7 const location = useLocation();89 useEffect(() => {10 trackPageView();11 }, [location.pathname]);1213 return (14 // ... your routes and layout15 );16}Expected result: Every page navigation in your app automatically sends a page_view event to your database.
Add click tracking to key UI elements
Add click tracking to key UI elements
For important actions like button clicks, form submissions, and link clicks, call trackEvent directly in your event handlers. Pass a descriptive element name and any relevant metadata. Focus on tracking actions that matter for your business — sign-up clicks, purchase buttons, feature usage — rather than every single click. Over-tracking creates noise that makes the data harder to analyze.
1// Example: tracking a button click2import { trackEvent } from '../utils/tracker';34function SignUpButton() {5 const handleClick = () => {6 trackEvent('click', 'signup_button', { plan: 'pro' });7 // ... proceed with sign-up logic8 };910 return <button onClick={handleClick}>Sign Up</button>;11}Expected result: Clicking tracked UI elements sends click events with element names and metadata to your database.
Build a simple analytics query endpoint
Build a simple analytics query endpoint
Add a GET endpoint that returns aggregated event data so you can see how users interact with your app. Group events by type and count them, or filter by date range. Protect this endpoint with a simple secret key check so only you can access the analytics data. Query the endpoint from Shell using curl or build a simple admin page.
1// GET /api/analytics?days=72router.get('/api/analytics', async (req, res) => {3 const adminKey = req.headers['x-admin-key'];4 if (adminKey !== process.env.ANALYTICS_ADMIN_KEY) {5 return res.status(403).json({ error: 'Unauthorized' });6 }78 const days = parseInt(req.query.days) || 7;910 try {11 const result = await pool.query(12 `SELECT event_type, COUNT(*) as count,13 COUNT(DISTINCT session_id) as unique_sessions14 FROM user_events15 WHERE created_at > NOW() - INTERVAL '1 day' * $116 GROUP BY event_type17 ORDER BY count DESC`,18 [days]19 );20 res.json({ period_days: days, events: result.rows });21 } catch (err) {22 console.error('Analytics query error:', err.message);23 res.status(500).json({ error: 'Query failed' });24 }25});Expected result: Calling GET /api/analytics?days=7 with the correct admin key returns aggregated event counts grouped by type.
Complete working example
1// src/utils/tracker.ts — Lightweight user interaction tracker2// Sends events to your backend without blocking the UI34const SESSION_ID = crypto.randomUUID();5const API_URL = '/api/events';67interface TrackingMetadata {8 [key: string]: string | number | boolean | null;9}1011export function trackEvent(12 eventType: string,13 element?: string,14 metadata?: TrackingMetadata15): void {16 try {17 const payload = {18 event_type: eventType,19 page: window.location.pathname,20 element: element || null,21 session_id: SESSION_ID,22 metadata: metadata || {}23 };2425 // Fire and forget — never await this26 fetch(API_URL, {27 method: 'POST',28 headers: { 'Content-Type': 'application/json' },29 body: JSON.stringify(payload)30 }).catch(() => {31 // Silently ignore tracking failures32 });33 } catch {34 // Never let tracking break the application35 }36}3738export function trackPageView(): void {39 trackEvent('page_view');40}4142export function trackClick(element: string, metadata?: TrackingMetadata): void {43 trackEvent('click', element, metadata);44}4546export function trackFormSubmit(formName: string, metadata?: TrackingMetadata): void {47 trackEvent('form_submit', formName, metadata);48}4950// Use sendBeacon for events that must fire on page unload51export function trackBeforeUnload(): void {52 window.addEventListener('beforeunload', () => {53 const payload = JSON.stringify({54 event_type: 'session_end',55 page: window.location.pathname,56 session_id: SESSION_ID,57 metadata: {}58 });59 navigator.sendBeacon(API_URL, payload);60 });61}Common mistakes when tracking user activity in Replit apps
Why it's a problem: Awaiting the tracking fetch call, which adds network latency to every user interaction
How to avoid: Call fetch without await and add .catch(() => {}) to silently handle failures. Tracking should be non-blocking.
Why it's a problem: Logging personally identifiable information (email, name, IP address) in the metadata field without user consent
How to avoid: Only log anonymous behavioral data (event type, page, element, session ID). If you need PII, add a privacy policy and consent mechanism first.
Why it's a problem: Not adding database indexes, causing analytics queries to slow down as the events table grows
How to avoid: Create indexes on event_type and created_at when you create the table. For large datasets, consider partitioning by date.
Best practices
- Never let tracking code crash your application — wrap all tracking calls in try-catch and silently ignore failures
- Use fire-and-forget fetch calls (do not await) so tracking adds zero latency to user interactions
- Store analytics admin keys in Replit Secrets, not in code, and protect analytics endpoints with authentication
- Track meaningful business events (sign-ups, purchases, feature usage) rather than every mouse click
- Use consistent snake_case naming for event types and element names to simplify querying
- Include a session_id with every event so you can reconstruct user journeys without requiring login
- Add database indexes on event_type and created_at columns for fast aggregation queries
- Respect user privacy: do not log personally identifiable information unless you have explicit consent and a privacy policy
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a React + Express app hosted on Replit with a PostgreSQL database. Help me build a lightweight user analytics system that tracks page views and button clicks. I need a database schema, an Express API endpoint for logging events, a React tracking utility, and a query endpoint for viewing aggregated data.
Add a user interaction tracking system to my app. Create a user_events table in my PostgreSQL database with columns for event_type, page, element, session_id, metadata (JSONB), and created_at. Add a POST /api/events endpoint to the Express server. Create a tracker utility in src/utils/tracker.ts that sends events without blocking the UI. Add page view tracking to the main App component.
Frequently asked questions
For simple apps, a custom tracker is lighter, faster, and gives you full control over your data. Google Analytics adds external JavaScript that can slow page loads and raises privacy concerns. If you need advanced features like funnels, cohorts, and heatmaps, a third-party tool may be worth the tradeoff.
Each event row is typically 200 to 500 bytes. At 1,000 events per day, you would use roughly 15 MB per month, well within Replit's 10 GB PostgreSQL limit. Add a cleanup job to delete events older than 90 days if storage becomes a concern.
Not if implemented correctly. The tracker uses fire-and-forget fetch calls that run in the background. The API endpoint inserts one row per event, which PostgreSQL handles in under a millisecond. Users will not notice any performance impact.
The session_id generated by crypto.randomUUID() identifies a browsing session without requiring authentication. Each page load creates a new session ID, so you can track behavior patterns without knowing who the user is.
Yes. Connect to your PostgreSQL database from Shell and use pg_dump or COPY TO to export event data as CSV or SQL. You can also build an API endpoint that returns data in JSON format for import into external tools.
Yes. RapidDev can help design a scalable analytics pipeline with proper data warehousing, real-time dashboards, and privacy-compliant tracking for Replit-hosted applications that have outgrown simple event logging.
Add a consent banner that sets a cookie or localStorage flag when the user accepts tracking. Check this flag before calling any trackEvent functions. If the user declines, do not send any events. Store the consent status as metadata on the first event after acceptance.
No. Static Deployments serve only HTML, CSS, and JavaScript files — there is no backend to receive events. You need an Autoscale or Reserved VM deployment for the API endpoint. Alternatively, send events to an external service.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation