footprinted 0.1.0 → 0.2.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/.simplecov +25 -0
- data/Appraisals +9 -0
- data/CHANGELOG.md +24 -1
- data/README.md +270 -104
- data/Rakefile +9 -1
- data/app/jobs/footprinted/track_job.rb +17 -0
- data/gemfiles/rails_7.2.gemfile +28 -0
- data/gemfiles/rails_7.2.gemfile.lock +302 -0
- data/gemfiles/rails_8.1.gemfile +28 -0
- data/gemfiles/rails_8.1.gemfile.lock +301 -0
- data/lib/footprinted/configuration.rb +2 -1
- data/lib/footprinted/footprint.rb +55 -0
- data/lib/footprinted/model.rb +45 -51
- data/lib/footprinted/version.rb +1 -1
- data/lib/footprinted.rb +1 -1
- data/lib/generators/footprinted/install_generator.rb +5 -1
- data/lib/generators/footprinted/templates/create_footprinted_footprints.rb.erb +43 -0
- metadata +23 -42
- data/lib/footprinted/trackable_activity.rb +0 -55
- data/lib/generators/footprinted/templates/create_footprinted_trackable_activities.rb.erb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0a6166d8616c0c1204631e17bc3ecf5c09cbff6fc94a38ee892061f2679b8c2
|
|
4
|
+
data.tar.gz: '0783420c75bf7f3736b0b4fcd84484281353f07d9fb89c1773066d25cb44dab7'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff0f0e67acc390309d8d8491f43efa63d001ae58f72768d041b88f5842a231d76a444d129067dad4daffda702a5cd9d377d08be51c87cd9c76d079fb3f937a79
|
|
7
|
+
data.tar.gz: ff2e6a3705a726900cc0c4bca87ec9c07ca635f3a1ff02f67464affce02a9f5d179fca9f33714c1b7cf5ba1f80577770362ed94a0a9f342709f17e672fedf403
|
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/Appraisals
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-02-08
|
|
4
|
+
|
|
5
|
+
**Full rewrite.** Breaking changes from v0.1.0.
|
|
6
|
+
|
|
7
|
+
- Rename `TrackableActivity` → `Footprint` (new table: `footprints`)
|
|
8
|
+
- Add `event_type` column for categorizing events (replaces multiple associations per type)
|
|
9
|
+
- Add JSONB `metadata` column with GIN index for arbitrary event data
|
|
10
|
+
- Add `performer` polymorphic reference (who triggered the event)
|
|
11
|
+
- Add `occurred_at` timestamp (defaults to `Time.current`)
|
|
12
|
+
- Add extended geo fields: `region`, `continent`, `timezone`, `latitude`, `longitude` (via trackdown 0.3+)
|
|
13
|
+
- Add generic `track(:event_type, ip:)` method for ad-hoc events
|
|
14
|
+
- Add scopes: `by_event`, `by_country`, `recent`, `last_days`, `between`, `performed_by`
|
|
15
|
+
- Add class methods: `.event_types`, `.countries`
|
|
16
|
+
- Add async mode with `Footprinted::TrackJob` (ActiveJob)
|
|
17
|
+
- Require trackdown ~> 0.3 for full geo field support
|
|
18
|
+
- Document JSONB performance at scale, column promotion pattern, and database compatibility
|
|
19
|
+
|
|
20
|
+
### Breaking changes
|
|
21
|
+
|
|
22
|
+
- Table renamed from `trackable_activities` to `footprints` — run the new generator and migrate
|
|
23
|
+
- Model renamed from `Footprinted::TrackableActivity` to `Footprinted::Footprint`
|
|
24
|
+
- `has_trackable` now creates `track_<singular>(ip:)` methods instead of the old API
|
|
25
|
+
|
|
3
26
|
## [0.1.0] - 2024-09-25
|
|
4
27
|
|
|
5
28
|
- Initial release
|
|
@@ -7,4 +30,4 @@
|
|
|
7
30
|
- Integrated with trackdown gem for IP geolocation
|
|
8
31
|
- Added customizable tracking associations
|
|
9
32
|
- Created install generator for easy setup
|
|
10
|
-
- Added configuration options
|
|
33
|
+
- Added configuration options
|
data/README.md
CHANGED
|
@@ -1,178 +1,344 @@
|
|
|
1
|
-
# 👣 `footprinted` -
|
|
1
|
+
# 👣 `footprinted` - Simple event tracking for Rails apps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/footprinted) [](https://github.com/rameerez/footprinted/actions)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=footprinted)**, 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=footprinted)!
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
`footprinted` makes it trivial to add event tracking to any Rails model.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
For example, let's say your users have profiles. Where has a particular profile been viewed from?
|
|
12
|
-
|
|
13
|
-
This gem makes it trivial to track and analyze this kind of data:
|
|
10
|
+
Think of a file transfer app like WeTransfer. You may want to track where every file download came from:
|
|
14
11
|
|
|
15
12
|
```ruby
|
|
16
|
-
#
|
|
17
|
-
has_trackable :
|
|
13
|
+
# Add to your model
|
|
14
|
+
has_trackable :downloads
|
|
18
15
|
|
|
19
|
-
#
|
|
20
|
-
@
|
|
16
|
+
# Track events in the controller
|
|
17
|
+
@file.track_download(ip: request.remote_ip, metadata: { version: "2.1.0" })
|
|
21
18
|
|
|
22
|
-
#
|
|
23
|
-
@
|
|
24
|
-
# =>
|
|
19
|
+
# Query the data
|
|
20
|
+
@file.downloads.by_country("US").last_days(30).count
|
|
21
|
+
# => 42
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
In the example above, `footprinted` adds `footprints` to your `File` model, allowing you to easily record event data; and provides you with methods to build dashboards and analytics / business intelligence systems.
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
More use cases:
|
|
27
|
+
- Track login attempts
|
|
28
|
+
- Track profile views in a social app (think: LinkedIn)
|
|
29
|
+
- Track document open events in a file-signing app (think: DocuSign)
|
|
30
|
+
- Track any business-critical operation for enterprise-compliant audit logs
|
|
31
|
+
- Track any interaction where knowing the *where* (IP, geolocation) or *what* (OS, app version, device ID...) matters
|
|
33
32
|
|
|
33
|
+
Every event (footprint) in `footprinted` records the IP address, full geolocation data (country, city, region, coordinates, timezone), arbitrary JSONB metadata, and who triggered it — all resolved automatically via [`trackdown`](https://github.com/rameerez/trackdown). `footprinted` allows you to trivially build analytics dashboards and audit logs for all your app events.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
> [!NOTE]
|
|
36
|
+
> By adding `has_trackable :downloads` to your model, `footprinted` automatically creates a `downloads` scoped association and a `track_download` method. No extra models or associations to define. Just track and query.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
## Installation
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
Add this to your Gemfile:
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- City
|
|
45
|
-
- Activity type
|
|
46
|
-
- Event timestamp
|
|
47
|
-
- Optionally, an associated `performer` record, which could be a `user`, `admin`, or any other model. It answers the question: "who triggered this activity?"
|
|
42
|
+
```ruby
|
|
43
|
+
gem "footprinted"
|
|
44
|
+
```
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
Then run:
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
```bash
|
|
49
|
+
bundle install
|
|
50
|
+
rails generate footprinted:install
|
|
51
|
+
rails db:migrate
|
|
52
|
+
```
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
This creates the `footprints` table with columns for IP, geolocation fields, event type, JSONB metadata, polymorphic trackable/performer references, and all the necessary indexes.
|
|
54
55
|
|
|
55
56
|
> [!IMPORTANT]
|
|
56
|
-
> This gem depends on
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
> This gem depends on [`trackdown`](https://github.com/rameerez/trackdown) for IP geolocation. `trackdown` works out of the box with Cloudflare (zero config) and also supports MaxMind. See the [trackdown README](https://github.com/rameerez/trackdown) for setup instructions.
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Include the concern in any model you want to track activity on:
|
|
61
62
|
|
|
62
63
|
```ruby
|
|
63
|
-
|
|
64
|
+
class Product < ApplicationRecord
|
|
65
|
+
include Footprinted::Model
|
|
66
|
+
|
|
67
|
+
has_trackable :activations
|
|
68
|
+
has_trackable :downloads
|
|
69
|
+
end
|
|
64
70
|
```
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
Track events in your controller:
|
|
67
73
|
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
```ruby
|
|
75
|
+
class DownloadsController < ApplicationController
|
|
76
|
+
def create
|
|
77
|
+
@product = Product.find(params[:product_id])
|
|
78
|
+
@product.track_download(ip: request.remote_ip, metadata: { version: params[:version] })
|
|
79
|
+
end
|
|
80
|
+
end
|
|
72
81
|
```
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
Query the data:
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
```ruby
|
|
86
|
+
@product.downloads.count # => 847
|
|
87
|
+
@product.downloads.by_country("US").count # => 529
|
|
88
|
+
@product.downloads.last_days(7) # recent downloads
|
|
89
|
+
@product.downloads.countries # => ["US", "UK", "CA", ...]
|
|
90
|
+
```
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
## `has_trackable` DSL
|
|
79
93
|
|
|
80
|
-
|
|
94
|
+
Declare one or more trackable event types on any model:
|
|
81
95
|
|
|
82
96
|
```ruby
|
|
83
|
-
class
|
|
97
|
+
class Resource < ApplicationRecord
|
|
84
98
|
include Footprinted::Model
|
|
85
|
-
|
|
86
|
-
# Track a single activity type
|
|
87
|
-
has_trackable :profile_views
|
|
88
|
-
|
|
89
|
-
# Track multiple activity types
|
|
99
|
+
|
|
90
100
|
has_trackable :downloads
|
|
91
|
-
has_trackable :
|
|
101
|
+
has_trackable :previews
|
|
102
|
+
has_trackable :shares
|
|
92
103
|
end
|
|
93
104
|
```
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
Each `has_trackable` call gives you:
|
|
96
107
|
|
|
97
|
-
`
|
|
108
|
+
- A **scoped association** (e.g., `resource.downloads`) that only returns footprints of that event type
|
|
109
|
+
- A **track method** (e.g., `resource.track_download(ip:)`) that creates footprints with the correct event type
|
|
98
110
|
|
|
99
|
-
|
|
111
|
+
The association name is pluralized, and the track method is singularized:
|
|
100
112
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
| Declaration | Association | Track method |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `has_trackable :profile_views` | `.profile_views` | `.track_profile_view(ip:)` |
|
|
116
|
+
| `has_trackable :downloads` | `.downloads` | `.track_download(ip:)` |
|
|
117
|
+
| `has_trackable :login_attempts` | `.login_attempts` | `.track_login_attempt(ip:)` |
|
|
118
|
+
|
|
119
|
+
### Track method parameters
|
|
104
120
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
ip: request.remote_ip,
|
|
108
|
-
|
|
121
|
+
```ruby
|
|
122
|
+
@resource.track_download(
|
|
123
|
+
ip: request.remote_ip, # Required: the IP address
|
|
124
|
+
request: request, # Optional: passed to Trackdown for better geolocation
|
|
125
|
+
performer: current_user, # Optional: who triggered this activity
|
|
126
|
+
metadata: { browser: "Chrome" },# Optional: arbitrary JSONB metadata
|
|
127
|
+
occurred_at: 2.hours.ago # Optional: defaults to Time.current
|
|
109
128
|
)
|
|
110
129
|
```
|
|
111
130
|
|
|
112
|
-
|
|
131
|
+
## Generic `track()` method
|
|
113
132
|
|
|
114
|
-
|
|
133
|
+
For ad-hoc event types that don't need a dedicated association, use the generic `track` method:
|
|
115
134
|
|
|
116
135
|
```ruby
|
|
117
|
-
|
|
118
|
-
user.
|
|
119
|
-
user.
|
|
120
|
-
|
|
136
|
+
@user.track(:signup, ip: request.remote_ip)
|
|
137
|
+
@user.track(:password_reset, ip: request.remote_ip, performer: admin)
|
|
138
|
+
@user.track("api_call", ip: request.remote_ip, metadata: { endpoint: "/users" })
|
|
139
|
+
```
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
user.profile_views.by_country('US')
|
|
124
|
-
user.profile_views.countries # => ['US', 'UK', 'CA', ...]
|
|
141
|
+
It accepts the same parameters as `track_<event_type>` and works with both symbols and strings. All events tracked this way are accessible through the `footprints` association:
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
user.
|
|
143
|
+
```ruby
|
|
144
|
+
@user.footprints.by_event("signup").count
|
|
128
145
|
```
|
|
129
146
|
|
|
130
|
-
|
|
147
|
+
## Scopes
|
|
131
148
|
|
|
132
|
-
|
|
149
|
+
`footprinted` provides several useful scopes out of the box:
|
|
133
150
|
|
|
134
151
|
```ruby
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
end
|
|
152
|
+
# Filter by event type
|
|
153
|
+
@user.footprints.by_event("download")
|
|
154
|
+
|
|
155
|
+
# Filter by country code
|
|
156
|
+
@user.footprints.by_country("US")
|
|
141
157
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
product.track_preview(ip: request.remote_ip)
|
|
158
|
+
# Order by most recent
|
|
159
|
+
@user.footprints.recent
|
|
145
160
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
# Time-based filtering
|
|
162
|
+
@user.footprints.last_days(30)
|
|
163
|
+
@user.footprints.between(1.week.ago, Time.current)
|
|
164
|
+
|
|
165
|
+
# Filter by performer
|
|
166
|
+
@user.footprints.performed_by(some_user)
|
|
150
167
|
```
|
|
151
168
|
|
|
152
|
-
|
|
169
|
+
Scopes are chainable:
|
|
153
170
|
|
|
154
171
|
```ruby
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
@user.profile_views
|
|
173
|
+
.by_country("US")
|
|
174
|
+
.last_days(7)
|
|
175
|
+
.performed_by(current_user)
|
|
176
|
+
.recent
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Class methods
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# Get all distinct event types
|
|
183
|
+
Footprinted::Footprint.event_types
|
|
184
|
+
# => ["view", "download", "login"]
|
|
185
|
+
|
|
186
|
+
# Get all distinct country codes (excludes nil)
|
|
187
|
+
Footprinted::Footprint.countries
|
|
188
|
+
# => ["US", "UK", "CA", "DE"]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## JSONB metadata
|
|
192
|
+
|
|
193
|
+
Every footprint can store arbitrary metadata as JSONB. This is great for device info, SDK versions, or any context you want to associate with the event:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
@product.track(:activation, ip: request.remote_ip, metadata: {
|
|
197
|
+
device_id: "A1B2C3",
|
|
198
|
+
app_version: "2.1.0",
|
|
199
|
+
platform: "macOS",
|
|
200
|
+
os_version: "15.2",
|
|
201
|
+
sdk_version: "0.4.0",
|
|
202
|
+
locale: "en_US"
|
|
203
|
+
})
|
|
204
|
+
```
|
|
162
205
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
206
|
+
Query metadata using your database's JSON operators:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# Find activations from macOS
|
|
210
|
+
@product.footprints
|
|
211
|
+
.by_event("activation")
|
|
212
|
+
.where("metadata->>'platform' = ?", "macOS")
|
|
213
|
+
|
|
214
|
+
# Group by app version
|
|
215
|
+
@product.footprints
|
|
216
|
+
.by_event("activation")
|
|
217
|
+
.group("metadata->>'app_version'")
|
|
166
218
|
.count
|
|
167
|
-
# => {0=>
|
|
219
|
+
# => { "2.0.0" => 12, "2.1.0" => 45 }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Performance at scale
|
|
223
|
+
|
|
224
|
+
The default migration creates a [GIN index](https://www.postgresql.org/docs/current/gin-intro.html) on the `metadata` column. GIN indexes are excellent for **containment queries** (`@>`, `?`, `?|`) but do **not** speed up key extraction queries like `GROUP BY metadata->>'field'` or `COUNT(DISTINCT metadata->>'field')`.
|
|
225
|
+
|
|
226
|
+
For small-to-medium tables (up to hundreds of thousands of rows), JSONB queries work just fine. At larger scale (millions of rows), if you're frequently grouping or counting distinct values on specific metadata keys, you have two options:
|
|
227
|
+
|
|
228
|
+
**Option 1: Expression indexes** — add B-tree indexes on the specific JSONB keys you query most. No schema change needed:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
# In a migration in your host app
|
|
232
|
+
add_index :footprints, "(metadata->>'device_id')", name: "idx_footprints_device_id"
|
|
233
|
+
add_index :footprints, "(metadata->>'app_version')", name: "idx_footprints_app_version"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Option 2: Promote to columns** — for your hottest query paths (e.g., `device_id` for DAU/MAU), add dedicated columns to the `footprints` table in your host app. This gives you proper B-tree indexes and fast `DISTINCT` counts:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# In a migration in your host app
|
|
240
|
+
add_column :footprints, :device_id, :string
|
|
241
|
+
add_column :footprints, :app_version, :string
|
|
242
|
+
add_column :footprints, :platform, :string
|
|
243
|
+
|
|
244
|
+
add_index :footprints, :device_id
|
|
245
|
+
add_index :footprints, :app_version
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Then write to both the column and the metadata hash in your tracking code. The gem stays generic; your app adds the columns it needs.
|
|
249
|
+
|
|
250
|
+
> [!TIP]
|
|
251
|
+
> 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.
|
|
252
|
+
|
|
253
|
+
### Database compatibility
|
|
254
|
+
|
|
255
|
+
| Feature | PostgreSQL | MySQL 5.7+ | SQLite |
|
|
256
|
+
|---|---|---|---|
|
|
257
|
+
| JSONB `metadata` column | `jsonb` (native, fast) | `json` (native) | `text` (stored as string) |
|
|
258
|
+
| GIN index on `metadata` | Supported | Not supported | Not supported |
|
|
259
|
+
| JSON queries (`->>`, `@>`) | Full support | `JSON_EXTRACT()` syntax | `json_extract()` via extension |
|
|
260
|
+
| Expression indexes | Supported | Supported (generated columns) | Not supported |
|
|
261
|
+
|
|
262
|
+
PostgreSQL is the recommended database for `footprinted`. It has the best JSONB support, GIN indexes for containment queries, and expression indexes for key extraction. MySQL works but uses different JSON syntax. SQLite works for development and testing but stores metadata as text and has limited JSON query support.
|
|
263
|
+
|
|
264
|
+
## Async mode with ActiveJob
|
|
265
|
+
|
|
266
|
+
For high-traffic endpoints, you can enqueue footprint creation in the background:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# config/initializers/footprinted.rb
|
|
270
|
+
Footprinted.configure do |config|
|
|
271
|
+
config.async = true
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
When async is enabled, `track` and `track_<event_type>` calls enqueue a `Footprinted::TrackJob` instead of writing to the database immediately. The job serializes all attributes (including metadata as a hash and occurred_at as ISO 8601) and processes them in the background.
|
|
276
|
+
|
|
277
|
+
You need a working ActiveJob backend (Sidekiq, Solid Queue, etc.) for this to work.
|
|
278
|
+
|
|
279
|
+
## Geolocation via Trackdown
|
|
280
|
+
|
|
281
|
+
`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:
|
|
282
|
+
|
|
283
|
+
| Field | Example |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `country_code` | `"US"` |
|
|
286
|
+
| `country_name` | `"United States"` |
|
|
287
|
+
| `city` | `"San Francisco"` |
|
|
288
|
+
| `region` | `"California"` |
|
|
289
|
+
| `continent` | `"NA"` |
|
|
290
|
+
| `timezone` | `"America/Los_Angeles"` |
|
|
291
|
+
| `latitude` | `37.7749` |
|
|
292
|
+
| `longitude` | `-122.4194` |
|
|
293
|
+
|
|
294
|
+
If geolocation fails (network error, invalid IP, etc.), the footprint is still saved — just without geolocation data. Errors are logged via `Rails.logger`.
|
|
295
|
+
|
|
296
|
+
If you already know the `country_code`, you can set it directly and geolocation will be skipped:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
@user.track_profile_view(ip: "1.2.3.4", country_code: "DE")
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Configuration
|
|
303
|
+
|
|
304
|
+
Create an initializer (the generator does this for you):
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# config/initializers/footprinted.rb
|
|
308
|
+
Footprinted.configure do |config|
|
|
309
|
+
# Enqueue footprint creation via ActiveJob (default: false)
|
|
310
|
+
config.async = false
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Generator
|
|
315
|
+
|
|
316
|
+
The install generator creates two things:
|
|
317
|
+
|
|
318
|
+
1. A migration for the `footprints` table with all geolocation columns, polymorphic references, JSONB metadata, indexes, and a composite index on `[trackable_type, trackable_id, event_type, occurred_at]`
|
|
319
|
+
2. An initializer at `config/initializers/footprinted.rb`
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
rails generate footprinted:install
|
|
323
|
+
rails db:migrate
|
|
168
324
|
```
|
|
169
325
|
|
|
170
326
|
## Development
|
|
171
327
|
|
|
172
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
|
|
328
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
173
329
|
|
|
174
330
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
175
331
|
|
|
332
|
+
### Releasing a new version
|
|
333
|
+
|
|
334
|
+
When bumping the version, update all three of these together:
|
|
335
|
+
|
|
336
|
+
1. `lib/footprinted/version.rb` — the version constant
|
|
337
|
+
2. `gemfiles/*.gemfile.lock` — run `bundle exec appraisal install` to regenerate
|
|
338
|
+
3. `test/footprinted/version_test.rb` — the hardcoded version assertion
|
|
339
|
+
|
|
340
|
+
CI will fail if any of these are out of sync.
|
|
341
|
+
|
|
176
342
|
## Contributing
|
|
177
343
|
|
|
178
344
|
Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/footprinted. Our code of conduct is: just be nice and make your mom proud of what you do and post online.
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task default: :test
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Footprinted
|
|
4
|
+
class TrackJob < ActiveJob::Base
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform(trackable_type, trackable_id, attributes)
|
|
8
|
+
trackable = trackable_type.constantize.find_by(id: trackable_id)
|
|
9
|
+
return unless trackable
|
|
10
|
+
|
|
11
|
+
attrs = attributes.symbolize_keys
|
|
12
|
+
attrs[:occurred_at] = Time.parse(attrs[:occurred_at]) if attrs[:occurred_at].is_a?(String)
|
|
13
|
+
|
|
14
|
+
trackable.footprints.create!(attrs)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 7.2.0"
|
|
7
|
+
|
|
8
|
+
group :development do
|
|
9
|
+
gem "irb"
|
|
10
|
+
gem "rubocop", "~> 1.0"
|
|
11
|
+
gem "rubocop-minitest", "~> 0.35"
|
|
12
|
+
gem "rubocop-performance", "~> 1.0"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
group :development, :test do
|
|
16
|
+
gem "appraisal"
|
|
17
|
+
gem "minitest", "~> 6.0"
|
|
18
|
+
gem "minitest-mock"
|
|
19
|
+
gem "minitest-reporters", "~> 1.6"
|
|
20
|
+
gem "mocha", "~> 2.0"
|
|
21
|
+
gem "rack-test"
|
|
22
|
+
gem "sqlite3", ">= 2.1"
|
|
23
|
+
gem "ostruct"
|
|
24
|
+
gem "webmock", "~> 3.19"
|
|
25
|
+
gem "simplecov", require: false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
gemspec path: "../"
|