footprinted 0.2.0 → 0.3.0
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/CHANGELOG.md +12 -0
- data/README.md +50 -1
- data/app/jobs/footprinted/track_job.rb +7 -0
- data/gemfiles/rails_7.2.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/footprinted/{railtie.rb → engine.rb} +1 -1
- data/lib/footprinted/model.rb +24 -0
- data/lib/footprinted/version.rb +1 -1
- data/lib/footprinted.rb +1 -1
- data/lib/generators/footprinted/install_generator.rb +28 -8
- data/lib/generators/footprinted/templates/create_footprinted_footprints.rb.erb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz: '
|
|
3
|
+
metadata.gz: 20ea0ce295c5fe9031013e323a46e744b92de81e6e433db994dc4df485373e31
|
|
4
|
+
data.tar.gz: '09c5e324348d00dcb53b8fbe7e581e22ee45e9d194f706d48685678cc999359a'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69f4c70cdddadd0bcf208bcecc337ab484ae5ac6991748488e4620a5d712c69ae6b444b26ceb6c0ad10a036c4652185a938509c203348d16b5cac0a28ae9c7a4
|
|
7
|
+
data.tar.gz: 5aaa74017232c3f54cc6db64155d75d762a7f758f04a7e886474e7af9cd579173264c744447f86f0eae56b84b096cb984f462bd40105e35a1ea80e1e8283448f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-02-15
|
|
4
|
+
|
|
5
|
+
- **Async mode now extracts geo data at enqueue time** when `request:` is passed
|
|
6
|
+
- Cloudflare headers are captured before the job is enqueued
|
|
7
|
+
- Background jobs no longer need MaxMind to get geo data
|
|
8
|
+
- Falls back to MaxMind lookup in the job if `request:` wasn't passed
|
|
9
|
+
- No breaking changes — existing code works unchanged
|
|
10
|
+
|
|
11
|
+
## [0.2.1] - 2026-02-09
|
|
12
|
+
|
|
13
|
+
- Fix async mode: change Railtie to Engine so `TrackJob` is autoloaded
|
|
14
|
+
|
|
3
15
|
## [0.2.0] - 2026-02-08
|
|
4
16
|
|
|
5
17
|
**Full rewrite.** Breaking changes from v0.1.0.
|
data/README.md
CHANGED
|
@@ -245,7 +245,41 @@ add_index :footprints, :device_id
|
|
|
245
245
|
add_index :footprints, :app_version
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
Your tracking calls stay the same — just pass everything in `metadata` as before. To auto-promote metadata keys into their dedicated columns, add a `before_save` callback in an initializer:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
# config/initializers/footprinted_extensions.rb
|
|
252
|
+
|
|
253
|
+
# String columns that map 1:1 from metadata
|
|
254
|
+
FOOTPRINT_PROMOTED_STRING_COLUMNS = %w[device_id app_version platform].freeze
|
|
255
|
+
|
|
256
|
+
# Integer columns that need casting
|
|
257
|
+
FOOTPRINT_PROMOTED_INTEGER_COLUMNS = %w[cpu_cores memory_gb].freeze
|
|
258
|
+
|
|
259
|
+
Rails.configuration.to_prepare do
|
|
260
|
+
Footprinted::Footprint.class_eval do
|
|
261
|
+
before_save :promote_metadata_columns
|
|
262
|
+
|
|
263
|
+
private
|
|
264
|
+
|
|
265
|
+
def promote_metadata_columns
|
|
266
|
+
return if metadata.blank?
|
|
267
|
+
|
|
268
|
+
m = metadata.stringify_keys
|
|
269
|
+
|
|
270
|
+
FOOTPRINT_PROMOTED_STRING_COLUMNS.each do |key|
|
|
271
|
+
self[key] = m[key] if self[key].blank? && m[key].present?
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
FOOTPRINT_PROMOTED_INTEGER_COLUMNS.each do |key|
|
|
275
|
+
self[key] = m[key].to_i if self[key].blank? && m[key].present?
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
This works regardless of how footprints are created — via `TrackJob` (async), direct `.create!`, or the Rails console. The gem stays generic; your app adds the columns and promotion logic it needs.
|
|
249
283
|
|
|
250
284
|
> [!TIP]
|
|
251
285
|
> Which metadata keys to promote depends on your use case. A licensing SaaS might promote `device_id` + `app_version`. An e-commerce app might promote `product_id` + `session_id`. A CMS might promote `page_url` + `referrer`. Keep the JSONB for everything else.
|
|
@@ -276,6 +310,21 @@ When async is enabled, `track` and `track_<event_type>` calls enqueue a `Footpri
|
|
|
276
310
|
|
|
277
311
|
You need a working ActiveJob backend (Sidekiq, Solid Queue, etc.) for this to work.
|
|
278
312
|
|
|
313
|
+
### Geolocation in async mode
|
|
314
|
+
|
|
315
|
+
When you pass `request:` in async mode, geolocation data is extracted **immediately** (before enqueueing) using Cloudflare headers via [`trackdown`](https://github.com/rameerez/trackdown). This means:
|
|
316
|
+
|
|
317
|
+
- **No MaxMind database needed** in your background workers if you use Cloudflare
|
|
318
|
+
- Geo data is captured at request time, not in the job
|
|
319
|
+
- If `request:` is not passed, the job falls back to MaxMind lookup
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
# In your controller — pass request: for Cloudflare geo extraction
|
|
323
|
+
@product.track(:purchase, ip: request.remote_ip, request: request)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
This is the recommended pattern for async mode behind Cloudflare.
|
|
327
|
+
|
|
279
328
|
## Geolocation via Trackdown
|
|
280
329
|
|
|
281
330
|
`footprinted` automatically resolves geolocation data from IP addresses using the [`trackdown`](https://github.com/rameerez/trackdown) gem. For every footprint, the following fields are populated:
|
|
@@ -11,6 +11,13 @@ module Footprinted
|
|
|
11
11
|
attrs = attributes.symbolize_keys
|
|
12
12
|
attrs[:occurred_at] = Time.parse(attrs[:occurred_at]) if attrs[:occurred_at].is_a?(String)
|
|
13
13
|
|
|
14
|
+
# Log geo data status for debugging
|
|
15
|
+
if attrs[:country_code].present?
|
|
16
|
+
Rails.logger.debug { "[Footprinted] TrackJob received pre-extracted geo: #{attrs[:country_code]}/#{attrs[:city]}" }
|
|
17
|
+
else
|
|
18
|
+
Rails.logger.debug { "[Footprinted] TrackJob has no pre-extracted geo, will attempt lookup for #{attrs[:ip]}" }
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
trackable.footprints.create!(attrs)
|
|
15
22
|
end
|
|
16
23
|
end
|
data/lib/footprinted/model.rb
CHANGED
|
@@ -28,6 +28,7 @@ module Footprinted
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if Footprinted.configuration.async
|
|
31
|
+
Footprinted::Model.enrich_with_geo_data!(attrs, ip, request)
|
|
31
32
|
Footprinted::TrackJob.perform_later(
|
|
32
33
|
self.class.name, id,
|
|
33
34
|
attrs.merge(occurred_at: attrs[:occurred_at].iso8601)
|
|
@@ -52,6 +53,7 @@ module Footprinted
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
if Footprinted.configuration.async
|
|
56
|
+
Footprinted::Model.enrich_with_geo_data!(attrs, ip, request)
|
|
55
57
|
Footprinted::TrackJob.perform_later(
|
|
56
58
|
self.class.name, id,
|
|
57
59
|
attrs.merge(occurred_at: attrs[:occurred_at].iso8601)
|
|
@@ -63,5 +65,27 @@ module Footprinted
|
|
|
63
65
|
record
|
|
64
66
|
end
|
|
65
67
|
end
|
|
68
|
+
|
|
69
|
+
# Extract geo data from request (Cloudflare headers) before enqueueing
|
|
70
|
+
# This allows async jobs to have geo data without needing MaxMind
|
|
71
|
+
def self.enrich_with_geo_data!(attrs, ip, request)
|
|
72
|
+
return unless request && defined?(Trackdown)
|
|
73
|
+
|
|
74
|
+
location = Trackdown.locate(ip.to_s, request: request)
|
|
75
|
+
attrs.merge!(
|
|
76
|
+
country_code: location.country_code,
|
|
77
|
+
country_name: location.country_name,
|
|
78
|
+
city: location.city,
|
|
79
|
+
region: location.region,
|
|
80
|
+
continent: location.continent,
|
|
81
|
+
timezone: location.timezone,
|
|
82
|
+
latitude: location.latitude,
|
|
83
|
+
longitude: location.longitude
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if location.country_code.present?
|
|
87
|
+
Rails.logger.debug { "[Footprinted] Extracted geo at enqueue: #{location.country_code}/#{location.city} for #{ip}" }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
66
90
|
end
|
|
67
91
|
end
|
data/lib/footprinted/version.rb
CHANGED
data/lib/footprinted.rb
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
5
|
|
|
6
6
|
module Footprinted
|
|
7
7
|
module Generators
|
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
|
9
9
|
include ActiveRecord::Generators::Migration
|
|
10
10
|
|
|
11
|
-
source_root File.expand_path(
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
12
|
|
|
13
|
-
def self.next_migration_number(
|
|
14
|
-
|
|
13
|
+
def self.next_migration_number(dirname)
|
|
14
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
15
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def create_migration_file
|
|
18
|
-
migration_template
|
|
19
|
+
migration_template "create_footprinted_footprints.rb.erb",
|
|
20
|
+
File.join(db_migrate_path, "create_footprinted_footprints.rb")
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def create_initializer
|
|
22
|
-
template
|
|
24
|
+
template "footprinted.rb", "config/initializers/footprinted.rb"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def display_post_install_message
|
|
28
|
+
say "\n🎉 The `footprinted` gem has been successfully installed!", :green
|
|
29
|
+
say "\nTo complete the setup:"
|
|
30
|
+
say " 1. Run `rails db:migrate` to create the footprints table."
|
|
31
|
+
say " ⚠️ You must run migrations before starting your app!", :yellow
|
|
32
|
+
say "\n 2. Add `include Footprinted::Model` to any model you want to track:"
|
|
33
|
+
say " class Product < ApplicationRecord"
|
|
34
|
+
say " include Footprinted::Model"
|
|
35
|
+
say " end"
|
|
36
|
+
say "\n 3. Create footprints from your controllers or services:"
|
|
37
|
+
say " product.footprints.create!("
|
|
38
|
+
say " ip: request.remote_ip,"
|
|
39
|
+
say " event_type: 'page_view',"
|
|
40
|
+
say " occurred_at: Time.current"
|
|
41
|
+
say " )"
|
|
42
|
+
say "\nSee the footprinted README for detailed usage and examples.", :cyan
|
|
43
|
+
say "Happy tracking! 👣\n", :green
|
|
23
44
|
end
|
|
24
45
|
|
|
25
46
|
private
|
|
@@ -27,7 +48,6 @@ module Footprinted
|
|
|
27
48
|
def migration_version
|
|
28
49
|
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
|
29
50
|
end
|
|
30
|
-
|
|
31
51
|
end
|
|
32
52
|
end
|
|
33
53
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: footprinted
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-02-
|
|
10
|
+
date: 2026-02-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -64,9 +64,9 @@ files:
|
|
|
64
64
|
- gemfiles/rails_8.1.gemfile.lock
|
|
65
65
|
- lib/footprinted.rb
|
|
66
66
|
- lib/footprinted/configuration.rb
|
|
67
|
+
- lib/footprinted/engine.rb
|
|
67
68
|
- lib/footprinted/footprint.rb
|
|
68
69
|
- lib/footprinted/model.rb
|
|
69
|
-
- lib/footprinted/railtie.rb
|
|
70
70
|
- lib/footprinted/version.rb
|
|
71
71
|
- lib/generators/footprinted/install_generator.rb
|
|
72
72
|
- lib/generators/footprinted/templates/create_footprinted_footprints.rb.erb
|