rails-persona 0.2.0 → 0.2.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/CHANGELOG.md +20 -14
- data/README.md +270 -270
- data/lib/persona/railtie.rb +4 -9
- data/lib/persona/version.rb +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 504b3d7d32f878f9b512a0d70ce5b5b6870fb311cfdb097d023c564a41716127
|
|
4
|
+
data.tar.gz: 5869948b33c80f3f1bf744ba0edc7875c1af22cb392f0fbabc8726aa70359cc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96ebda806fbdd89448dea893b5befb3e17c82d5e1be9dd4d63103e93df29a2ef34a46cfbe8b24228b96012b046e7e7f273020df92bf018fb1981fa934b918fa2
|
|
7
|
+
data.tar.gz: e12d8d2b32ec67d57a30dfad49cfcd78ac0ac0daf4cf3596a9d1cbf1ca5595f7285bdf629359554ad22caf05d90915f384b010866f931bfec38c2da997f4ffe5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [0.1
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
- `
|
|
7
|
-
- `
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- `
|
|
13
|
-
-
|
|
14
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.1] - 2026-06-01
|
|
4
|
+
### Fixed
|
|
5
|
+
- Replace `config.paths` in Railtie with `File.expand_path` to fix `NoMethodError` on boot (fixes #1)
|
|
6
|
+
- Add missing `tasks/persona_tasks.rake` that was referenced but never created (fixes #2)
|
|
7
|
+
- Fix rake task load path using `__dir__` instead of relative path
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2024-05-31
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `Persona::Trackable` concern with DSL (`persona do track :action end`)
|
|
13
|
+
- `track!` instance method with optional metadata
|
|
14
|
+
- Full query API: `action_count`, `most_frequent_action`, `least_frequent_action`,
|
|
15
|
+
`last_action`, `last_active_at`, `inactive_since?`, `ever_did?`,
|
|
16
|
+
`persona_summary`, `actions_between`, `activity_log`
|
|
17
|
+
- `Persona::Configuration` with `inactivity_threshold_days` and `max_events_per_record`
|
|
18
|
+
- `UntrackedActionError` for safety when tracking undeclared actions
|
|
19
|
+
- Database migration with polymorphic `persona_events` table
|
|
20
|
+
- Railtie for automatic migration path injection
|
data/README.md
CHANGED
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
# rails-persona 🎭
|
|
2
|
-
|
|
3
|
-
> Model-level behavioral analytics for Rails — own your data, zero external services.
|
|
4
|
-
|
|
5
|
-
[](LICENSE)
|
|
7
|
-
|
|
8
|
-
**rails-persona** is a lightweight Rails gem that adds first-class behavioral tracking directly to your ActiveRecord models. Unlike [ahoy](https://github.com/ankane/ahoy), which is focused on HTTP visit and page-view tracking, rails-persona is built for **model-level action tracking** — understanding *what your users actually do* in your app, not just what pages they visit.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Why rails-persona over ahoy?
|
|
13
|
-
|
|
14
|
-
| | ahoy | rails-persona |
|
|
15
|
-
|---|---|---|
|
|
16
|
-
| Focus | HTTP visits + page views | Model actions + user behavior |
|
|
17
|
-
| Setup | Controllers + JS snippet | Pure Ruby — one concern |
|
|
18
|
-
| Async | Manual Sidekiq setup | Built-in (`async: true`) |
|
|
19
|
-
| Bulk tracking | ❌ | ✅ `bulk_track!` with `insert_all!` |
|
|
20
|
-
| Class-level analytics | ❌ | ✅ Leaderboards, class summaries |
|
|
21
|
-
| Open tracking mode | ❌ | ✅ No whitelist required |
|
|
22
|
-
| Streak / pattern queries | ❌ | ✅ `daily_activity`, `peak_hour` |
|
|
23
|
-
| Cookies / sessions | Required | Never needed |
|
|
24
|
-
| Works on non-User models | Awkward | First-class |
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Installation
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
gem "rails-persona"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
bundle install
|
|
36
|
-
rails db:migrate
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Quick start
|
|
42
|
-
|
|
43
|
-
### 1. Include in any model
|
|
44
|
-
|
|
45
|
-
```ruby
|
|
46
|
-
class User < ApplicationRecord
|
|
47
|
-
include Persona::Trackable
|
|
48
|
-
|
|
49
|
-
persona do
|
|
50
|
-
track :login
|
|
51
|
-
track :export_report
|
|
52
|
-
track :view_dashboard
|
|
53
|
-
track :upgrade_plan
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 2. Track actions in your app
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
# In a controller, service, or job:
|
|
62
|
-
current_user.track!(:login)
|
|
63
|
-
current_user.track!(:upgrade_plan, metadata: { plan: "pro", amount: 49 })
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### 3. Query behavior
|
|
67
|
-
|
|
68
|
-
```ruby
|
|
69
|
-
user.action_count(:login) # => 42
|
|
70
|
-
user.most_frequent_action # => :login
|
|
71
|
-
user.least_frequent_action # => :upgrade_plan
|
|
72
|
-
user.top_actions(3) # => { login: 42, view_dashboard: 18, export_report: 5 }
|
|
73
|
-
user.last_action # => :export_report
|
|
74
|
-
user.last_active_at # => 2024-05-30 14:22 UTC
|
|
75
|
-
user.first_action # => :login
|
|
76
|
-
user.first_active_at # => 2023-01-10 08:00 UTC
|
|
77
|
-
user.inactive_since? # => false (default threshold: 30 days)
|
|
78
|
-
user.inactive_since?(7) # => false (custom: 7 days)
|
|
79
|
-
user.days_since_last_activity # => 2
|
|
80
|
-
user.ever_did?(:export_report) # => true
|
|
81
|
-
user.never_did?(:upgrade_plan) # => false
|
|
82
|
-
user.action_share(:login) # => 64.6 (% of all events)
|
|
83
|
-
user.total_events # => 65
|
|
84
|
-
|
|
85
|
-
user.persona_summary
|
|
86
|
-
# => { login: 42, view_dashboard: 18, export_report: 5, upgrade_plan: 1 }
|
|
87
|
-
|
|
88
|
-
user.actions_between(1.week.ago, Time.current)
|
|
89
|
-
# => { login: 7, view_dashboard: 3 }
|
|
90
|
-
|
|
91
|
-
user.activity_log(5)
|
|
92
|
-
# => [
|
|
93
|
-
# { action: :export_report, at: 2024-05-30 14:22:00, metadata: {} },
|
|
94
|
-
# { action: :login, at: 2024-05-30 09:01:00, metadata: {} },
|
|
95
|
-
# ]
|
|
96
|
-
|
|
97
|
-
user.daily_activity(30)
|
|
98
|
-
# => { "2024-05-28" => 4, "2024-05-29" => 7, "2024-05-30" => 2 }
|
|
99
|
-
|
|
100
|
-
user.peak_hour
|
|
101
|
-
# => 14 (2pm is when this user is most active)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## Class-level analytics
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
# Top 10 most active users
|
|
110
|
-
User.persona_leaderboard(limit: 10)
|
|
111
|
-
# => [
|
|
112
|
-
# { record: #<User id=4>, total_events: 128 },
|
|
113
|
-
# { record: #<User id=9>, total_events: 97 },
|
|
114
|
-
# ]
|
|
115
|
-
|
|
116
|
-
# App-wide breakdown of all user actions
|
|
117
|
-
User.persona_class_summary
|
|
118
|
-
# => { login: 8420, view_dashboard: 5210, export_report: 820 }
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## Bulk tracking (high-performance)
|
|
124
|
-
|
|
125
|
-
Uses `insert_all!` — no N+1, no per-row callbacks:
|
|
126
|
-
|
|
127
|
-
```ruby
|
|
128
|
-
user.bulk_track!([:login, :view_dashboard, :export_report])
|
|
129
|
-
user.bulk_track!([:login, :login, :login]) # track repeated actions
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## Async tracking (Sidekiq)
|
|
135
|
-
|
|
136
|
-
```ruby
|
|
137
|
-
# config/initializers/persona.rb
|
|
138
|
-
Persona.configure do |config|
|
|
139
|
-
config.async = true # fires a Sidekiq job instead of writing inline
|
|
140
|
-
end
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Requires the `sidekiq` gem. Falls back to synchronous if Sidekiq is not available.
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Open tracking (no whitelist)
|
|
148
|
-
|
|
149
|
-
If you want to track arbitrary actions without declaring them:
|
|
150
|
-
|
|
151
|
-
```ruby
|
|
152
|
-
class Post < ApplicationRecord
|
|
153
|
-
include Persona::Trackable
|
|
154
|
-
|
|
155
|
-
persona do
|
|
156
|
-
open_tracking! # any string is valid — no UntrackedActionError raised
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
post.track!("custom_#{SecureRandom.hex(4)}") # works fine
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## Works on any model
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
class Post < ApplicationRecord
|
|
169
|
-
include Persona::Trackable
|
|
170
|
-
|
|
171
|
-
persona do
|
|
172
|
-
track :viewed
|
|
173
|
-
track :shared
|
|
174
|
-
track :bookmarked
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
post.track!(:viewed)
|
|
179
|
-
post.action_count(:viewed) # => 128
|
|
180
|
-
post.most_frequent_action # => :viewed
|
|
181
|
-
Post.persona_class_summary # => { viewed: 50_420, shared: 890, bookmarked: 210 }
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
## Configuration
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
# config/initializers/persona.rb
|
|
190
|
-
Persona.configure do |config|
|
|
191
|
-
config.inactivity_threshold_days = 14 # default: 30
|
|
192
|
-
config.max_events_per_record = 500 # default: nil (unlimited)
|
|
193
|
-
config.async = true # default: false
|
|
194
|
-
config.auto_prune_after_days = 90 # default: nil (no auto-prune)
|
|
195
|
-
end
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## Manual pruning
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
# Delete events older than 60 days for all records
|
|
204
|
-
Persona::Pruner.prune_older_than(60)
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Add to a scheduled job (e.g. `whenever` or Sidekiq-Cron):
|
|
208
|
-
|
|
209
|
-
```ruby
|
|
210
|
-
# lib/tasks/persona.rake
|
|
211
|
-
namespace :persona do
|
|
212
|
-
desc "Prune old persona events"
|
|
213
|
-
task prune: :environment do
|
|
214
|
-
Persona::Pruner.prune_older_than(Persona.configuration.auto_prune_after_days || 90)
|
|
215
|
-
puts "Pruned old persona events"
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
---
|
|
221
|
-
|
|
222
|
-
## Comparison with other gems
|
|
223
|
-
|
|
224
|
-
| Gem | Tracks | rails-persona advantage |
|
|
225
|
-
|---|---|---|
|
|
226
|
-
| **ahoy** | HTTP visits, JS events | Model actions, no JS needed, async built-in |
|
|
227
|
-
| **paper_trail** | Model attribute changes | Behavioral patterns, not diffs |
|
|
228
|
-
| **audited** | CRUD audit logs | Who *acted*, not what *changed* |
|
|
229
|
-
| **mixpanel-ruby** | Remote SaaS events | Your DB, no 3rd party, no cost |
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
## API reference
|
|
234
|
-
|
|
235
|
-
| Method | Description |
|
|
236
|
-
|--------|-------------|
|
|
237
|
-
| `track!(action, metadata: {})` | Record an action (sync or async) |
|
|
238
|
-
| `bulk_track!(actions)` | Record multiple actions via `insert_all!` |
|
|
239
|
-
| `reset_persona!` | Delete all events for this record |
|
|
240
|
-
| `action_count(action)` | Count of a specific action |
|
|
241
|
-
| `total_events` | Total event count |
|
|
242
|
-
| `most_frequent_action` | Most-performed action |
|
|
243
|
-
| `least_frequent_action` | Least-performed action |
|
|
244
|
-
| `top_actions(n)` | Top N actions by count |
|
|
245
|
-
| `last_action` | Most recent action symbol |
|
|
246
|
-
| `last_active_at` | Timestamp of last action |
|
|
247
|
-
| `first_action` | Earliest action symbol |
|
|
248
|
-
| `first_active_at` | Timestamp of first action |
|
|
249
|
-
| `inactive_since?(days)` | True if no action in N days |
|
|
250
|
-
| `days_since_last_activity` | Integer days since last event |
|
|
251
|
-
| `ever_did?(action)` | True if action occurred |
|
|
252
|
-
| `never_did?(action)` | True if action never occurred |
|
|
253
|
-
| `action_share(action)` | % of all events this action represents |
|
|
254
|
-
| `persona_summary` | Full action → count hash |
|
|
255
|
-
| `actions_between(from, to)` | Actions in a time window |
|
|
256
|
-
| `activity_log(limit)` | Recent events as array of hashes |
|
|
257
|
-
| `daily_activity(days)` | Events grouped by day |
|
|
258
|
-
| `peak_hour` | Hour (0-23) with most activity |
|
|
259
|
-
| `User.persona_leaderboard(limit:)` | Top N most active records |
|
|
260
|
-
| `User.persona_class_summary` | App-wide action breakdown |
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
## Contributing
|
|
265
|
-
|
|
266
|
-
Bug reports and pull requests welcome at https://github.com/sghani001/rails-persona.
|
|
267
|
-
|
|
268
|
-
## License
|
|
269
|
-
|
|
270
|
-
MIT — © Syed M. Ghani
|
|
1
|
+
# rails-persona 🎭
|
|
2
|
+
|
|
3
|
+
> Model-level behavioral analytics for Rails — own your data, zero external services.
|
|
4
|
+
|
|
5
|
+
[](https://rubygems.org/gems/rails-persona)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**rails-persona** is a lightweight Rails gem that adds first-class behavioral tracking directly to your ActiveRecord models. Unlike [ahoy](https://github.com/ankane/ahoy), which is focused on HTTP visit and page-view tracking, rails-persona is built for **model-level action tracking** — understanding *what your users actually do* in your app, not just what pages they visit.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why rails-persona over ahoy?
|
|
13
|
+
|
|
14
|
+
| | ahoy | rails-persona |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Focus | HTTP visits + page views | Model actions + user behavior |
|
|
17
|
+
| Setup | Controllers + JS snippet | Pure Ruby — one concern |
|
|
18
|
+
| Async | Manual Sidekiq setup | Built-in (`async: true`) |
|
|
19
|
+
| Bulk tracking | ❌ | ✅ `bulk_track!` with `insert_all!` |
|
|
20
|
+
| Class-level analytics | ❌ | ✅ Leaderboards, class summaries |
|
|
21
|
+
| Open tracking mode | ❌ | ✅ No whitelist required |
|
|
22
|
+
| Streak / pattern queries | ❌ | ✅ `daily_activity`, `peak_hour` |
|
|
23
|
+
| Cookies / sessions | Required | Never needed |
|
|
24
|
+
| Works on non-User models | Awkward | First-class |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem "rails-persona"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bundle install
|
|
36
|
+
rails db:migrate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
### 1. Include in any model
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class User < ApplicationRecord
|
|
47
|
+
include Persona::Trackable
|
|
48
|
+
|
|
49
|
+
persona do
|
|
50
|
+
track :login
|
|
51
|
+
track :export_report
|
|
52
|
+
track :view_dashboard
|
|
53
|
+
track :upgrade_plan
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Track actions in your app
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# In a controller, service, or job:
|
|
62
|
+
current_user.track!(:login)
|
|
63
|
+
current_user.track!(:upgrade_plan, metadata: { plan: "pro", amount: 49 })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Query behavior
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
user.action_count(:login) # => 42
|
|
70
|
+
user.most_frequent_action # => :login
|
|
71
|
+
user.least_frequent_action # => :upgrade_plan
|
|
72
|
+
user.top_actions(3) # => { login: 42, view_dashboard: 18, export_report: 5 }
|
|
73
|
+
user.last_action # => :export_report
|
|
74
|
+
user.last_active_at # => 2024-05-30 14:22 UTC
|
|
75
|
+
user.first_action # => :login
|
|
76
|
+
user.first_active_at # => 2023-01-10 08:00 UTC
|
|
77
|
+
user.inactive_since? # => false (default threshold: 30 days)
|
|
78
|
+
user.inactive_since?(7) # => false (custom: 7 days)
|
|
79
|
+
user.days_since_last_activity # => 2
|
|
80
|
+
user.ever_did?(:export_report) # => true
|
|
81
|
+
user.never_did?(:upgrade_plan) # => false
|
|
82
|
+
user.action_share(:login) # => 64.6 (% of all events)
|
|
83
|
+
user.total_events # => 65
|
|
84
|
+
|
|
85
|
+
user.persona_summary
|
|
86
|
+
# => { login: 42, view_dashboard: 18, export_report: 5, upgrade_plan: 1 }
|
|
87
|
+
|
|
88
|
+
user.actions_between(1.week.ago, Time.current)
|
|
89
|
+
# => { login: 7, view_dashboard: 3 }
|
|
90
|
+
|
|
91
|
+
user.activity_log(5)
|
|
92
|
+
# => [
|
|
93
|
+
# { action: :export_report, at: 2024-05-30 14:22:00, metadata: {} },
|
|
94
|
+
# { action: :login, at: 2024-05-30 09:01:00, metadata: {} },
|
|
95
|
+
# ]
|
|
96
|
+
|
|
97
|
+
user.daily_activity(30)
|
|
98
|
+
# => { "2024-05-28" => 4, "2024-05-29" => 7, "2024-05-30" => 2 }
|
|
99
|
+
|
|
100
|
+
user.peak_hour
|
|
101
|
+
# => 14 (2pm is when this user is most active)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Class-level analytics
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# Top 10 most active users
|
|
110
|
+
User.persona_leaderboard(limit: 10)
|
|
111
|
+
# => [
|
|
112
|
+
# { record: #<User id=4>, total_events: 128 },
|
|
113
|
+
# { record: #<User id=9>, total_events: 97 },
|
|
114
|
+
# ]
|
|
115
|
+
|
|
116
|
+
# App-wide breakdown of all user actions
|
|
117
|
+
User.persona_class_summary
|
|
118
|
+
# => { login: 8420, view_dashboard: 5210, export_report: 820 }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Bulk tracking (high-performance)
|
|
124
|
+
|
|
125
|
+
Uses `insert_all!` — no N+1, no per-row callbacks:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
user.bulk_track!([:login, :view_dashboard, :export_report])
|
|
129
|
+
user.bulk_track!([:login, :login, :login]) # track repeated actions
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Async tracking (Sidekiq)
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# config/initializers/persona.rb
|
|
138
|
+
Persona.configure do |config|
|
|
139
|
+
config.async = true # fires a Sidekiq job instead of writing inline
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Requires the `sidekiq` gem. Falls back to synchronous if Sidekiq is not available.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Open tracking (no whitelist)
|
|
148
|
+
|
|
149
|
+
If you want to track arbitrary actions without declaring them:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
class Post < ApplicationRecord
|
|
153
|
+
include Persona::Trackable
|
|
154
|
+
|
|
155
|
+
persona do
|
|
156
|
+
open_tracking! # any string is valid — no UntrackedActionError raised
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
post.track!("custom_#{SecureRandom.hex(4)}") # works fine
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Works on any model
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
class Post < ApplicationRecord
|
|
169
|
+
include Persona::Trackable
|
|
170
|
+
|
|
171
|
+
persona do
|
|
172
|
+
track :viewed
|
|
173
|
+
track :shared
|
|
174
|
+
track :bookmarked
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
post.track!(:viewed)
|
|
179
|
+
post.action_count(:viewed) # => 128
|
|
180
|
+
post.most_frequent_action # => :viewed
|
|
181
|
+
Post.persona_class_summary # => { viewed: 50_420, shared: 890, bookmarked: 210 }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# config/initializers/persona.rb
|
|
190
|
+
Persona.configure do |config|
|
|
191
|
+
config.inactivity_threshold_days = 14 # default: 30
|
|
192
|
+
config.max_events_per_record = 500 # default: nil (unlimited)
|
|
193
|
+
config.async = true # default: false
|
|
194
|
+
config.auto_prune_after_days = 90 # default: nil (no auto-prune)
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Manual pruning
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# Delete events older than 60 days for all records
|
|
204
|
+
Persona::Pruner.prune_older_than(60)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Add to a scheduled job (e.g. `whenever` or Sidekiq-Cron):
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# lib/tasks/persona.rake
|
|
211
|
+
namespace :persona do
|
|
212
|
+
desc "Prune old persona events"
|
|
213
|
+
task prune: :environment do
|
|
214
|
+
Persona::Pruner.prune_older_than(Persona.configuration.auto_prune_after_days || 90)
|
|
215
|
+
puts "Pruned old persona events"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Comparison with other gems
|
|
223
|
+
|
|
224
|
+
| Gem | Tracks | rails-persona advantage |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| **ahoy** | HTTP visits, JS events | Model actions, no JS needed, async built-in |
|
|
227
|
+
| **paper_trail** | Model attribute changes | Behavioral patterns, not diffs |
|
|
228
|
+
| **audited** | CRUD audit logs | Who *acted*, not what *changed* |
|
|
229
|
+
| **mixpanel-ruby** | Remote SaaS events | Your DB, no 3rd party, no cost |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## API reference
|
|
234
|
+
|
|
235
|
+
| Method | Description |
|
|
236
|
+
|--------|-------------|
|
|
237
|
+
| `track!(action, metadata: {})` | Record an action (sync or async) |
|
|
238
|
+
| `bulk_track!(actions)` | Record multiple actions via `insert_all!` |
|
|
239
|
+
| `reset_persona!` | Delete all events for this record |
|
|
240
|
+
| `action_count(action)` | Count of a specific action |
|
|
241
|
+
| `total_events` | Total event count |
|
|
242
|
+
| `most_frequent_action` | Most-performed action |
|
|
243
|
+
| `least_frequent_action` | Least-performed action |
|
|
244
|
+
| `top_actions(n)` | Top N actions by count |
|
|
245
|
+
| `last_action` | Most recent action symbol |
|
|
246
|
+
| `last_active_at` | Timestamp of last action |
|
|
247
|
+
| `first_action` | Earliest action symbol |
|
|
248
|
+
| `first_active_at` | Timestamp of first action |
|
|
249
|
+
| `inactive_since?(days)` | True if no action in N days |
|
|
250
|
+
| `days_since_last_activity` | Integer days since last event |
|
|
251
|
+
| `ever_did?(action)` | True if action occurred |
|
|
252
|
+
| `never_did?(action)` | True if action never occurred |
|
|
253
|
+
| `action_share(action)` | % of all events this action represents |
|
|
254
|
+
| `persona_summary` | Full action → count hash |
|
|
255
|
+
| `actions_between(from, to)` | Actions in a time window |
|
|
256
|
+
| `activity_log(limit)` | Recent events as array of hashes |
|
|
257
|
+
| `daily_activity(days)` | Events grouped by day |
|
|
258
|
+
| `peak_hour` | Hour (0-23) with most activity |
|
|
259
|
+
| `User.persona_leaderboard(limit:)` | Top N most active records |
|
|
260
|
+
| `User.persona_class_summary` | App-wide action breakdown |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Contributing
|
|
265
|
+
|
|
266
|
+
Bug reports and pull requests welcome at https://github.com/sghani001/rails-persona.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT — © Syed M. Ghani
|
data/lib/persona/railtie.rb
CHANGED
|
@@ -2,20 +2,15 @@ require "rails"
|
|
|
2
2
|
|
|
3
3
|
module Persona
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
|
-
initializer "persona.load_app_instance_data" do |app|
|
|
6
|
-
Persona::Railtie.instance_variable_set(:@app, app)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
5
|
initializer "persona.append_migrations" do |app|
|
|
10
6
|
unless app.root.to_s == File.expand_path("../..", __dir__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
7
|
+
migrations_path = File.expand_path("../../../db/migrate", __dir__)
|
|
8
|
+
app.config.paths["db/migrate"] << migrations_path
|
|
14
9
|
end
|
|
15
10
|
end
|
|
16
11
|
|
|
17
12
|
rake_tasks do
|
|
18
|
-
load "tasks/persona_tasks.rake"
|
|
13
|
+
load File.expand_path("../../../tasks/persona_tasks.rake", __dir__)
|
|
19
14
|
end
|
|
20
15
|
end
|
|
21
|
-
end
|
|
16
|
+
end
|
data/lib/persona/version.rb
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
module Persona
|
|
2
|
-
VERSION = "0.2.
|
|
3
|
-
end
|
|
1
|
+
module Persona
|
|
2
|
+
VERSION = "0.2.1"
|
|
3
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-persona
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Syed M. Ghani
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|