Module: Sentiero::Geo

Defined in:
lib/sentiero/geo.rb

Overview

Deliberately coarse — country/city/region/timezone, no coordinates, no IP: resolution happens in-request and only the result is stored, so config.anonymize_ip is unaffected.

Constant Summary collapse

MAX_VALUE_LENGTH =
256
PROC_WARNING_LOCK =
Mutex.new
CLOUDFLARE_HEADERS =

CF-IPCountry ships with Cloudflare IP geolocation; the other headers need the "Add visitor location headers" managed transform, so country-only is the common case.

{
  "geo_country" => "HTTP_CF_IPCOUNTRY",
  "geo_city" => "HTTP_CF_IPCITY",
  "geo_region" => "HTTP_CF_REGION",
  "geo_timezone" => "HTTP_CF_TIMEZONE"
}.freeze
CLOUDFLARE_UNKNOWN =

Cloudflare placeholder codes: XX = unknown, T1 = Tor exit.

%w[XX T1].freeze
PROC_KEY_MAP =
{
  "country" => "geo_country",
  "city" => "geo_city",
  "region" => "geo_region",
  "timezone" => "geo_timezone"
}.freeze
TARGET_KEYS =
PROC_KEY_MAP.values.freeze

Class Method Summary collapse

Class Method Details

.clean(value) ⇒ Object



77
78
79
80
81
82
83
84
# File 'lib/sentiero/geo.rb', line 77

def clean(value)
  return nil unless value.is_a?(String)

  stripped = value.strip
  return nil if stripped.empty?

  stripped[0, MAX_VALUE_LENGTH]
end

.from_cloudflare(env) ⇒ Object



43
44
45
46
47
48
49
50
# File 'lib/sentiero/geo.rb', line 43

def from_cloudflare(env)
  geo = CLOUDFLARE_HEADERS.each_with_object({}) do |(key, header), acc|
    value = clean(env[header])
    acc[key] = value if value
  end
  geo.delete("geo_country") if CLOUDFLARE_UNKNOWN.include?(geo["geo_country"])
  geo
end

.from_proc(env, source, client_metadata = {}) ⇒ Object

A broken geo hook must never break ingest: rescue everything, warn once per process, resolve empty. A two-arg resolver opts into the client metadata.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/sentiero/geo.rb', line 54

def from_proc(env, source,  = {})
  raw = (source.arity == 1) ? source.call(env) : source.call(env, )
  return {} unless raw.is_a?(Hash)

  raw.each_with_object({}) do |(key, value), acc|
    mapped = PROC_KEY_MAP[key.to_s]
    value = clean(value)
    acc[mapped] = value if mapped && value
  end
rescue => e
  PROC_WARNING_LOCK.synchronize do
    return {} if @proc_warning_emitted
    @proc_warning_emitted = true
  end

  warn "[Sentiero] geo_source raised #{e.class}: #{e.message}; skipping geo capture"
  {}
end

.reset_proc_warning!Object



73
74
75
# File 'lib/sentiero/geo.rb', line 73

def reset_proc_warning!
  @proc_warning_emitted = false
end

.resolve(env, source, client_metadata = {}) ⇒ Object



35
36
37
38
39
40
41
# File 'lib/sentiero/geo.rb', line 35

def resolve(env, source,  = {})
  case source
  when nil then {}
  when :cloudflare then from_cloudflare(env)
  else from_proc(env, source, )
  end
end