# How to track user activity in Replit apps

- Tool: Replit
- Difficulty: Beginner
- Time required: 20-30 minutes
- Compatibility: All Replit plans with PostgreSQL support (Core and Pro recommended). Works with React, Express, and any JavaScript-based stack.
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
CREATE TABLE IF NOT EXISTS user_events (
  id SERIAL PRIMARY KEY,
  event_type VARCHAR(50) NOT NULL,
  page VARCHAR(255),
  element VARCHAR(255),
  session_id VARCHAR(100),
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_events_type ON user_events(event_type);
CREATE 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.

### 2. 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.

```
// server/routes/analytics.js
import { Router } from 'express';
import pg from 'pg';

const router = Router();
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

router.post('/api/events', async (req, res) => {
  const { event_type, page, element, session_id, metadata } = req.body;

  if (!event_type) {
    return res.status(400).json({ error: 'event_type is required' });
  }

  try {
    await pool.query(
      `INSERT INTO user_events (event_type, page, element, session_id, metadata)
       VALUES ($1, $2, $3, $4, $5)`,
      [event_type, page, element, session_id, JSON.stringify(metadata || {})]
    );
    res.status(201).json({ success: true });
  } catch (err) {
    console.error('Event logging error:', err.message);
    res.status(500).json({ error: 'Failed to log event' });
  }
});

export default router;
```

**Expected result:** POST requests to /api/events insert a row into user_events and return a 201 status.

### 3. 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.

```
// src/utils/tracker.ts
const SESSION_ID = crypto.randomUUID();
const API_URL = '/api/events';

export function trackEvent(
  eventType: string,
  element?: string,
  metadata?: Record<string, unknown>
) {
  try {
    const payload = {
      event_type: eventType,
      page: window.location.pathname,
      element,
      session_id: SESSION_ID,
      metadata
    };

    fetch(API_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    }).catch(() => {}); // Silently ignore tracking failures
  } catch {
    // Never let tracking break the app
  }
}

export function trackPageView() {
  trackEvent('page_view');
}
```

**Expected result:** Importing and calling trackEvent or trackPageView sends event data to your API without affecting app performance.

### 4. 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.

```
// src/App.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { trackPageView } from './utils/tracker';

function App() {
  const location = useLocation();

  useEffect(() => {
    trackPageView();
  }, [location.pathname]);

  return (
    // ... your routes and layout
  );
}
```

**Expected result:** Every page navigation in your app automatically sends a page_view event to your database.

### 5. 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.

```
// Example: tracking a button click
import { trackEvent } from '../utils/tracker';

function SignUpButton() {
  const handleClick = () => {
    trackEvent('click', 'signup_button', { plan: 'pro' });
    // ... proceed with sign-up logic
  };

  return <button onClick={handleClick}>Sign Up</button>;
}
```

**Expected result:** Clicking tracked UI elements sends click events with element names and metadata to your database.

### 6. 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.

```
// GET /api/analytics?days=7
router.get('/api/analytics', async (req, res) => {
  const adminKey = req.headers['x-admin-key'];
  if (adminKey !== process.env.ANALYTICS_ADMIN_KEY) {
    return res.status(403).json({ error: 'Unauthorized' });
  }

  const days = parseInt(req.query.days) || 7;

  try {
    const result = await pool.query(
      `SELECT event_type, COUNT(*) as count,
              COUNT(DISTINCT session_id) as unique_sessions
       FROM user_events
       WHERE created_at > NOW() - INTERVAL '1 day' * $1
       GROUP BY event_type
       ORDER BY count DESC`,
      [days]
    );
    res.json({ period_days: days, events: result.rows });
  } catch (err) {
    console.error('Analytics query error:', err.message);
    res.status(500).json({ error: 'Query failed' });
  }
});
```

**Expected result:** Calling GET /api/analytics?days=7 with the correct admin key returns aggregated event counts grouped by type.

## Complete code example

File: `src/utils/tracker.ts`

```typescript
// src/utils/tracker.ts — Lightweight user interaction tracker
// Sends events to your backend without blocking the UI

const SESSION_ID = crypto.randomUUID();
const API_URL = '/api/events';

interface TrackingMetadata {
  [key: string]: string | number | boolean | null;
}

export function trackEvent(
  eventType: string,
  element?: string,
  metadata?: TrackingMetadata
): void {
  try {
    const payload = {
      event_type: eventType,
      page: window.location.pathname,
      element: element || null,
      session_id: SESSION_ID,
      metadata: metadata || {}
    };

    // Fire and forget — never await this
    fetch(API_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    }).catch(() => {
      // Silently ignore tracking failures
    });
  } catch {
    // Never let tracking break the application
  }
}

export function trackPageView(): void {
  trackEvent('page_view');
}

export function trackClick(element: string, metadata?: TrackingMetadata): void {
  trackEvent('click', element, metadata);
}

export function trackFormSubmit(formName: string, metadata?: TrackingMetadata): void {
  trackEvent('form_submit', formName, metadata);
}

// Use sendBeacon for events that must fire on page unload
export function trackBeforeUnload(): void {
  window.addEventListener('beforeunload', () => {
    const payload = JSON.stringify({
      event_type: 'session_end',
      page: window.location.pathname,
      session_id: SESSION_ID,
      metadata: {}
    });
    navigator.sendBeacon(API_URL, payload);
  });
}
```

## Common mistakes

- **Awaiting the tracking fetch call, which adds network latency to every user interaction** — undefined Fix: Call fetch without await and add .catch(() => {}) to silently handle failures. Tracking should be non-blocking.
- **Logging personally identifiable information (email, name, IP address) in the metadata field without user consent** — undefined Fix: Only log anonymous behavioral data (event type, page, element, session ID). If you need PII, add a privacy policy and consent mechanism first.
- **Not adding database indexes, causing analytics queries to slow down as the events table grows** — undefined Fix: 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

## Frequently asked questions

### Should I use Google Analytics instead of building my own tracking?

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.

### How much database storage does event tracking use?

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.

### Will tracking slow down my application?

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.

### How do I track events from users who are not logged in?

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.

### Can I export my analytics data from Replit?

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.

### Can RapidDev help set up a production-grade analytics system?

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.

### How do I handle tracking consent for GDPR compliance?

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.

### Does event tracking work in Replit Static Deployments?

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.

---

Source: https://www.rapidevelopers.com/replit-tutorial/how-to-monitor-and-log-user-interactions-in-a-web-application-hosted-on-replit
© RapidDev — https://www.rapidevelopers.com/replit-tutorial/how-to-monitor-and-log-user-interactions-in-a-web-application-hosted-on-replit
