Module: Sessions::Geolocation
- Defined in:
- lib/sessions/geolocation.rb
Overview
IP geolocation through the ‘trackdown` gem — a SOFT dependency, never required: every call site is `defined?(::Trackdown)`-guarded and rescue-everything (trackdown raises on private/loopback IPs in development, and a geo hiccup must never block a login write). The integration contract is lifted verbatim from footprinted’s proven one (→ docs/research/02-ecosystem.md §2):
- always pass `request:` through so Cloudflare headers win when present
(zero config, free, synchronous header read);
- go async only when sync would mean a database lookup (the MaxMind
mode) — Sessions::GeolocateJob enriches the row after commit;
- skip lookups when country_code is already present.
Without trackdown, geo columns simply stay nil and the devices page omits location cleanly.
Constant Summary collapse
- COLUMNS =
%i[country_code country_name city region].freeze
Class Method Summary collapse
-
.async_capable? ⇒ Boolean
Whether an async MaxMind lookup could possibly succeed — used to avoid enqueueing a no-op job per login on hosts without a MaxMind database.
-
.cloudflare_headers?(request) ⇒ Boolean
Whether this request already carries a Cloudflare geo answer — the header read is free, so we resolve synchronously.
- .columns_from(result) ⇒ Object
-
.coordinates_from(result) ⇒ Object
Latitude/longitude for EVENT rows only, precision-reduced per config.geo_precision (2 decimals ≈ 1km — privacy now, impossible-travel math later).
- .enabled? ⇒ Boolean
-
.enqueue(record) ⇒ Object
Hand a record to the async MaxMind path (no-op without ActiveJob or a trackdown database — see async_capable?).
-
.locate(ip, request: nil, coordinates: false) ⇒ Object
Synchronous geolocation — called inline at session-creation time ONLY when it’s free (Cloudflare already did the lookup and put the answer in request headers).
- .presence(value) ⇒ Object
Class Method Details
.async_capable? ⇒ Boolean
Whether an async MaxMind lookup could possibly succeed — used to avoid enqueueing a no-op job per login on hosts without a MaxMind database.
71 72 73 74 75 76 77 78 |
# File 'lib/sessions/geolocation.rb', line 71 def async_capable? enabled? && defined?(::ActiveJob) && ::Trackdown.respond_to?(:database_exists?) && ::Trackdown.database_exists? rescue StandardError false end |
.cloudflare_headers?(request) ⇒ Boolean
Whether this request already carries a Cloudflare geo answer — the header read is free, so we resolve synchronously.
60 61 62 63 64 65 66 67 |
# File 'lib/sessions/geolocation.rb', line 60 def cloudflare_headers?(request) return false unless request country = request.get_header("HTTP_CF_IPCOUNTRY") !country.nil? && !country.empty? && country != "XX" rescue StandardError false end |
.columns_from(result) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/sessions/geolocation.rb', line 80 def columns_from(result) return {} unless result return {} if result.country_code.to_s.empty? { country_code: result.country_code, country_name: presence(result.country_name), city: presence(result.city), region: presence(result.region) }.compact end |
.coordinates_from(result) ⇒ Object
Latitude/longitude for EVENT rows only, precision-reduced per config.geo_precision (2 decimals ≈ 1km — privacy now, impossible-travel math later).
95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/sessions/geolocation.rb', line 95 def coordinates_from(result) return {} unless result.respond_to?(:latitude) && result.latitude precision = Sessions.config.geo_precision { latitude: result.latitude.to_f.round(precision), longitude: result.longitude.to_f.round(precision) } rescue StandardError {} end |
.enabled? ⇒ Boolean
24 25 26 |
# File 'lib/sessions/geolocation.rb', line 24 def enabled? Sessions.config.geolocate == :auto && defined?(::Trackdown) end |
.enqueue(record) ⇒ Object
Hand a record to the async MaxMind path (no-op without ActiveJob or a trackdown database — see async_capable?).
47 48 49 50 51 52 53 54 55 56 |
# File 'lib/sessions/geolocation.rb', line 47 def enqueue(record) return false unless record&.persisted? return false unless async_capable? Sessions::GeolocateJob.perform_later(record.class.name, record.id) true rescue StandardError => e Sessions.warn("geolocation enqueue failed: #{e.class}: #{e.}") false end |
.locate(ip, request: nil, coordinates: false) ⇒ Object
Synchronous geolocation — called inline at session-creation time ONLY when it’s free (Cloudflare already did the lookup and put the answer in request headers). Returns a column hash or {}. ‘coordinates: true` adds precision-reduced lat/lng (event rows only).
32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/sessions/geolocation.rb', line 32 def locate(ip, request: nil, coordinates: false) return {} unless enabled? return {} if ip.to_s.empty? result = ::Trackdown.locate(ip.to_s, request: request) columns = columns_from(result) columns = columns.merge(coordinates_from(result)) if coordinates && columns.any? columns rescue StandardError => e Sessions.warn("geolocation failed for #{ip}: #{e.class}: #{e.}") {} end |
.presence(value) ⇒ Object
107 108 109 |
# File 'lib/sessions/geolocation.rb', line 107 def presence(value) value.nil? || value.to_s.empty? || value.to_s == "Unknown" ? nil : value end |