Module: Studio::IpLocations
- Defined in:
- lib/studio/ip_locations.rb
Overview
The shape, dedupe rule and growth cap for users.ip_locations — the jsonb
array recording the distinct places an account has been seen from.
Deliberately PURE: it takes the existing array and returns a new one. No ActiveRecord, no clock beyond what the caller passes, no geo lookup. That keeps the analytics shape identical in every app while leaving the RESOLUTION to the host — turf-monster already resolves IP → state/country through Geocoder in ApplicationController#detect_geo_state and can hand the result straight here; an app with no geo lookup at all still gets a useful record of the distinct IPs.
An entry:
{ "ip" => "203.0.113.7", "country" => "US", "region" => "TX",
"city" => "Austin", "first_seen_at" => "2026-08-13T21:00:00Z",
"last_seen_at" => "2026-08-14T09:30:00Z", "count" => 4 }
String keys throughout, ISO-8601 times: this round-trips through jsonb, and a symbol-keyed hash written today would come back string-keyed tomorrow.
Constant Summary collapse
- MAX_ENTRIES =
Distinct locations kept per user, most-recently-seen first. A cap is not optional for a column that grows on sign-in: an account that travels, or sits behind a rotating consumer IP, would otherwise grow this row without bound and drag every SELECT that loads the user along with it.
50- SKIPPED_IP =
Loopback and private ranges. A dev session signs in from 127.0.0.1 all day and a container from 10.x; recording those buys nothing and would crowd the real entries out of the cap.
/\A(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|::1\z|fe80:|f[cd])/i
Class Method Summary collapse
-
.location_key(ip:, country: nil, region: nil, city: nil) ⇒ Object
What the location is, for deduping.
- .normalize(entries) ⇒ Object
- .normalize_time(value) ⇒ Object
- .presence(value) ⇒ Object
-
.push(entries, ip:, country: nil, region: nil, city: nil, at: nil) ⇒ Object
Returns the array to store.
-
.seen?(entries, ip: nil, country: nil, region: nil, city: nil) ⇒ Boolean
Has this account been seen here before? The question a caller asks to avoid a write on every single request.
Class Method Details
.location_key(ip:, country: nil, region: nil, city: nil) ⇒ Object
What the location is, for deduping. Geo when we have any of it — two sign-ins from Austin are ONE location even from different addresses, which is the whole point of tracking places rather than addresses. Falls back to the IP when the lookup gave us nothing, so an app with no geo still records something useful. nil (record nothing) when there is neither.
79 80 81 82 83 84 85 |
# File 'lib/studio/ip_locations.rb', line 79 def location_key(ip:, country: nil, region: nil, city: nil) geo = [country, region, city].map { |v| presence(v) } return "geo:" + geo.map(&:to_s).join("|").downcase if geo.any? return nil if ip.blank? || ip.match?(SKIPPED_IP) "ip:#{ip.downcase}" end |
.normalize(entries) ⇒ Object
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/studio/ip_locations.rb', line 87 def normalize(entries) Array(entries).filter_map do |entry| next unless entry.respond_to?(:to_h) row = entry.to_h.transform_keys(&:to_s) next if row.empty? # Backfill for rows written before these fields existed, so an upgrade in # place neither duplicates locations it already holds nor treats them as # the stalest thing in the column. row["key"] ||= location_key(ip: row["ip"].to_s, country: row["country"], region: row["region"], city: row["city"]) # A stored row is evidence of at least one sighting. row["count"] = 1 if row["count"].to_i < 1 row["last_seen_at"] ||= row["first_seen_at"] row end end |
.normalize_time(value) ⇒ Object
106 107 108 109 110 111 |
# File 'lib/studio/ip_locations.rb', line 106 def normalize_time(value) return value if value.is_a?(String) && value.present? return Time.now.utc.iso8601 if value.nil? value.respond_to?(:utc) ? value.utc.iso8601 : value.to_s end |
.presence(value) ⇒ Object
113 114 115 116 |
# File 'lib/studio/ip_locations.rb', line 113 def presence(value) str = value.to_s.strip str.empty? ? nil : str end |
.push(entries, ip:, country: nil, region: nil, city: nil, at: nil) ⇒ Object
Returns the array to store. Appends a new entry when this location has not been seen before; otherwise refreshes the existing one in place.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/studio/ip_locations.rb', line 39 def push(entries, ip:, country: nil, region: nil, city: nil, at: nil) at = normalize_time(at) ip = ip.to_s.strip key = location_key(ip: ip, country: country, region: region, city: city) return normalize(entries) if key.nil? existing = normalize(entries) match = existing.find { |e| e["key"] == key } if match match["last_seen_at"] = at match["count"] = match["count"].to_i + 1 # The IP within a location legitimately changes (a reassigned lease); the # LOCATION is what we deduped on, so keep the freshest address for it. match["ip"] = ip if ip.present? else existing << { "key" => key, "ip" => ip.presence, "country" => presence(country), "region" => presence(region), "city" => presence(city), "first_seen_at" => at, "last_seen_at" => at, "count" => 1 } end existing.sort_by { |e| e["last_seen_at"].to_s }.reverse.first(MAX_ENTRIES) end |
.seen?(entries, ip: nil, country: nil, region: nil, city: nil) ⇒ Boolean
Has this account been seen here before? The question a caller asks to avoid a write on every single request.
67 68 69 70 71 72 |
# File 'lib/studio/ip_locations.rb', line 67 def seen?(entries, ip: nil, country: nil, region: nil, city: nil) key = location_key(ip: ip.to_s.strip, country: country, region: region, city: city) return false if key.nil? normalize(entries).any? { |e| e["key"] == key } end |