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).
- .location_value(result, attribute) ⇒ Object
- .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 91 92 |
# File 'lib/sessions/geolocation.rb', line 80 def columns_from(result) return {} unless result country_code = location_value(result, :country_code) return {} if country_code.to_s.empty? { country_code: country_code, country_name: presence(location_value(result, :country_name)), city: presence(location_value(result, :city)), region: presence(location_value(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).
97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/sessions/geolocation.rb', line 97 def coordinates_from(result) latitude = location_value(result, :latitude) longitude = location_value(result, :longitude) return {} if latitude.nil? || longitude.nil? precision = Sessions.config.geo_precision { latitude: latitude.to_f.round(precision), longitude: 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 |
.location_value(result, attribute) ⇒ Object
111 112 113 114 115 |
# File 'lib/sessions/geolocation.rb', line 111 def location_value(result, attribute) result.public_send(attribute) if result.respond_to?(attribute) rescue StandardError nil end |
.presence(value) ⇒ Object
117 118 119 |
# File 'lib/sessions/geolocation.rb', line 117 def presence(value) value.nil? || value.to_s.empty? || value.to_s == "Unknown" ? nil : value end |