trakable 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 +7 -0
- data/.rubocop.yml +81 -0
- data/CHANGELOG.md +50 -0
- data/LICENSE +21 -0
- data/README.md +330 -0
- data/Rakefile +16 -0
- data/benchmark/full_benchmark.rb +221 -0
- data/benchmark/integration_memory.rb +70 -0
- data/benchmark/memory_benchmark.rb +141 -0
- data/benchmark/perf_benchmark.rb +130 -0
- data/integration/README.md +65 -0
- data/integration/run_all.rb +62 -0
- data/integration/scenarios/01-basic-tracking/scenario.rb +51 -0
- data/integration/scenarios/02-revert-restoration/scenario.rb +103 -0
- data/integration/scenarios/03-whodunnit-tracking/scenario.rb +72 -0
- data/integration/scenarios/04-cleanup-retention/scenario.rb +66 -0
- data/integration/scenarios/05-without-tracking/scenario.rb +62 -0
- data/integration/scenarios/06-callback-lifecycle/scenario.rb +103 -0
- data/integration/scenarios/07-global-config/scenario.rb +52 -0
- data/integration/scenarios/08-controller-integration/scenario.rb +44 -0
- data/integration/scenarios/09-cleanup-max-traks/scenario.rb +58 -0
- data/integration/scenarios/10-model-configuration/scenario.rb +68 -0
- data/integration/scenarios/11-conditional-tracking/scenario.rb +48 -0
- data/integration/scenarios/12-metadata/scenario.rb +54 -0
- data/integration/scenarios/13-traks-association/scenario.rb +80 -0
- data/integration/scenarios/14-time-travel/scenario.rb +132 -0
- data/integration/scenarios/15-diffing-changeset/scenario.rb +109 -0
- data/integration/scenarios/16-serialization/scenario.rb +159 -0
- data/integration/scenarios/17-associations-tracking/scenario.rb +143 -0
- data/integration/scenarios/18-bulk-operations/scenario.rb +70 -0
- data/integration/scenarios/19-transactions/scenario.rb +89 -0
- data/integration/scenarios/20-performance/scenario.rb +89 -0
- data/integration/scenarios/21-storage-backends/scenario.rb +52 -0
- data/integration/scenarios/22-multi-tenancy/scenario.rb +49 -0
- data/integration/scenarios/23-sti/scenario.rb +58 -0
- data/integration/scenarios/24-edge-cases-part1/scenario.rb +86 -0
- data/integration/scenarios/25-edge-cases-part2/scenario.rb +74 -0
- data/integration/scenarios/26-edge-cases-part3/scenario.rb +76 -0
- data/integration/scenarios/27-api-query-interface/scenario.rb +78 -0
- data/integration/scenarios/28-security-compliance/scenario.rb +61 -0
- data/integration/scenarios/29-soft-delete/scenario.rb +43 -0
- data/integration/scenarios/30-custom-events/scenario.rb +45 -0
- data/integration/scenarios/31-gem-packaging/scenario.rb +58 -0
- data/integration/scenarios/32-bypass-fail-closed/scenario.rb +77 -0
- data/integration/scenarios/33-coexistence-standalone/scenario.rb +53 -0
- data/integration/scenarios/34-real-tracking/scenario.rb +254 -0
- data/integration/scenarios/35-revert-undo/scenario.rb +235 -0
- data/integration/scenarios/36-whodunnit-deep/scenario.rb +281 -0
- data/integration/scenarios/37-real-world-use-cases/scenario.rb +1213 -0
- data/integration/scenarios/38-concurrency/scenario.rb +163 -0
- data/integration/scenarios/39-query-scopes/scenario.rb +126 -0
- data/integration/scenarios/40-whodunnit-config/scenario.rb +113 -0
- data/integration/scenarios/41-batch-cleanup/scenario.rb +186 -0
- data/integration/scenarios/scenario_runner.rb +68 -0
- data/lib/generators/trakable/install_generator.rb +28 -0
- data/lib/generators/trakable/templates/create_traks_migration.rb +23 -0
- data/lib/generators/trakable/templates/trakable_initializer.rb +15 -0
- data/lib/trakable/cleanup.rb +89 -0
- data/lib/trakable/config.rb +22 -0
- data/lib/trakable/context.rb +85 -0
- data/lib/trakable/controller.rb +25 -0
- data/lib/trakable/model.rb +99 -0
- data/lib/trakable/railtie.rb +28 -0
- data/lib/trakable/revertable.rb +166 -0
- data/lib/trakable/tracker.rb +134 -0
- data/lib/trakable/trak.rb +98 -0
- data/lib/trakable/version.rb +5 -0
- data/lib/trakable.rb +51 -0
- data/trakable.gemspec +41 -0
- metadata +242 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0dcab04bb1079556fb3f2f036a3e7d6a4ee423dc5d9ab31cb27ea41c4bbd4099
|
|
4
|
+
data.tar.gz: c7b63e5946469731c3ecacd67d013d44928f635308c70118a91b07adddc82887
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ed12c7a3f09be0d82af29c7d47bc155a70c504c2f46370298e75e65fbad52706a3cf168f6a214d04f52b330f11c39b6404fd4a10de504cb646e33cfe0dbb6ff9
|
|
7
|
+
data.tar.gz: 654294059fa0e7c7a07531b702797988f0f6bedc491496d2c8ee8c37a3083927e02d60b24dc9e315c5c605796d18ac7d23b33219e08748c324084a307ebdd02c
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
plugins: []
|
|
2
|
+
|
|
3
|
+
AllCops:
|
|
4
|
+
NewCops: enable
|
|
5
|
+
TargetRubyVersion: 3.1
|
|
6
|
+
SuggestExtensions: false
|
|
7
|
+
Exclude:
|
|
8
|
+
- benchmark/**/*
|
|
9
|
+
- integration/**/*
|
|
10
|
+
- vendor/**/*
|
|
11
|
+
|
|
12
|
+
Metrics/BlockLength:
|
|
13
|
+
Exclude:
|
|
14
|
+
- test/**/*
|
|
15
|
+
- rakelib/**/*
|
|
16
|
+
- "*.gemspec"
|
|
17
|
+
- lib/trakable/model.rb
|
|
18
|
+
|
|
19
|
+
Metrics/ClassLength:
|
|
20
|
+
Max: 200
|
|
21
|
+
|
|
22
|
+
Metrics/MethodLength:
|
|
23
|
+
Max: 20
|
|
24
|
+
Exclude:
|
|
25
|
+
- test/**/*
|
|
26
|
+
|
|
27
|
+
Metrics/ParameterLists:
|
|
28
|
+
Max: 6
|
|
29
|
+
|
|
30
|
+
Metrics/AbcSize:
|
|
31
|
+
Exclude:
|
|
32
|
+
- test/**/*
|
|
33
|
+
- lib/trakable/revertable.rb
|
|
34
|
+
|
|
35
|
+
Metrics/CyclomaticComplexity:
|
|
36
|
+
Exclude:
|
|
37
|
+
- lib/trakable/revertable.rb
|
|
38
|
+
|
|
39
|
+
Metrics/PerceivedComplexity:
|
|
40
|
+
Exclude:
|
|
41
|
+
- lib/trakable/revertable.rb
|
|
42
|
+
|
|
43
|
+
Lint/MissingSuper:
|
|
44
|
+
Exclude:
|
|
45
|
+
- lib/trakable/trak.rb
|
|
46
|
+
|
|
47
|
+
Naming/MethodParameterName:
|
|
48
|
+
Exclude:
|
|
49
|
+
- test/**/*
|
|
50
|
+
|
|
51
|
+
Style/OptionalBooleanParameter:
|
|
52
|
+
Exclude:
|
|
53
|
+
- test/**/*
|
|
54
|
+
|
|
55
|
+
Layout/LineLength:
|
|
56
|
+
Exclude:
|
|
57
|
+
- "*.gemspec"
|
|
58
|
+
|
|
59
|
+
Style/Documentation:
|
|
60
|
+
Exclude:
|
|
61
|
+
- test/**/*
|
|
62
|
+
- lib/trakable/version.rb
|
|
63
|
+
- lib/generators/**/*
|
|
64
|
+
- lib/trakable/railtie.rb
|
|
65
|
+
|
|
66
|
+
Style/ClassAndModuleChildren:
|
|
67
|
+
Enabled: false
|
|
68
|
+
|
|
69
|
+
Style/OneClassPerFile:
|
|
70
|
+
Exclude:
|
|
71
|
+
- test/**/*
|
|
72
|
+
|
|
73
|
+
Style/WordArray:
|
|
74
|
+
Exclude:
|
|
75
|
+
- test/**/*
|
|
76
|
+
|
|
77
|
+
Gemspec/DevelopmentDependencies:
|
|
78
|
+
Enabled: false
|
|
79
|
+
|
|
80
|
+
Gemspec/RequireMFA:
|
|
81
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-03-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Query scopes** on `Trakable::Trak` (AR-only): `for_item_type`, `for_event`, `for_whodunnit`, `created_before`, `created_after`, `recent`
|
|
13
|
+
- **Batch deletion** in `run_retention` with configurable `batch_size` (default: 1,000) to avoid table locks
|
|
14
|
+
- **CI/CD** via GitHub Actions (Ruby 3.1–3.4 matrix, tests + RuboCop)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Cleanup is no longer synchronous** — `Cleanup.run` is no longer called after every trak creation. Run it from a background job instead.
|
|
19
|
+
- `run_retention` now returns the total number of deleted rows (Integer) instead of `true`
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2026-03-20
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Initial release of Trakable gem
|
|
26
|
+
- **Trak model** with JSON serialization for object, changeset, and metadata
|
|
27
|
+
- **Polymorphic whodunnit** tracking (type + id) instead of string
|
|
28
|
+
- **Trakable DSL** for ActiveRecord models with options:
|
|
29
|
+
- `only:` - track specific attributes
|
|
30
|
+
- `ignore:` - skip specific attributes
|
|
31
|
+
- `if:` / `unless:` - conditional tracking
|
|
32
|
+
- `on:` - selective events (create, update, destroy)
|
|
33
|
+
- **Revertable module**:
|
|
34
|
+
- `reify` - build non-persisted record from Trak state
|
|
35
|
+
- `revert!` - restore record to previous state
|
|
36
|
+
- `trak_at` - get record state at specific timestamp
|
|
37
|
+
- **Controller concern** with automatic whodunnit setting via `around_action`
|
|
38
|
+
- **Cleanup module** with:
|
|
39
|
+
- `max_traks` - limit number of traks per record
|
|
40
|
+
- `retention` - automatic pruning of old traks
|
|
41
|
+
- **Railtie** for Rails integration with:
|
|
42
|
+
- Configuration auto-loading from `config.trakable`
|
|
43
|
+
- Install generator for migration and initializer
|
|
44
|
+
- **Thread-safe context** for storing whodunnit and metadata
|
|
45
|
+
- **Global configuration** via `Trakable.configure`
|
|
46
|
+
- Comprehensive README with usage examples
|
|
47
|
+
- MIT License
|
|
48
|
+
|
|
49
|
+
[0.2.0]: https://github.com/hadrienblanc/trakable/releases/tag/v0.2.0
|
|
50
|
+
[0.1.0]: https://github.com/hadrienblanc/trakable/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hadrien Blanc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Trakable
|
|
2
|
+
|
|
3
|
+
[](https://github.com/hadrienblanc/trakable/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/trakable)
|
|
5
|
+
[](https://github.com/hadrienblanc/trakable)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
Audit logging and version tracking for ActiveRecord models.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add this line to your application's Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'trakable'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
And then execute:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
$ bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install it yourself as:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
$ gem install trakable
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Generate the migration
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
$ rails generate trakable:install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This creates:
|
|
39
|
+
- `db/migrate/create_traks.rb` - Migration for the traks table
|
|
40
|
+
- `config/initializers/trakable.rb` - Configuration file
|
|
41
|
+
|
|
42
|
+
Run the migration:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
$ rails db:migrate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Add tracking to your models
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class Post < ApplicationRecord
|
|
52
|
+
include Trakable::Model
|
|
53
|
+
|
|
54
|
+
trakable only: %i[title body], ignore: %i[views_count]
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Whodunnit is automatic
|
|
59
|
+
|
|
60
|
+
Trakable auto-includes its controller concern via Railtie. It calls `current_user` by default — no setup needed.
|
|
61
|
+
|
|
62
|
+
To use a different method:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Trakable.configure do |config|
|
|
66
|
+
config.whodunnit_method = :current_admin
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
### Global configuration
|
|
73
|
+
|
|
74
|
+
In `config/initializers/trakable.rb`:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Trakable.configure do |config|
|
|
78
|
+
# Enable/disable tracking globally
|
|
79
|
+
config.enabled = true
|
|
80
|
+
|
|
81
|
+
# Attributes to ignore by default
|
|
82
|
+
config.ignored_attrs = %w[created_at updated_at id]
|
|
83
|
+
|
|
84
|
+
# Controller method that returns the current user (default: :current_user)
|
|
85
|
+
config.whodunnit_method = :current_user
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Per-model options
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class Post < ApplicationRecord
|
|
93
|
+
include Trakable::Model
|
|
94
|
+
|
|
95
|
+
trakable(
|
|
96
|
+
only: %i[title body], # Only track these attributes
|
|
97
|
+
ignore: %i[views_count], # Ignore these attributes
|
|
98
|
+
on: %i[create update destroy], # Only track these events (default: all)
|
|
99
|
+
if: -> { published? }, # Conditional tracking
|
|
100
|
+
unless: -> { draft? } # Skip if true
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
### Accessing traks
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
post = Post.first
|
|
111
|
+
|
|
112
|
+
# Get all traks for a record
|
|
113
|
+
post.traks
|
|
114
|
+
|
|
115
|
+
# Get the last trak
|
|
116
|
+
post.traks.last
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Trak properties
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
trak = post.traks.last
|
|
123
|
+
|
|
124
|
+
trak.event # => "update"
|
|
125
|
+
trak.create? # => false
|
|
126
|
+
trak.update? # => true
|
|
127
|
+
trak.destroy? # => false
|
|
128
|
+
|
|
129
|
+
trak.changeset # => { "title" => ["Old Title", "New Title"] }
|
|
130
|
+
trak.object # => { "title" => "Old Title", "body" => "..." }
|
|
131
|
+
trak.metadata # => { "ip" => "192.168.1.1", "user_agent" => "..." }
|
|
132
|
+
trak.created_at # => 2024-01-15 10:30:00 UTC
|
|
133
|
+
|
|
134
|
+
# Whodunnit (polymorphic)
|
|
135
|
+
trak.whodunnit_type # => "User"
|
|
136
|
+
trak.whodunnit_id # => 42
|
|
137
|
+
trak.whodunnit # => #<User id: 42, ...>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Setting metadata
|
|
141
|
+
|
|
142
|
+
You can add custom metadata to traks using the context:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
Trakable::Context.metadata = { ip: request.ip, user_agent: request.user_agent }
|
|
146
|
+
post.update(title: "New Title")
|
|
147
|
+
# The created trak will include the metadata
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Revert changes
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# Restore the record to the state before this trak
|
|
154
|
+
post.traks.last.revert!
|
|
155
|
+
|
|
156
|
+
# Revert and create a trak for the revert action
|
|
157
|
+
post.traks.last.revert!(trak_revert: true)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Time travel
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# Get the state at a specific point in time
|
|
164
|
+
post.trak_at(1.day.ago) # => Non-persisted record with state from 1 day ago
|
|
165
|
+
|
|
166
|
+
# Get the state from a specific trak
|
|
167
|
+
post.traks.last.reify # => Non-persisted record with state at that trak
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Query scopes
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# Filter by model type
|
|
174
|
+
Trakable::Trak.for_item_type('Post')
|
|
175
|
+
|
|
176
|
+
# Filter by event
|
|
177
|
+
Trakable::Trak.for_event(:update)
|
|
178
|
+
|
|
179
|
+
# Filter by whodunnit
|
|
180
|
+
Trakable::Trak.for_whodunnit(current_user)
|
|
181
|
+
|
|
182
|
+
# Filter by time range
|
|
183
|
+
Trakable::Trak.created_after(1.week.ago)
|
|
184
|
+
Trakable::Trak.created_before(Date.yesterday)
|
|
185
|
+
|
|
186
|
+
# Newest first
|
|
187
|
+
Trakable::Trak.recent
|
|
188
|
+
|
|
189
|
+
# Combine them
|
|
190
|
+
Trakable::Trak.for_item_type('Post').for_event(:update).created_after(1.day.ago).recent
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Temporarily disable tracking
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Disable tracking for a block
|
|
197
|
+
Trakable.without_tracking do
|
|
198
|
+
post.update(title: "Won't be tracked")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Force tracking when globally disabled
|
|
202
|
+
Trakable.with_tracking do
|
|
203
|
+
post.update(title: "Will be tracked")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Set whodunnit manually
|
|
207
|
+
Trakable.with_user(current_user) do
|
|
208
|
+
post.update(title: "Tracked with user")
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Cleanup
|
|
213
|
+
|
|
214
|
+
Configure cleanup options per model:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
class Post < ApplicationRecord
|
|
218
|
+
include Trakable::Model
|
|
219
|
+
|
|
220
|
+
trakable max_traks: 100 # Keep only last 100 traks
|
|
221
|
+
trakable retention: 90.days # Delete traks older than 90 days
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Cleanup is **not** automatic — call it from a background job to keep your traks table lean:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# In a recurring job (e.g. daily cron)
|
|
229
|
+
Trakable::Cleanup.run_retention(Post)
|
|
230
|
+
|
|
231
|
+
# Per-record cleanup (e.g. after a batch import)
|
|
232
|
+
Trakable::Cleanup.run(post)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Edge cases
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
# When no trak exists at the timestamp, returns current state
|
|
239
|
+
post.trak_at(1.year.ago) # => Returns current state if no older traks exist
|
|
240
|
+
|
|
241
|
+
# When whodunnit record is deleted, returns nil
|
|
242
|
+
trak.whodunnit # => nil (if the user was deleted)
|
|
243
|
+
|
|
244
|
+
# Revert on destroy re-creates the record (with new ID)
|
|
245
|
+
destroy_trak = post.traks.where(event: 'destroy').last
|
|
246
|
+
destroy_trak.revert! # => Creates new record with same attributes but new ID
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## API Reference
|
|
250
|
+
|
|
251
|
+
### Trakable::Model
|
|
252
|
+
|
|
253
|
+
| Method | Description |
|
|
254
|
+
|--------|-------------|
|
|
255
|
+
| `trakable(options)` | Configure tracking for this model |
|
|
256
|
+
| `traks` | Association to all traks for this record |
|
|
257
|
+
| `trak_at(timestamp)` | Get record state at a specific time |
|
|
258
|
+
|
|
259
|
+
### Trakable::Trak
|
|
260
|
+
|
|
261
|
+
| Method | Description |
|
|
262
|
+
|--------|-------------|
|
|
263
|
+
| `item` | The tracked record (polymorphic) |
|
|
264
|
+
| `whodunnit` | The user who made the change (polymorphic) |
|
|
265
|
+
| `event` | The event type: "create", "update", or "destroy" |
|
|
266
|
+
| `changeset` | Hash of changed attributes with [old, new] values |
|
|
267
|
+
| `object` | Changed attributes before the change (delta for updates, full snapshot for destroys) |
|
|
268
|
+
| `create?` | True if this is a create event |
|
|
269
|
+
| `update?` | True if this is an update event |
|
|
270
|
+
| `destroy?` | True if this is a destroy event |
|
|
271
|
+
| `reify` | Build non-persisted record with state at this trak |
|
|
272
|
+
| `revert!` | Restore record to state before this trak |
|
|
273
|
+
| `for_item_type(type)` | Scope: filter by item type |
|
|
274
|
+
| `for_event(event)` | Scope: filter by event |
|
|
275
|
+
| `for_whodunnit(user)` | Scope: filter by whodunnit (polymorphic) |
|
|
276
|
+
| `created_before(time)` | Scope: traks before a timestamp |
|
|
277
|
+
| `created_after(time)` | Scope: traks after a timestamp |
|
|
278
|
+
| `recent` | Scope: newest first |
|
|
279
|
+
|
|
280
|
+
### Trakable::Controller (auto-included via Railtie)
|
|
281
|
+
|
|
282
|
+
| Option | Description |
|
|
283
|
+
|--------|-------------|
|
|
284
|
+
| `config.whodunnit_method` | Controller method that returns the current user (default: `:current_user`) |
|
|
285
|
+
|
|
286
|
+
## Performance Tips
|
|
287
|
+
|
|
288
|
+
### Eager loading (N+1 prevention)
|
|
289
|
+
|
|
290
|
+
When loading multiple records with their traks, use `includes` to avoid N+1 queries:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# Bad — N+1
|
|
294
|
+
posts = Post.all
|
|
295
|
+
posts.each { |p| p.traks.count }
|
|
296
|
+
|
|
297
|
+
# Good — eager loaded
|
|
298
|
+
posts = Post.includes(:traks).all
|
|
299
|
+
posts.each { |p| p.traks.size }
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Compress serialized columns (Rails 7.1+)
|
|
303
|
+
|
|
304
|
+
For large `object`/`changeset` payloads, enable column compression by adding a custom initializer:
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# config/initializers/trakable_compression.rb
|
|
308
|
+
Rails.application.config.after_initialize do
|
|
309
|
+
Trakable::Trak.serialize :object, coder: JSON, compress: true
|
|
310
|
+
Trakable::Trak.serialize :changeset, coder: JSON, compress: true
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
This uses zlib under the hood and can reduce storage by 60-80% for large payloads.
|
|
315
|
+
|
|
316
|
+
## Differences from PaperTrail
|
|
317
|
+
|
|
318
|
+
| Feature | PaperTrail | Trakable |
|
|
319
|
+
|---------|------------|----------|
|
|
320
|
+
| Whodunnit | String | Polymorphic (type + id) |
|
|
321
|
+
| Changeset | Opt-in | Always stored |
|
|
322
|
+
| Metadata | Not native | Built-in column |
|
|
323
|
+
| Retention | Manual | Built-in (max_traks, retention) |
|
|
324
|
+
| Serialization | YAML default | JSON only |
|
|
325
|
+
| Table name | versions | traks |
|
|
326
|
+
| Updated_at | Yes | No (immutable) |
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
The gem is available as open source under the terms of the [MIT License](./LICENSE).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << 'test'
|
|
8
|
+
t.libs << 'lib'
|
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task default: :test
|
|
13
|
+
|
|
14
|
+
require 'rubocop/rake_task'
|
|
15
|
+
|
|
16
|
+
RuboCop::RakeTask.new
|