10x-media plugins
Analytics

Geo resolution

Resolve visitor country, region, and city at ingest, with headers, MaxMind, or your own resolver.

The native engine resolves geo per ingested event, server-side. Resolution never blocks ingestion: on any failure the geo fields stay empty and the event is stored anyway.

Default: platform headers

Out of the box, geo comes from headers your hosting platform already injects. platformHeaderResolver reads:

  • x-vercel-ip-country, x-vercel-ip-country-region, x-vercel-ip-city (Vercel)
  • cf-ipcountry (Cloudflare, as the country fallback)

On Vercel or behind Cloudflare this means country-level analytics with zero configuration and zero databases.

MaxMind (optional)

For self-hosted deployments without geo headers, point the engine at a MaxMind GeoLite2 database:

native({ geoDbPath: '/data/GeoLite2-City.mmdb' })

This composes headers-first with MaxMind: IP lookup runs only when headers did not already supply a country. It requires two things you provide:

pnpm add maxmind   # optional peer dependency, loaded lazily

and a downloaded .mmdb file (GeoLite2 is free with a MaxMind account). If the library is missing, the file is unreadable, or a lookup fails, resolution degrades to empty fields with a single logged warning.

Custom pipelines

The @10x-media/analytics/geo subpath exports the building blocks:

import {
  platformHeaderResolver,
  maxmindResolver,
  composeGeoResolvers,
  noopResolver,
  type GeoResolver,
  type Geo,
} from '@10x-media/analytics/geo'

A GeoResolver is (headers: Headers) => Geo | Promise<Geo> where Geo is { country?, region?, city? }. Compose resolvers left to right; the first one that produces a country wins:

native({
  geoResolver: composeGeoResolvers(
    platformHeaderResolver,
    maxmindResolver({ dbPath: '/data/GeoLite2-City.mmdb' }),
    async (headers) => lookupInMyService(headers.get('x-real-ip')),
  ),
})

Use noopResolver to disable geo entirely (geoResolver: noopResolver).

Where geo shows up

Geo lands on each raw event and in the site-wide country rollup, which powers the Countries dashboard widget and the country dimension. Events without a resolved country are simply omitted from country breakdowns; totals are unaffected.

On this page