Skip to content

[Feature]: Add click geographic distribution analytics using IP geolocation so users can see which countries and cities their links are being clicked from #307

Description

@divyanshim27

Summary

piik.me's analytics dashboard currently tracks device type, browser, referrer, and click history with timestamps. However, there is no geographic data — users cannot see which countries or regions their links are being clicked from. Geographic analytics is one of the most-requested features in link analytics tools and is offered by every major competitor (Bitly, Short.io, Rebrandly). The existing clickHistory Firestore schema has room for a country and city field.

Problem

  • The GET /:shortCode redirect handler processes a click and tracks device, browser, and referrer — but the request IP address is not used for geolocation.
  • The analytics Firestore collection has no countries or regions map — geographic data is nowhere in the current schema.
  • The Socket.IO analyticsUpdate event sends impressions, clicks, shares, etc. — no geographic payload exists.
  • The globe visualization (Globe.gl and Three.js are listed as frontend dependencies!) appears to be in the codebase but likely has no real data powering it — it may be displaying placeholder data. Geographic click data would make this globe meaningful and visually impressive.
  • Marketing users — the core audience for a URL shortener — specifically need geographic data to evaluate campaign performance by region.

Proposed Solution

1. Add IP geolocation using geoip-lite (offline, no API key required):

npm install geoip-lite
// src/utils/geo.utils.js
const geoip = require('geoip-lite');

function getGeoFromIP(ip) {
  // Handle IPv4-mapped IPv6 addresses (e.g., ::ffff:1.2.3.4)
  const cleanIP = ip.replace('::ffff:', '');
  const geo = geoip.lookup(cleanIP);
  if (!geo) return { country: 'Unknown', city: 'Unknown', ll: null };
  return {
    country: geo.country || 'Unknown',
    city: geo.city || 'Unknown',
    ll: geo.ll, // [latitude, longitude]
  };
}

module.exports = { getGeoFromIP };

2. Update the click tracking in the redirect handler:

// server.js or routes/redirect.routes.js
const { getGeoFromIP } = require('./src/utils/geo.utils.js');

app.get('/:shortCode', async (req, res) => {
  // ... existing link lookup and expiry check ...

  const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.socket.remoteAddress;
  const geo = getGeoFromIP(ip);

  // Update Firestore analytics
  await db.collection('analytics').doc(shortCode).update({
    clicks: admin.firestore.FieldValue.increment(1),
    [`countries.${geo.country}`]: admin.firestore.FieldValue.increment(1),
    clickHistory: admin.firestore.FieldValue.arrayUnion({
      timestamp: admin.firestore.FieldValue.serverTimestamp(),
      device: detectDevice(req.headers['user-agent']),
      browser: detectBrowser(req.headers['user-agent']),
      referrer: req.headers['referer'] || 'direct',
      country: geo.country,      // NEW
      city: geo.city,            // NEW
      ll: geo.ll,                // NEW — [lat, lng] for globe visualization
    }),
  });

  // Emit real-time update via Socket.IO including geo data
  io.emit('analyticsUpdate', {
    shortCode,
    country: geo.country,
    city: geo.city,
    ll: geo.ll,
  });

  res.redirect(link.originalUrl);
});

3. Update the analytics Firestore schema:

{
  // ... existing fields ...
  countries: {        // NEW — map of country code to click count
    "US": 145,
    "IN": 87,
    "GB": 34,
  },
  clickHistory: [{
    // ... existing fields ...
    country: string,  // NEW — "US", "IN", "Unknown"
    city: string,     // NEW — "New York", "Unknown"
    ll: [number, number] | null, // NEW — [lat, lng] for globe
  }]
}

4. Frontend — power the existing Globe.gl visualization with real data:

Since globe.gl is already a dependency, connect the real-time geo click events to the globe:

// public/js/app.js — globe section
socket.on('analyticsUpdate', (data) => {
  if (data.ll) {
    globe.pointsData([...existingPoints, {
      lat: data.ll[0],
      lng: data.ll[1],
      size: 0.5,
      color: 'rgba(0, 200, 255, 0.8)',
      label: `${data.city}, ${data.country}`,
    }]);
  }
});

Additional Notes

  • geoip-lite uses a bundled MaxMind GeoLite2 database (~30MB) — no external API call is made during redirect, so geolocation adds under 1ms latency.
  • For privacy compliance: IP addresses are never stored in Firestore — only the derived country and city are persisted.
  • geoip-lite is accurate to country-level for ~99% of IPs and city-level for ~70%.
  • The existing globe.gl dependency suggests geographic visualization was always intended — this issue delivers the data layer to make it real.

Could you assign this issue to me?

Labels: enhancement, feature, analytics, GSSoC 2026

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions