Class: Trackdown::LocationResult

Inherits:
Object
  • Object
show all
Defined in:
lib/trackdown/location_result.rb

Overview

Where an IP address probably is — and an honest account of how we know.

Every result carries its own provenance: which provider answered, from which source, when it answered, how precise that provider says the answer is, and whether the host vouched for the path the request arrived through.

Nothing here is guessed. Every field is either what the provider said or something derived from it by a documented rule — never a plausible-looking placeholder. A field the answering provider cannot supply is nil, so a caller can always tell "the provider said no" apart from "it never said".

GeoIP is an estimate. It never proves that a person or a device was in a place. MaxMind documents those limits exactly: https://support.maxmind.com/knowledge-base/articles/maxmind-geolocation-accuracy

Constant Summary collapse

UNKNOWN =

What providers have always returned for a value they don't have. Kept for backwards compatibility — new code should ask #available? and read the nil-able fields instead of parsing display strings.

'Unknown'
UNKNOWN_FLAG =
'🏳️'
UNAVAILABLE_REASONS =

Why a lookup came back with no location. Stable and machine-readable: these symbols are part of the public API and are never translated.

%i[
  no_provider_available
  address_not_found
  provider_returned_unknown_country
  provider_data_incomplete
].freeze
SOURCE_TRUSTS =

How much the host vouches for the source of a request-backed result. :host_verified only ever comes from the host's own verifier — see Trackdown::Configuration#verify_request_came_through_trusted_cloudflare_path_with and #verify_request_came_through_trusted_cloudfront_path_with.

%i[unverified host_verified].freeze
LOCATION_FIELDS =

Where the IP is.

%i[
  country_code country_name city flag_emoji
  region region_code continent timezone
  latitude longitude postal_code metro_code
].freeze
PROVENANCE_FIELDS =

How we know, and how sure we are.

%i[
  provider_name provider_source source_trust resolved_at
  available estimated unavailable_reason
  accuracy_radius_in_kilometers accuracy_radius_confidence_percentage
  database_build_epoch database_built_at database_sha256
].freeze
FIELDS =

Every field #to_h can emit, in the order it emits them.

(LOCATION_FIELDS + PROVENANCE_FIELDS + %i[country_info]).freeze
DEFAULT_FIELDS =

What a no-argument #to_h returns: the exact shape Trackdown returned before provenance existed, kept that way as an API compatibility guarantee. Ask for the rest with include_provenance: true or name it in only:.

(LOCATION_FIELDS + %i[country_info]).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(country_code, country_name, city, flag_emoji, region: nil, region_code: nil, continent: nil, timezone: nil, latitude: nil, longitude: nil, postal_code: nil, metro_code: nil, provider_name: nil, provider_source: nil, source_trust: nil, resolved_at: nil, unavailable_reason: nil, accuracy_radius_in_kilometers: nil, accuracy_radius_confidence_percentage: nil, database_build_epoch: nil, database_sha256: nil) ⇒ LocationResult

Every keyword is optional, so a provider only supplies what it actually knows.

Parameters:

  • database_sha256 (String, #call, nil) (defaults to: nil)

    the digest itself, or something that returns it, so an expensive digest is computed only if someone asks. Concurrent readers may each call it, so a callable should memoize its own work — Trackdown's does.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/trackdown/location_result.rb', line 84

def initialize(country_code, country_name, city, flag_emoji,
               region: nil, region_code: nil, continent: nil,
               timezone: nil, latitude: nil, longitude: nil,
               postal_code: nil, metro_code: nil,
               provider_name: nil, provider_source: nil, source_trust: nil,
               resolved_at: nil, unavailable_reason: nil,
               accuracy_radius_in_kilometers: nil,
               accuracy_radius_confidence_percentage: nil,
               database_build_epoch: nil, database_sha256: nil)
  @country_code = country_code
  @country_name = country_name
  @city = city
  @flag_emoji = flag_emoji
  @region = region
  @region_code = region_code
  @continent = continent
  @timezone = timezone
  @latitude = latitude
  @longitude = longitude
  @postal_code = postal_code
  @metro_code = metro_code

  @provider_name = provider_name
  @provider_source = provider_source
  @source_trust = validate!(source_trust, SOURCE_TRUSTS, 'source trust')
  @resolved_at = resolved_at || Time.now.utc
  @unavailable_reason = validate!(unavailable_reason, UNAVAILABLE_REASONS, 'unavailable reason') ||
                        (:provider_data_incomplete if blank?(country_code))
  @accuracy_radius_in_kilometers = accuracy_radius_in_kilometers
  @accuracy_radius_confidence_percentage = accuracy_radius_confidence_percentage
  @database_build_epoch = database_build_epoch
  @database_sha256 = database_sha256
end

Instance Attribute Details

#accuracy_radius_confidence_percentageObject (readonly)

Returns the value of attribute accuracy_radius_confidence_percentage.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def accuracy_radius_confidence_percentage
  @accuracy_radius_confidence_percentage
end

#accuracy_radius_in_kilometersObject (readonly) Also known as: accuracy_radius_km

Returns the value of attribute accuracy_radius_in_kilometers.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def accuracy_radius_in_kilometers
  @accuracy_radius_in_kilometers
end

#cityObject (readonly)

Returns the value of attribute city.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def city
  @city
end

#continentObject (readonly)

Returns the value of attribute continent.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def continent
  @continent
end

#country_codeObject (readonly)

Returns the value of attribute country_code.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def country_code
  @country_code
end

#country_nameObject (readonly) Also known as: country

Returns the value of attribute country_name.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def country_name
  @country_name
end

#database_build_epochObject (readonly)

Returns the value of attribute database_build_epoch.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def database_build_epoch
  @database_build_epoch
end

#flag_emojiObject (readonly) Also known as: emoji, emoji_flag, country_flag

Returns the value of attribute flag_emoji.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def flag_emoji
  @flag_emoji
end

#latitudeObject (readonly)

Returns the value of attribute latitude.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def latitude
  @latitude
end

#longitudeObject (readonly)

Returns the value of attribute longitude.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def longitude
  @longitude
end

#metro_codeObject (readonly)

Returns the value of attribute metro_code.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def metro_code
  @metro_code
end

#postal_codeObject (readonly)

Returns the value of attribute postal_code.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def postal_code
  @postal_code
end

#provider_nameObject (readonly) Also known as: provider

Returns the value of attribute provider_name.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def provider_name
  @provider_name
end

#provider_sourceObject (readonly)

Returns the value of attribute provider_source.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def provider_source
  @provider_source
end

#regionObject (readonly)

Returns the value of attribute region.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def region
  @region
end

#region_codeObject (readonly)

Returns the value of attribute region_code.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def region_code
  @region_code
end

#resolved_atObject (readonly)

Returns the value of attribute resolved_at.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def resolved_at
  @resolved_at
end

#source_trustObject (readonly)

Returns the value of attribute source_trust.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def source_trust
  @source_trust
end

#timezoneObject (readonly)

Returns the value of attribute timezone.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def timezone
  @timezone
end

#unavailable_reasonObject (readonly)

Returns the value of attribute unavailable_reason.



70
71
72
# File 'lib/trackdown/location_result.rb', line 70

def unavailable_reason
  @unavailable_reason
end

Class Method Details

.unavailable(reason, **provenance) ⇒ Object

A lookup that resolved nothing, and says why.

LocationResult.unavailable(:address_not_found, provider_name: :maxmind)


121
122
123
124
125
126
127
128
129
130
# File 'lib/trackdown/location_result.rb', line 121

def self.unavailable(reason, **provenance)
  if reason.nil?
    raise ArgumentError, "An unavailable result has to say why. Must be one of: #{UNAVAILABLE_REASONS.join(', ')}"
  end
  if provenance.key?(:unavailable_reason)
    raise ArgumentError, 'Pass the unavailable reason once, as the first argument to LocationResult.unavailable'
  end

  new(nil, UNKNOWN, UNKNOWN, UNKNOWN_FLAG, unavailable_reason: reason, **provenance)
end

Instance Method Details

#available?Boolean

Did we actually resolve a location?

Returns:

  • (Boolean)


140
141
142
# File 'lib/trackdown/location_result.rb', line 140

def available?
  @unavailable_reason.nil?
end

#country_infoObject



174
175
176
177
178
# File 'lib/trackdown/location_result.rb', line 174

def country_info
  return nil unless country_code

  ISO3166::Country.new(country_code)
end

#database_built_atObject

When the database that answered was built.



170
171
172
# File 'lib/trackdown/location_result.rb', line 170

def database_built_at
  Time.at(@database_build_epoch).utc if @database_build_epoch.is_a?(Numeric)
end

#database_sha256Object

The MaxMind database digest, computed the first time it's asked for.



164
165
166
167
# File 'lib/trackdown/location_result.rb', line 164

def database_sha256
  @database_sha256 = @database_sha256.call if @database_sha256.respond_to?(:call)
  @database_sha256
end

#estimated?Boolean

True whenever a provider resolved any location data, including a partial result that has a city or coordinates but no country. Availability answers "could we name a country?"; estimated answers "is any returned location an inference?" Those are deliberately independent questions.

Returns:

  • (Boolean)


152
153
154
# File 'lib/trackdown/location_result.rb', line 152

def estimated?
  available? || partial_location_estimate?
end

#source_was_verified_by_host?Boolean Also known as: host_verified?

True only when the host's own verifier vouched for this request's path. Header presence alone never gets you here.

Returns:

  • (Boolean)


158
159
160
# File 'lib/trackdown/location_result.rb', line 158

def source_was_verified_by_host?
  @source_trust == :host_verified
end

#to_h(only: nil, include_country_info: true, include_provenance: false) ⇒ Object

The whole result as a hash, or exactly the fields you name.

result.to_h
result.to_h(only: %i[country_code city latitude longitude provider_name])
result.to_h(include_country_info: false)
result.to_h(include_provenance: true)

Parameters:

  • only (Array<Symbol>, Symbol, nil) (defaults to: nil)

    the exact fields to serialize, in the order you name them. What you name is what you get: naming a field that doesn't exist raises, and nothing you name is ever dropped, so a typo can't silently cost you a column in a record you're keeping. The full list of names is FIELDS.

  • include_country_info (Boolean) (defaults to: true)

    the derived countries gem payload is large; pass false to leave it out of the default shape. Ignored when you pass only:, which already says exactly what you want.

  • include_provenance (Boolean) (defaults to: false)

    add every provenance field except the database digest, whose first read costs a full pass over the database file. Ignored when only: names an exact shape.



198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/trackdown/location_result.rb', line 198

def to_h(only: nil, include_country_info: true, include_provenance: false)
  fields = if only
             requested_fields(only)
           elsif include_provenance
             provenance_fields(include_country_info: include_country_info)
           elsif include_country_info
             DEFAULT_FIELDS
           else
             DEFAULT_FIELDS - %i[country_info]
           end

  fields.to_h { |field| [field, value_of(field)] }
end

#unavailable?Boolean

Returns:

  • (Boolean)


144
145
146
# File 'lib/trackdown/location_result.rb', line 144

def unavailable?
  !available?
end