Visitor Geolocation
Sentiero can enrich each session with the visitor’s country, city, region, and timezone. Resolution happens server-side at event ingest; the recorder is unchanged and no IP address is ever stored — deliberately coarse, in keeping with anonymize_ip.
Geo capture is off by default. Location headers are ordinary request headers — any client can send them — so only enable a source that your infrastructure actually guarantees.
Cloudflare
If your app is behind Cloudflare, enable IP geolocation (Network settings) for country, and the “Add visitor location headers” managed transform (Rules → Transform rules) for city/region/timezone. Then:
Sentiero.configure do |config|
config.geo_source = :cloudflare
end
Sentiero reads CF-IPCountry, CF-IPCity, CF-Region, and CF-Timezone, skipping Cloudflare’s XX (unknown) and T1 (Tor) markers. Country-only capture (no managed transform) is fine — the other fields just stay unset.
Caveat: only set
:cloudflarewhen requests genuinely arrive through Cloudflare. If clients can reach your origin directly, they can spoof these headers — lock the origin to Cloudflare’s IP ranges (or enable Authenticated Origin Pulls) so the headers are trustworthy.
Any other source
geo_source also accepts any callable taking the Rack env and returning a Hash with "country", "city", "region", and/or "timezone" string values. This covers other CDNs:
# CloudFront (enable the CloudFront-Viewer-Country header policy)
config.geo_source = ->(env) { {"country" => env["HTTP_CLOUDFRONT_VIEWER_COUNTRY"]} }
or a self-managed MaxMind database — resolve the IP in-request and return only the coarse result, so the IP itself is never persisted and anonymize_ip is unaffected:
# Gemfile: gem "maxmind-geoip2"
GEO_DB = MaxMind::GeoIP2::Reader.new(database: "/srv/geoip/GeoLite2-City.mmdb")
config.geo_source = lambda do |env|
record = GEO_DB.city(env["REMOTE_ADDR"]) # resolve, then discard the IP
{"country" => record.country.iso_code, "city" => record.city&.name}
rescue MaxMind::GeoIP2::AddressNotFoundError
nil
end
A resolver that raises or returns something other than a Hash is ignored (with a one-time warning) — geo problems never break event ingest.
Client-supplied geo_* metadata is never trusted by default, so a page cannot forge its own location. If you do want a resolver to use page-provided data, declare a second parameter and it receives the client metadata — an explicit, server-side opt-in:
config.geo_source = ->(env, client_metadata) { {"country" => client_metadata["geo_country"]} }
Where it shows up
Resolved values are stored as geo_country, geo_city, geo_region, and geo_timezone in the session’s metadata:
- Analytics overview — Countries and Cities cards (shown once geo data exists).
- Segments — a Country filter dropdown; filter by city with the generic metadata filter (
metadata_key=geo_city). - Session detail — in the session’s metadata panel.
Server-resolved geo is authoritative for these keys — a page setting geo_country (or the others) via setMetadata() is ignored, unless a two-argument resolver explicitly opts into the page’s value (above).