trackdown 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.simplecov +25 -0
- data/CHANGELOG.md +15 -0
- data/README.md +123 -8
- data/Rakefile +0 -1
- data/lib/trackdown/location_result.rb +23 -2
- data/lib/trackdown/providers/auto_provider.rb +67 -2
- data/lib/trackdown/providers/cloudflare_provider.rb +34 -1
- data/lib/trackdown/providers/maxmind_provider.rb +16 -1
- data/lib/trackdown/version.rb +1 -1
- metadata +10 -119
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78637df00207906a24e8553f69a16536a402d59af543c531613b9b3b881bad2c
|
|
4
|
+
data.tar.gz: 21c3fc9d6845db2322bfe85cd1a85d87539c16b211c12a3f3c9dc4de3bc4dd12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08cc2f24e4339a74690c74a9adfb687fb9db337c4215911f7673cf2c9583054e8325b8250c9ab219f7c878a80b8ba2287e238cf7776b61fe475830ef54203a07'
|
|
7
|
+
data.tar.gz: dfbff6a70b8d6ecbd8b18b191d0b6a819af2e72aad179e8ee9c007f8511af5a22a9531d80a698c87f33759f27ec48905aec23ffa6282b13b44ca1bb9cf021400
|
data/.simplecov
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
SimpleCov.start do
|
|
4
|
+
formatter SimpleCov::Formatter::SimpleFormatter
|
|
5
|
+
|
|
6
|
+
add_filter "/test/"
|
|
7
|
+
|
|
8
|
+
track_files "{lib,app}/**/*.rb"
|
|
9
|
+
|
|
10
|
+
enable_coverage :branch
|
|
11
|
+
|
|
12
|
+
minimum_coverage line: 80, branch: 65
|
|
13
|
+
|
|
14
|
+
command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
SimpleCov.at_exit do
|
|
18
|
+
SimpleCov.result.format!
|
|
19
|
+
puts "\n" + "=" * 60
|
|
20
|
+
puts "COVERAGE SUMMARY"
|
|
21
|
+
puts "=" * 60
|
|
22
|
+
puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
|
|
23
|
+
puts "Branch Coverage: #{SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || 'N/A'}%"
|
|
24
|
+
puts "=" * 60
|
|
25
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [0.3.1] - 2026-02-24
|
|
2
|
+
|
|
3
|
+
- Fix incorrect Cloudflare geolocation when an upstream proxy sits before Cloudflare
|
|
4
|
+
- In `:auto` mode, compare `CF-Connecting-IP` with the target IP and fall back to MaxMind on mismatch
|
|
5
|
+
- Normalize compared IPs with `IPAddr` so equivalent IPv6 / IPv4-mapped forms are treated as matches
|
|
6
|
+
- Add tests covering matching and mismatching `CF-Connecting-IP` scenarios
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-02-08
|
|
9
|
+
|
|
10
|
+
- Add 8 new geolocation fields: `region`, `region_code`, `continent`, `timezone`, `latitude`, `longitude`, `postal_code`, `metro_code`
|
|
11
|
+
- All new fields available from both Cloudflare and MaxMind providers
|
|
12
|
+
- All 10 Cloudflare "Add visitor location headers" now fully supported
|
|
13
|
+
- Backward compatible — all new fields are optional, existing API unchanged
|
|
14
|
+
- `to_h` now includes all new fields
|
|
15
|
+
|
|
1
16
|
## [0.2.0] - 2026-01-02
|
|
2
17
|
|
|
3
18
|
- Completely decouple Maxmind from the gem, making it optional
|
data/README.md
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
# 📍 `trackdown` - Ruby gem to geolocate IPs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/trackdown) [](https://github.com/rameerez/trackdown/actions)
|
|
4
|
+
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=trackdown)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=trackdown)!
|
|
7
|
+
|
|
8
|
+
`trackdown` is a Ruby gem that allows you to geolocate IP addresses easily.
|
|
9
|
+
|
|
10
|
+
It works out-of-the-box with **Cloudflare** (zero config!); and it's also a simple, convenient wrapper on top of **MaxMind** (just bring your own MaxMind key, and you're good to go!).
|
|
11
|
+
|
|
12
|
+
`trackdown` offers a clean API for Rails applications to fetch country, city, region, continent, timezone, coordinates, and emoji flag information for any IP address.
|
|
4
13
|
|
|
5
14
|
Given an IP, it gives you the corresponding:
|
|
6
15
|
- 🗺️ Country (two-letter country code + country name)
|
|
7
16
|
- 📍 City
|
|
17
|
+
- 🏔️ Region / state (e.g. "California") and region code (e.g. "CA")
|
|
18
|
+
- 🌍 Continent (e.g. "NA", "EU")
|
|
19
|
+
- 🕐 Timezone (e.g. "America/Los_Angeles")
|
|
20
|
+
- 📌 Latitude and longitude coordinates
|
|
21
|
+
- 📮 Postal code (e.g. "94107")
|
|
22
|
+
- 📺 Metro code (e.g. "807")
|
|
8
23
|
- 🇺🇸 Emoji flag of the country
|
|
9
24
|
|
|
10
|
-
##
|
|
25
|
+
## First, choose your `trackdown` Geo IP provider
|
|
11
26
|
|
|
12
27
|
### Option 1: Cloudflare (recommended, zero config)
|
|
13
28
|
|
|
14
|
-
If your app is behind Cloudflare, you can use `trackdown` with **zero configuration**:
|
|
29
|
+
If your Rails app is behind Cloudflare, you can use `trackdown` with **zero configuration**:
|
|
15
30
|
- No API keys needed
|
|
16
31
|
- No database downloads
|
|
17
32
|
- No external dependencies
|
|
18
33
|
- Instant lookups from Cloudflare headers
|
|
19
34
|
|
|
20
|
-
Just enable "IP Geolocation" in your Cloudflare dashboard and you're done!
|
|
35
|
+
Just enable "IP Geolocation" in your Cloudflare dashboard and you're done! For the full set of location fields (city, region, coordinates, etc.), enable ["Add visitor location headers"](https://developers.cloudflare.com/rules/transform/managed-transforms/reference/) in Managed Transforms. We automatically read these headers from the `request` and provide you with the IP geo data.
|
|
21
36
|
|
|
22
37
|
### Option 2: MaxMind (BYOK - Bring Your Own Key)
|
|
23
38
|
|
|
@@ -119,7 +134,7 @@ production:
|
|
|
119
134
|
refresh_trackdown_database:
|
|
120
135
|
class: TrackdownDatabaseRefreshJob
|
|
121
136
|
queue: default
|
|
122
|
-
schedule: every Saturday at 4am
|
|
137
|
+
schedule: every Saturday at 4am
|
|
123
138
|
```
|
|
124
139
|
|
|
125
140
|
> [!NOTE]
|
|
@@ -162,12 +177,23 @@ result.country_code # => 'US'
|
|
|
162
177
|
result.country_name # => 'United States'
|
|
163
178
|
result.country # => 'United States' (alias for country_name)
|
|
164
179
|
result.city # => 'Mountain View' (from MaxMind or Cloudflare's "Add visitor location headers")
|
|
180
|
+
result.region # => 'California'
|
|
181
|
+
result.region_code # => 'CA'
|
|
182
|
+
result.continent # => 'NA'
|
|
183
|
+
result.timezone # => 'America/Los_Angeles'
|
|
184
|
+
result.latitude # => 37.7749
|
|
185
|
+
result.longitude # => -122.4194
|
|
186
|
+
result.postal_code # => '94107'
|
|
187
|
+
result.metro_code # => '807'
|
|
165
188
|
result.flag_emoji # => '🇺🇸'
|
|
166
189
|
result.emoji # => '🇺🇸' (alias for flag_emoji)
|
|
167
190
|
result.country_flag # => '🇺🇸' (alias for flag_emoji)
|
|
168
191
|
result.country_info # => # Rich country data from the `countries` gem
|
|
169
192
|
```
|
|
170
193
|
|
|
194
|
+
> [!NOTE]
|
|
195
|
+
> The `region`, `region_code`, `continent`, `timezone`, `latitude`, `longitude`, `postal_code`, and `metro_code` fields require Cloudflare's ["Add visitor location headers"](https://developers.cloudflare.com/rules/transform/managed-transforms/reference/) Managed Transform to be enabled, or a MaxMind GeoLite2-City database. These fields return `nil` when not available.
|
|
196
|
+
|
|
171
197
|
### Rich country information
|
|
172
198
|
|
|
173
199
|
For `country_info` we're leveraging the [`countries`](https://github.com/countries/countries) gem, so you get a lot of information about the country, like the continent, the region, the languages spoken, the currency, and more:
|
|
@@ -191,6 +217,14 @@ result.to_h
|
|
|
191
217
|
# country_name: 'United States',
|
|
192
218
|
# city: 'Mountain View',
|
|
193
219
|
# flag_emoji: '🇺🇸',
|
|
220
|
+
# region: 'California',
|
|
221
|
+
# region_code: 'CA',
|
|
222
|
+
# continent: 'NA',
|
|
223
|
+
# timezone: 'America/Los_Angeles',
|
|
224
|
+
# latitude: 37.7749,
|
|
225
|
+
# longitude: -122.4194,
|
|
226
|
+
# postal_code: '94107',
|
|
227
|
+
# metro_code: '807',
|
|
194
228
|
# country_info: { ... }
|
|
195
229
|
# }
|
|
196
230
|
```
|
|
@@ -243,14 +277,95 @@ Trackdown.update_database
|
|
|
243
277
|
|
|
244
278
|
### Cloudflare Provider
|
|
245
279
|
|
|
246
|
-
When you enable "IP Geolocation" in Cloudflare, they add the `CF-IPCountry` header to every request. If you enable "Add visitor location headers" (via Managed Transforms), you
|
|
280
|
+
When you enable "IP Geolocation" in Cloudflare, they add the `CF-IPCountry` header to every request. If you also enable ["Add visitor location headers"](https://developers.cloudflare.com/rules/transform/managed-transforms/reference/) (via Managed Transforms), you get all 10 location headers:
|
|
281
|
+
|
|
282
|
+
| Cloudflare header | `trackdown` field |
|
|
283
|
+
|---|---|
|
|
284
|
+
| `cf-ipcountry` | `country_code` |
|
|
285
|
+
| `cf-ipcity` | `city` |
|
|
286
|
+
| `cf-ipcontinent` | `continent` |
|
|
287
|
+
| `cf-iplatitude` | `latitude` |
|
|
288
|
+
| `cf-iplongitude` | `longitude` |
|
|
289
|
+
| `cf-region` | `region` |
|
|
290
|
+
| `cf-region-code` | `region_code` |
|
|
291
|
+
| `cf-metro-code` | `metro_code` |
|
|
292
|
+
| `cf-postal-code` | `postal_code` |
|
|
293
|
+
| `cf-timezone` | `timezone` |
|
|
247
294
|
|
|
248
|
-
Trackdown reads these headers directly from the request with zero overhead
|
|
295
|
+
Trackdown reads these headers directly from the request with zero overhead — no database lookups, no external API calls.
|
|
249
296
|
|
|
250
297
|
### MaxMind Provider
|
|
251
298
|
|
|
252
|
-
Downloads the GeoLite2-City database to your server and performs local lookups using connection pooling for performance.
|
|
299
|
+
Downloads the [GeoLite2-City](https://dev.maxmind.com/geoip/docs/databases/city-and-country/) database to your server and performs local lookups using connection pooling for performance. All fields (`country`, `city`, `region`, `continent`, `timezone`, `latitude`, `longitude`, `postal_code`, `metro_code`) are extracted from the database record.
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
## Docker & Container Deployments
|
|
303
|
+
|
|
304
|
+
When deploying with Docker, Kubernetes, or similar container orchestration, the MaxMind database file needs special handling since container filesystems are ephemeral.
|
|
305
|
+
|
|
306
|
+
### Option 1: Persistent Volume (Recommended)
|
|
307
|
+
|
|
308
|
+
Mount a persistent volume for the database file so it survives container restarts and deployments.
|
|
309
|
+
|
|
310
|
+
**Kamal (`config/deploy.yml`):**
|
|
311
|
+
```yaml
|
|
312
|
+
volumes:
|
|
313
|
+
- "trackdown_data:/rails/db/geodata"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Then configure the database path:
|
|
317
|
+
```ruby
|
|
318
|
+
# config/initializers/trackdown.rb
|
|
319
|
+
config.database_path = Rails.root.join('db', 'geodata', 'GeoLite2-City.mmdb').to_s
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Docker Compose:**
|
|
323
|
+
```yaml
|
|
324
|
+
services:
|
|
325
|
+
app:
|
|
326
|
+
volumes:
|
|
327
|
+
- trackdown_data:/rails/db/geodata
|
|
328
|
+
|
|
329
|
+
volumes:
|
|
330
|
+
trackdown_data:
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Option 2: Download on Container Start
|
|
334
|
+
|
|
335
|
+
If you prefer not to use volumes, download the database when the container starts. Add to your entrypoint or a post-deploy hook:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# In your entrypoint.sh or deploy hook
|
|
339
|
+
bin/rails runner "Trackdown.update_database unless File.exist?(Trackdown.configuration.database_path)"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Or create a job that runs on boot:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# config/initializers/trackdown_boot.rb
|
|
346
|
+
Rails.application.config.after_initialize do
|
|
347
|
+
if Rails.env.production? && !File.exist?(Trackdown.configuration.database_path)
|
|
348
|
+
Trackdown.update_database
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
> [!WARNING]
|
|
354
|
+
> Option 2 adds startup time (~10-30 seconds) on fresh deploys and requires network access during boot. A persistent volume is more reliable for production.
|
|
355
|
+
|
|
356
|
+
### Background Jobs Consideration
|
|
253
357
|
|
|
358
|
+
When using background job processors (Sidekiq, SolidQueue, GoodJob), geolocation lookups in jobs **cannot use Cloudflare headers** since there's no HTTP request. These jobs will fall back to MaxMind automatically when using `:auto` provider.
|
|
359
|
+
|
|
360
|
+
Make sure MaxMind is properly configured if you're doing geolocation in background jobs:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# This works in controllers (has request)
|
|
364
|
+
Trackdown.locate(ip, request: request) # Uses Cloudflare if available
|
|
365
|
+
|
|
366
|
+
# This works in background jobs (no request)
|
|
367
|
+
Trackdown.locate(ip) # Falls back to MaxMind
|
|
368
|
+
```
|
|
254
369
|
|
|
255
370
|
## Development
|
|
256
371
|
|
data/Rakefile
CHANGED
|
@@ -4,13 +4,26 @@ require 'countries'
|
|
|
4
4
|
|
|
5
5
|
module Trackdown
|
|
6
6
|
class LocationResult
|
|
7
|
-
attr_reader :country_code, :country_name, :city, :flag_emoji
|
|
7
|
+
attr_reader :country_code, :country_name, :city, :flag_emoji,
|
|
8
|
+
:region, :region_code, :continent, :timezone, :latitude, :longitude,
|
|
9
|
+
:postal_code, :metro_code
|
|
8
10
|
|
|
9
|
-
def initialize(country_code, country_name, city, flag_emoji
|
|
11
|
+
def initialize(country_code, country_name, city, flag_emoji,
|
|
12
|
+
region: nil, region_code: nil, continent: nil,
|
|
13
|
+
timezone: nil, latitude: nil, longitude: nil,
|
|
14
|
+
postal_code: nil, metro_code: nil)
|
|
10
15
|
@country_code = country_code
|
|
11
16
|
@country_name = country_name
|
|
12
17
|
@city = city
|
|
13
18
|
@flag_emoji = flag_emoji
|
|
19
|
+
@region = region
|
|
20
|
+
@region_code = region_code
|
|
21
|
+
@continent = continent
|
|
22
|
+
@timezone = timezone
|
|
23
|
+
@latitude = latitude
|
|
24
|
+
@longitude = longitude
|
|
25
|
+
@postal_code = postal_code
|
|
26
|
+
@metro_code = metro_code
|
|
14
27
|
end
|
|
15
28
|
|
|
16
29
|
alias_method :country, :country_name
|
|
@@ -29,6 +42,14 @@ module Trackdown
|
|
|
29
42
|
country_name: @country_name,
|
|
30
43
|
city: @city,
|
|
31
44
|
flag_emoji: @flag_emoji,
|
|
45
|
+
region: @region,
|
|
46
|
+
region_code: @region_code,
|
|
47
|
+
continent: @continent,
|
|
48
|
+
timezone: @timezone,
|
|
49
|
+
latitude: @latitude,
|
|
50
|
+
longitude: @longitude,
|
|
51
|
+
postal_code: @postal_code,
|
|
52
|
+
metro_code: @metro_code,
|
|
32
53
|
country_info: country_info&.data || {}
|
|
33
54
|
}
|
|
34
55
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
3
5
|
require_relative 'base_provider'
|
|
4
6
|
require_relative 'cloudflare_provider'
|
|
5
7
|
require_relative 'maxmind_provider'
|
|
@@ -9,11 +11,19 @@ module Trackdown
|
|
|
9
11
|
# Intelligent provider that automatically selects the best available provider
|
|
10
12
|
# Priority order:
|
|
11
13
|
# 1. Cloudflare (fastest, zero overhead, no external dependencies)
|
|
12
|
-
# 2. MaxMind (fallback when Cloudflare not available)
|
|
14
|
+
# 2. MaxMind (fallback when Cloudflare not available or IP mismatch)
|
|
13
15
|
#
|
|
14
16
|
# This is the recommended default for most applications
|
|
17
|
+
#
|
|
18
|
+
# IMPORTANT: When there's an upstream proxy before Cloudflare (e.g., a legacy
|
|
19
|
+
# API gateway), Cloudflare's geo headers will reflect the proxy's location,
|
|
20
|
+
# not the real client. AutoProvider detects this by comparing CF-Connecting-IP
|
|
21
|
+
# with the passed IP and falls back to MaxMind when they don't match.
|
|
15
22
|
class AutoProvider < BaseProvider
|
|
23
|
+
CF_CONNECTING_IP_HEADER = 'HTTP_CF_CONNECTING_IP'
|
|
24
|
+
|
|
16
25
|
@@warned_no_providers = false
|
|
26
|
+
@@warned_ip_mismatch = false
|
|
17
27
|
@@warn_mutex = Mutex.new
|
|
18
28
|
|
|
19
29
|
class << self
|
|
@@ -29,8 +39,16 @@ module Trackdown
|
|
|
29
39
|
# @return [LocationResult] The location information
|
|
30
40
|
def locate(ip, request: nil)
|
|
31
41
|
# Try Cloudflare first - it's instant and free!
|
|
42
|
+
# But only if the IP matches what Cloudflare geolocated
|
|
32
43
|
if CloudflareProvider.available?(request: request)
|
|
33
|
-
|
|
44
|
+
if cloudflare_ip_matches?(ip, request)
|
|
45
|
+
return CloudflareProvider.locate(ip, request: request)
|
|
46
|
+
else
|
|
47
|
+
# IP mismatch: there's likely an upstream proxy before Cloudflare
|
|
48
|
+
# Cloudflare's geo headers are based on the proxy IP, not the real client
|
|
49
|
+
# Fall back to MaxMind with the correct IP
|
|
50
|
+
warn_ip_mismatch(ip, request)
|
|
51
|
+
end
|
|
34
52
|
end
|
|
35
53
|
|
|
36
54
|
# Fall back to MaxMind if available
|
|
@@ -45,6 +63,53 @@ module Trackdown
|
|
|
45
63
|
|
|
46
64
|
private
|
|
47
65
|
|
|
66
|
+
# Check if the IP we want to geolocate matches what Cloudflare saw as the client
|
|
67
|
+
# If they don't match, there's an upstream proxy and Cloudflare's geo headers are wrong
|
|
68
|
+
def cloudflare_ip_matches?(ip, request)
|
|
69
|
+
return true unless request # No request means we can't check
|
|
70
|
+
|
|
71
|
+
cf_connecting_ip = request.env[CF_CONNECTING_IP_HEADER]
|
|
72
|
+
return true if cf_connecting_ip.nil? || cf_connecting_ip.empty?
|
|
73
|
+
|
|
74
|
+
# Normalize IPs for comparison (handle IPv6 formatting differences)
|
|
75
|
+
normalize_ip(ip) == normalize_ip(cf_connecting_ip)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_ip(ip)
|
|
79
|
+
return nil if ip.nil?
|
|
80
|
+
|
|
81
|
+
value = ip.to_s.strip
|
|
82
|
+
return nil if value.empty?
|
|
83
|
+
|
|
84
|
+
parsed_ip = IPAddr.new(value)
|
|
85
|
+
parsed_ip = parsed_ip.native if parsed_ip.ipv4_mapped?
|
|
86
|
+
parsed_ip.to_s.downcase
|
|
87
|
+
rescue IPAddr::InvalidAddressError
|
|
88
|
+
# If parsing fails, fall back to string comparison so we still have
|
|
89
|
+
# deterministic behavior and can trigger MaxMind fallback on mismatch.
|
|
90
|
+
value.downcase
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def warn_ip_mismatch(ip, request)
|
|
94
|
+
return if @@warned_ip_mismatch
|
|
95
|
+
|
|
96
|
+
@@warn_mutex.synchronize do
|
|
97
|
+
return if @@warned_ip_mismatch
|
|
98
|
+
@@warned_ip_mismatch = true
|
|
99
|
+
|
|
100
|
+
cf_ip = request&.env&.dig(CF_CONNECTING_IP_HEADER)
|
|
101
|
+
message = "[Trackdown] IP mismatch detected: request IP (#{ip}) differs from " \
|
|
102
|
+
"CF-Connecting-IP (#{cf_ip}). This usually means there's an upstream " \
|
|
103
|
+
"proxy before Cloudflare. Falling back to MaxMind for accurate geolocation."
|
|
104
|
+
|
|
105
|
+
if defined?(Rails)
|
|
106
|
+
Rails.logger.info(message)
|
|
107
|
+
else
|
|
108
|
+
warn(message)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
48
113
|
def warn_no_providers
|
|
49
114
|
# Only warn once per process to avoid log spam
|
|
50
115
|
return if @@warned_no_providers
|
|
@@ -13,6 +13,14 @@ module Trackdown
|
|
|
13
13
|
class CloudflareProvider < BaseProvider
|
|
14
14
|
COUNTRY_HEADER = 'HTTP_CF_IPCOUNTRY'
|
|
15
15
|
CITY_HEADER = 'HTTP_CF_IPCITY'
|
|
16
|
+
REGION_HEADER = 'HTTP_CF_REGION'
|
|
17
|
+
REGION_CODE_HEADER = 'HTTP_CF_REGION_CODE'
|
|
18
|
+
LATITUDE_HEADER = 'HTTP_CF_IPLATITUDE'
|
|
19
|
+
LONGITUDE_HEADER = 'HTTP_CF_IPLONGITUDE'
|
|
20
|
+
TIMEZONE_HEADER = 'HTTP_CF_TIMEZONE'
|
|
21
|
+
CONTINENT_HEADER = 'HTTP_CF_IPCONTINENT'
|
|
22
|
+
METRO_CODE_HEADER = 'HTTP_CF_METRO_CODE'
|
|
23
|
+
POSTAL_CODE_HEADER = 'HTTP_CF_POSTAL_CODE'
|
|
16
24
|
|
|
17
25
|
# Special Cloudflare country codes
|
|
18
26
|
UNKNOWN_CODE = 'XX'
|
|
@@ -43,7 +51,17 @@ module Trackdown
|
|
|
43
51
|
city = extract_city(request)
|
|
44
52
|
flag_emoji = get_emoji_flag(country_code)
|
|
45
53
|
|
|
46
|
-
LocationResult.new(
|
|
54
|
+
LocationResult.new(
|
|
55
|
+
country_code, country_name, city, flag_emoji,
|
|
56
|
+
region: extract_header(request, REGION_HEADER),
|
|
57
|
+
region_code: extract_header(request, REGION_CODE_HEADER),
|
|
58
|
+
continent: extract_header(request, CONTINENT_HEADER),
|
|
59
|
+
timezone: extract_header(request, TIMEZONE_HEADER),
|
|
60
|
+
latitude: parse_coordinate(request.env[LATITUDE_HEADER]),
|
|
61
|
+
longitude: parse_coordinate(request.env[LONGITUDE_HEADER]),
|
|
62
|
+
postal_code: extract_header(request, POSTAL_CODE_HEADER),
|
|
63
|
+
metro_code: extract_header(request, METRO_CODE_HEADER)
|
|
64
|
+
)
|
|
47
65
|
end
|
|
48
66
|
|
|
49
67
|
private
|
|
@@ -64,6 +82,21 @@ module Trackdown
|
|
|
64
82
|
|
|
65
83
|
city
|
|
66
84
|
end
|
|
85
|
+
|
|
86
|
+
def extract_header(request, header)
|
|
87
|
+
value = request.env[header]
|
|
88
|
+
return nil if value.nil? || value.empty?
|
|
89
|
+
|
|
90
|
+
value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_coordinate(value)
|
|
94
|
+
return nil if value.nil? || value.empty?
|
|
95
|
+
|
|
96
|
+
Float(value)
|
|
97
|
+
rescue ArgumentError, TypeError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
67
100
|
end
|
|
68
101
|
end
|
|
69
102
|
end
|
|
@@ -48,7 +48,17 @@ module Trackdown
|
|
|
48
48
|
city = extract_city(record)
|
|
49
49
|
flag_emoji = get_emoji_flag(country_code)
|
|
50
50
|
|
|
51
|
-
LocationResult.new(
|
|
51
|
+
LocationResult.new(
|
|
52
|
+
country_code, country_name, city, flag_emoji,
|
|
53
|
+
region: extract_region(record),
|
|
54
|
+
region_code: record&.dig('subdivisions', 0, 'iso_code'),
|
|
55
|
+
continent: record&.dig('continent', 'code'),
|
|
56
|
+
timezone: record&.dig('location', 'time_zone'),
|
|
57
|
+
latitude: record&.dig('location', 'latitude'),
|
|
58
|
+
longitude: record&.dig('location', 'longitude'),
|
|
59
|
+
postal_code: record&.dig('postal', 'code'),
|
|
60
|
+
metro_code: record&.dig('location', 'metro_code')&.to_s
|
|
61
|
+
)
|
|
52
62
|
end
|
|
53
63
|
|
|
54
64
|
private
|
|
@@ -103,6 +113,11 @@ module Trackdown
|
|
|
103
113
|
(record&.dig('city', 'names')&.values&.first) ||
|
|
104
114
|
'Unknown'
|
|
105
115
|
end
|
|
116
|
+
|
|
117
|
+
def extract_region(record)
|
|
118
|
+
record&.dig('subdivisions', 0, 'names', 'en') ||
|
|
119
|
+
record&.dig('subdivisions', 0, 'names')&.values&.first
|
|
120
|
+
end
|
|
106
121
|
end
|
|
107
122
|
end
|
|
108
123
|
end
|
data/lib/trackdown/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trackdown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-02-24 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: countries
|
|
@@ -23,129 +23,19 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.0'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
version: '13.0'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '13.0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: rubocop
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1.21'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1.21'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: minitest
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '5.25'
|
|
61
|
-
type: :development
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '5.25'
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: mocha
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '2.0'
|
|
75
|
-
type: :development
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '2.0'
|
|
82
|
-
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: simplecov
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - "~>"
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '0.22'
|
|
89
|
-
type: :development
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - "~>"
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '0.22'
|
|
96
|
-
- !ruby/object:Gem::Dependency
|
|
97
|
-
name: webmock
|
|
98
|
-
requirement: !ruby/object:Gem::Requirement
|
|
99
|
-
requirements:
|
|
100
|
-
- - "~>"
|
|
101
|
-
- !ruby/object:Gem::Version
|
|
102
|
-
version: '3.0'
|
|
103
|
-
type: :development
|
|
104
|
-
prerelease: false
|
|
105
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
-
requirements:
|
|
107
|
-
- - "~>"
|
|
108
|
-
- !ruby/object:Gem::Version
|
|
109
|
-
version: '3.0'
|
|
110
|
-
- !ruby/object:Gem::Dependency
|
|
111
|
-
name: maxmind-db
|
|
112
|
-
requirement: !ruby/object:Gem::Requirement
|
|
113
|
-
requirements:
|
|
114
|
-
- - "~>"
|
|
115
|
-
- !ruby/object:Gem::Version
|
|
116
|
-
version: '1.2'
|
|
117
|
-
type: :development
|
|
118
|
-
prerelease: false
|
|
119
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
-
requirements:
|
|
121
|
-
- - "~>"
|
|
122
|
-
- !ruby/object:Gem::Version
|
|
123
|
-
version: '1.2'
|
|
124
|
-
- !ruby/object:Gem::Dependency
|
|
125
|
-
name: connection_pool
|
|
126
|
-
requirement: !ruby/object:Gem::Requirement
|
|
127
|
-
requirements:
|
|
128
|
-
- - "~>"
|
|
129
|
-
- !ruby/object:Gem::Version
|
|
130
|
-
version: '2.4'
|
|
131
|
-
type: :development
|
|
132
|
-
prerelease: false
|
|
133
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
-
requirements:
|
|
135
|
-
- - "~>"
|
|
136
|
-
- !ruby/object:Gem::Version
|
|
137
|
-
version: '2.4'
|
|
138
|
-
description: Trackdown is a Ruby gem that easily allows you to geolocate IP addresses.
|
|
139
|
-
It works out of the box with Cloudflare headers if you're using it, or you can use
|
|
140
|
-
MaxMind (BYOK). The gem offers a clean API for Rails applications to fetch country,
|
|
141
|
-
city, and emoji flag information for any IP address. Supports Cloudflare headers
|
|
142
|
-
(instant, zero overhead) and MaxMind GeoLite2 database (offline capable).
|
|
26
|
+
description: Trackdown is a Ruby gem that allows you to geolocate IP addresses easily.
|
|
27
|
+
It works out of the box with Cloudflare headers if your Rails app is behind it;
|
|
28
|
+
or you can also use MaxMind databases (BYOK). The gem offers a clean API for Rails
|
|
29
|
+
applications to fetch country, city, emoji flag, region, continent, postal code,
|
|
30
|
+
latitude, longitude and other GeoIP information for any IP address. Supports Cloudflare
|
|
31
|
+
headers, and MaxMind GeoLite2 databases (offline capable).
|
|
143
32
|
email:
|
|
144
33
|
- rubygems@rameerez.com
|
|
145
34
|
executables: []
|
|
146
35
|
extensions: []
|
|
147
36
|
extra_rdoc_files: []
|
|
148
37
|
files:
|
|
38
|
+
- ".simplecov"
|
|
149
39
|
- AGENTS.md
|
|
150
40
|
- CHANGELOG.md
|
|
151
41
|
- CLAUDE.md
|
|
@@ -173,6 +63,7 @@ licenses:
|
|
|
173
63
|
- MIT
|
|
174
64
|
metadata:
|
|
175
65
|
allowed_push_host: https://rubygems.org
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
176
67
|
homepage_uri: https://github.com/rameerez/trackdown
|
|
177
68
|
source_code_uri: https://github.com/rameerez/trackdown
|
|
178
69
|
changelog_uri: https://github.com/rameerez/trackdown/blob/main/CHANGELOG.md
|