sinaliza 0.1.3 → 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/README.md +57 -5
- data/app/controllers/concerns/sinaliza/traceable.rb +2 -1
- data/app/controllers/sinaliza/events_controller.rb +2 -0
- data/app/jobs/sinaliza/record_event_job.rb +4 -0
- data/app/models/concerns/sinaliza/trackable.rb +21 -2
- data/app/models/sinaliza/event.rb +4 -1
- data/app/views/sinaliza/events/_filters.html.erb +10 -0
- data/app/views/sinaliza/events/index.html.erb +2 -0
- data/app/views/sinaliza/events/show.html.erb +4 -0
- data/db/migrate/20260220000000_add_context_to_sinaliza_events.rb +5 -0
- data/lib/sinaliza/version.rb +1 -1
- data/lib/sinaliza.rb +4 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc89fcab6f596adbd552ed8b28da09bba68fc38afc1d784855ebe95361b077ce
|
|
4
|
+
data.tar.gz: eb6b974f6c9701a96b8aca56364e562ace77804aa939c87e5ca808a26efbe7e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d60d7c5c5d669fb061882336df233956bea2fed3517e6a9e95756f27e0e70103e3601f743e27b3e41f85c9b281b6ee26d5ca2ee6fb9482bf52a4a2c278d9f39
|
|
7
|
+
data.tar.gz: 7b40ff04f632e503bc099a8cb68c9e75dbdde24680085fa65d882cc2767e7d6426ab8ec07dfec667fa3b65e03f28314a3df0c69ebde6a0cdd4b0fb79e24c3d50
|
data/README.md
CHANGED
|
@@ -50,6 +50,8 @@ Both methods accept:
|
|
|
50
50
|
| `ip_address`| IP address | `nil` |
|
|
51
51
|
| `user_agent`| User agent string | `nil` |
|
|
52
52
|
| `request_id`| Request ID | `nil` |
|
|
53
|
+
| `context` | Business context for grouping (any model) | `nil` |
|
|
54
|
+
| `parent` | Parent event (for hierarchies) | `nil` |
|
|
53
55
|
|
|
54
56
|
### Model concern — `Sinaliza::Trackable`
|
|
55
57
|
|
|
@@ -64,16 +66,62 @@ end
|
|
|
64
66
|
This gives you:
|
|
65
67
|
|
|
66
68
|
```ruby
|
|
67
|
-
user.events_as_actor
|
|
68
|
-
user.events_as_target
|
|
69
|
+
user.events_as_actor # events where user is the actor
|
|
70
|
+
user.events_as_target # events where user is the target
|
|
71
|
+
user.events_as_context # events where user is the context
|
|
69
72
|
|
|
70
73
|
user.track_event("profile.updated", metadata: { field: "email" })
|
|
71
|
-
user.track_event("post.published", target: post)
|
|
74
|
+
user.track_event("post.published", target: post, context: subscription)
|
|
75
|
+
user.track_event("invoice.paid", target: invoice, context: subscription, parent: signup_event)
|
|
72
76
|
|
|
73
77
|
post.track_event_as_target("post.featured", actor: admin)
|
|
78
|
+
|
|
79
|
+
subscription.track_event_as_context("plan.upgraded", actor: user)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Events are recorded with `source: "model"`. When an actor, target, or context is destroyed, associated events are preserved with nullified references (`dependent: :nullify`).
|
|
83
|
+
|
|
84
|
+
### Event context
|
|
85
|
+
|
|
86
|
+
The `context` parameter is a polymorphic association that lets you group events under a business object. This is useful when multiple events belong to the same logical context — such as a subscription, an order, or a project.
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
subscription = user.subscriptions.current
|
|
90
|
+
|
|
91
|
+
# Record events within a subscription context
|
|
92
|
+
Sinaliza.record(name: "plan.upgraded", actor: user, context: subscription, metadata: { from: "basic", to: "pro" })
|
|
93
|
+
Sinaliza.record(name: "payment.processed", actor: user, context: subscription)
|
|
94
|
+
Sinaliza.record(name: "invoice.sent", target: user, context: subscription)
|
|
95
|
+
|
|
96
|
+
# Query events by context
|
|
97
|
+
subscription.events_as_context # all events for this subscription
|
|
98
|
+
Sinaliza::Event.by_context(subscription) # same, via scope
|
|
99
|
+
Sinaliza::Event.by_context_type("Subscription") # all events for any subscription
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Parent & children events
|
|
103
|
+
|
|
104
|
+
Events support a parent/children hierarchy. Use this to represent causal chains or group sub-steps under a main event.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Create a parent event
|
|
108
|
+
signup = Sinaliza.record(name: "user.signed_up", actor: user)
|
|
109
|
+
|
|
110
|
+
# Create child events
|
|
111
|
+
Sinaliza.record(name: "welcome_email.sent", actor: user, parent: signup)
|
|
112
|
+
Sinaliza.record(name: "default_settings.created", actor: user, parent: signup)
|
|
113
|
+
|
|
114
|
+
# Navigate the hierarchy
|
|
115
|
+
signup.children # child events
|
|
116
|
+
signup.root? # => true
|
|
117
|
+
signup.children.first.child? # => true
|
|
118
|
+
signup.children.first.parent # => the signup event
|
|
119
|
+
|
|
120
|
+
# Query only top-level events
|
|
121
|
+
Sinaliza::Event.roots
|
|
74
122
|
```
|
|
75
123
|
|
|
76
|
-
|
|
124
|
+
When a parent event is destroyed, its children are also destroyed (`dependent: :destroy`).
|
|
77
125
|
|
|
78
126
|
### Controller concern — `Sinaliza::Traceable`
|
|
79
127
|
|
|
@@ -112,6 +160,9 @@ The actor is resolved by calling the method defined in `Sinaliza.configuration.a
|
|
|
112
160
|
Sinaliza::Event.by_name("user.login")
|
|
113
161
|
Sinaliza::Event.by_source("controller")
|
|
114
162
|
Sinaliza::Event.by_actor_type("User")
|
|
163
|
+
Sinaliza::Event.by_context(subscription) # events for a specific context record
|
|
164
|
+
Sinaliza::Event.by_context_type("Subscription") # events for any record of this type
|
|
165
|
+
Sinaliza::Event.roots # only top-level events (no parent)
|
|
115
166
|
Sinaliza::Event.since(1.week.ago)
|
|
116
167
|
Sinaliza::Event.before(Date.yesterday)
|
|
117
168
|
Sinaliza::Event.between(1.week.ago, 1.day.ago)
|
|
@@ -124,6 +175,7 @@ Scopes are chainable:
|
|
|
124
175
|
|
|
125
176
|
```ruby
|
|
126
177
|
Sinaliza::Event.by_name("order.created").by_actor_type("User").since(1.day.ago)
|
|
178
|
+
Sinaliza::Event.by_context(subscription).roots.reverse_chronological
|
|
127
179
|
```
|
|
128
180
|
|
|
129
181
|
## Dashboard
|
|
@@ -179,7 +231,7 @@ Schedule it with cron, Heroku Scheduler, or whatever you prefer.
|
|
|
179
231
|
|
|
180
232
|
## Database schema
|
|
181
233
|
|
|
182
|
-
Events are stored in a single `sinaliza_events` table with polymorphic `actor` and `
|
|
234
|
+
Events are stored in a single `sinaliza_events` table with polymorphic `actor`, `target`, and `context` columns, plus a `parent_id` foreign key for hierarchies. The `metadata` column uses `json` type for cross-database compatibility (SQLite, PostgreSQL, MySQL).
|
|
183
235
|
|
|
184
236
|
## License
|
|
185
237
|
|
|
@@ -18,13 +18,14 @@ module Sinaliza
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
def record_event(name, target: nil, parent: nil, metadata: {})
|
|
21
|
+
def record_event(name, target: nil, context: nil, parent: nil, metadata: {})
|
|
22
22
|
actor = resolve_actor
|
|
23
23
|
|
|
24
24
|
attributes = {
|
|
25
25
|
name: name,
|
|
26
26
|
actor: actor,
|
|
27
27
|
target: target,
|
|
28
|
+
context: context,
|
|
28
29
|
parent: parent,
|
|
29
30
|
metadata: metadata,
|
|
30
31
|
source: "controller"
|
|
@@ -11,6 +11,7 @@ module Sinaliza
|
|
|
11
11
|
@filter_names = Event.distinct.pluck(:name).sort
|
|
12
12
|
@filter_sources = Event.distinct.pluck(:source).sort
|
|
13
13
|
@filter_actor_types = Event.where.not(actor_type: nil).distinct.pluck(:actor_type).sort
|
|
14
|
+
@filter_context_types = Event.where.not(context_type: nil).distinct.pluck(:context_type).sort
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def show
|
|
@@ -24,6 +25,7 @@ module Sinaliza
|
|
|
24
25
|
@events = @events.by_name(params[:name]) if params[:name].present?
|
|
25
26
|
@events = @events.by_source(params[:source]) if params[:source].present?
|
|
26
27
|
@events = @events.by_actor_type(params[:actor_type]) if params[:actor_type].present?
|
|
28
|
+
@events = @events.by_context_type(params[:context_type]) if params[:context_type].present?
|
|
27
29
|
@events = @events.search(params[:q]) if params[:q].present?
|
|
28
30
|
@events = @events.since(Date.parse(params[:since])) if params[:since].present?
|
|
29
31
|
@events = @events.before(Date.parse(params[:before]).end_of_day) if params[:before].present?
|
|
@@ -13,6 +13,10 @@ module Sinaliza
|
|
|
13
13
|
attributes[:target] = GlobalID::Locator.locate(attributes[:target])
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
if attributes[:context].is_a?(String)
|
|
17
|
+
attributes[:context] = GlobalID::Locator.locate(attributes[:context])
|
|
18
|
+
end
|
|
19
|
+
|
|
16
20
|
Sinaliza::Event.create!(attributes)
|
|
17
21
|
end
|
|
18
22
|
end
|
|
@@ -12,24 +12,43 @@ module Sinaliza
|
|
|
12
12
|
class_name: "Sinaliza::Event",
|
|
13
13
|
as: :target,
|
|
14
14
|
dependent: :nullify
|
|
15
|
+
|
|
16
|
+
has_many :events_as_context,
|
|
17
|
+
class_name: "Sinaliza::Event",
|
|
18
|
+
as: :context,
|
|
19
|
+
dependent: :nullify
|
|
15
20
|
end
|
|
16
21
|
|
|
17
|
-
def track_event(name, target: nil, parent: nil, metadata: {})
|
|
22
|
+
def track_event(name, target: nil, context: nil, parent: nil, metadata: {})
|
|
18
23
|
Sinaliza.record(
|
|
19
24
|
name: name,
|
|
20
25
|
actor: self,
|
|
21
26
|
target: target,
|
|
27
|
+
context: context,
|
|
22
28
|
parent: parent,
|
|
23
29
|
metadata: metadata,
|
|
24
30
|
source: "model"
|
|
25
31
|
)
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
def track_event_as_target(name, actor: nil, parent: nil, metadata: {})
|
|
34
|
+
def track_event_as_target(name, actor: nil, context: nil, parent: nil, metadata: {})
|
|
29
35
|
Sinaliza.record(
|
|
30
36
|
name: name,
|
|
31
37
|
actor: actor,
|
|
32
38
|
target: self,
|
|
39
|
+
context: context,
|
|
40
|
+
parent: parent,
|
|
41
|
+
metadata: metadata,
|
|
42
|
+
source: "model"
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def track_event_as_context(name, actor: nil, target: nil, parent: nil, metadata: {})
|
|
47
|
+
Sinaliza.record(
|
|
48
|
+
name: name,
|
|
49
|
+
actor: actor,
|
|
50
|
+
target: target,
|
|
51
|
+
context: self,
|
|
33
52
|
parent: parent,
|
|
34
53
|
metadata: metadata,
|
|
35
54
|
source: "model"
|
|
@@ -2,6 +2,7 @@ module Sinaliza
|
|
|
2
2
|
class Event < ApplicationRecord
|
|
3
3
|
belongs_to :actor, polymorphic: true, optional: true
|
|
4
4
|
belongs_to :target, polymorphic: true, optional: true
|
|
5
|
+
belongs_to :context, polymorphic: true, optional: true
|
|
5
6
|
belongs_to :parent, class_name: "Sinaliza::Event", optional: true
|
|
6
7
|
|
|
7
8
|
has_many :children, class_name: "Sinaliza::Event", foreign_key: :parent_id, dependent: :destroy
|
|
@@ -11,6 +12,8 @@ module Sinaliza
|
|
|
11
12
|
scope :by_name, ->(name) { where(name: name) }
|
|
12
13
|
scope :by_source, ->(source) { where(source: source) }
|
|
13
14
|
scope :by_actor_type, ->(type) { where(actor_type: type) }
|
|
15
|
+
scope :by_context, ->(context) { where(context_type: context.class.name, context_id: context.id) }
|
|
16
|
+
scope :by_context_type, ->(type) { where(context_type: type) }
|
|
14
17
|
scope :since, ->(time) { where(created_at: time..) }
|
|
15
18
|
scope :before, ->(time) { where(created_at: ..time) }
|
|
16
19
|
scope :between, ->(from, to) { where(created_at: from..to) }
|
|
@@ -18,7 +21,7 @@ module Sinaliza
|
|
|
18
21
|
scope :reverse_chronological, -> { order(created_at: :desc) }
|
|
19
22
|
scope :roots, -> { where(parent_id: nil) }
|
|
20
23
|
scope :search, ->(query) {
|
|
21
|
-
where("name LIKE :q OR source LIKE :q OR actor_type LIKE :q OR target_type LIKE :q", q: "%#{query}%")
|
|
24
|
+
where("name LIKE :q OR source LIKE :q OR actor_type LIKE :q OR target_type LIKE :q OR context_type LIKE :q", q: "%#{query}%")
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
def root?
|
|
@@ -35,6 +35,16 @@
|
|
|
35
35
|
</select>
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
|
+
<div class="sinaliza-filters__field">
|
|
39
|
+
<label for="context_type">Context type</label>
|
|
40
|
+
<select name="context_type" id="context_type">
|
|
41
|
+
<option value="">All</option>
|
|
42
|
+
<% @filter_context_types.each do |type| %>
|
|
43
|
+
<option value="<%= type %>" <%= "selected" if params[:context_type] == type %>><%= type %></option>
|
|
44
|
+
<% end %>
|
|
45
|
+
</select>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
38
48
|
<div class="sinaliza-filters__field">
|
|
39
49
|
<label for="since">Since</label>
|
|
40
50
|
<input type="date" name="since" id="since" value="<%= params[:since] %>">
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
<th>Source</th>
|
|
12
12
|
<th>Actor</th>
|
|
13
13
|
<th>Target</th>
|
|
14
|
+
<th>Context</th>
|
|
14
15
|
<th>Children</th>
|
|
15
16
|
<th></th>
|
|
16
17
|
</tr>
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
<td><span class="sinaliza-badge sinaliza-badge--source"><%= event.source %></span></td>
|
|
24
25
|
<td><%= event.actor ? "#{event.actor_type}##{event.actor_id}" : "-" %></td>
|
|
25
26
|
<td><%= event.target ? "#{event.target_type}##{event.target_id}" : "-" %></td>
|
|
27
|
+
<td><%= event.context ? "#{event.context_type}##{event.context_id}" : "-" %></td>
|
|
26
28
|
<td><%= event.children.size %></td>
|
|
27
29
|
<td><%= link_to "Detail", event_path(event), class: "sinaliza-link" %></td>
|
|
28
30
|
</tr>
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
<th>Target</th>
|
|
27
27
|
<td><%= @event.target ? "#{@event.target_type}##{@event.target_id}" : "-" %></td>
|
|
28
28
|
</tr>
|
|
29
|
+
<tr>
|
|
30
|
+
<th>Context</th>
|
|
31
|
+
<td><%= @event.context ? "#{@event.context_type}##{@event.context_id}" : "-" %></td>
|
|
32
|
+
</tr>
|
|
29
33
|
<tr>
|
|
30
34
|
<th>Metadata</th>
|
|
31
35
|
<td><pre class="sinaliza-json"><%= JSON.pretty_generate(@event.metadata) %></pre></td>
|
data/lib/sinaliza/version.rb
CHANGED
data/lib/sinaliza.rb
CHANGED
|
@@ -12,13 +12,14 @@ module Sinaliza
|
|
|
12
12
|
yield(configuration)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def record(name:, actor: nil, target: nil, parent: nil, metadata: {}, source: nil, ip_address: nil, user_agent: nil, request_id: nil)
|
|
15
|
+
def record(name:, actor: nil, target: nil, context: nil, parent: nil, metadata: {}, source: nil, ip_address: nil, user_agent: nil, request_id: nil)
|
|
16
16
|
parent_id = parent.is_a?(Sinaliza::Event) ? parent.id : parent
|
|
17
17
|
|
|
18
18
|
Sinaliza::Event.create!(
|
|
19
19
|
name: name,
|
|
20
20
|
actor: actor,
|
|
21
21
|
target: target,
|
|
22
|
+
context: context,
|
|
22
23
|
parent_id: parent_id,
|
|
23
24
|
metadata: metadata,
|
|
24
25
|
source: source || configuration.default_source,
|
|
@@ -28,7 +29,7 @@ module Sinaliza
|
|
|
28
29
|
)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def record_later(name:, actor: nil, target: nil, parent: nil, metadata: {}, source: nil, ip_address: nil, user_agent: nil, request_id: nil)
|
|
32
|
+
def record_later(name:, actor: nil, target: nil, context: nil, parent: nil, metadata: {}, source: nil, ip_address: nil, user_agent: nil, request_id: nil)
|
|
32
33
|
attributes = {
|
|
33
34
|
name: name,
|
|
34
35
|
metadata: metadata,
|
|
@@ -40,6 +41,7 @@ module Sinaliza
|
|
|
40
41
|
|
|
41
42
|
attributes[:actor] = actor.to_global_id.to_s if actor
|
|
42
43
|
attributes[:target] = target.to_global_id.to_s if target
|
|
44
|
+
attributes[:context] = context.to_global_id.to_s if context
|
|
43
45
|
attributes[:parent_id] = parent.is_a?(Sinaliza::Event) ? parent.id : parent if parent
|
|
44
46
|
|
|
45
47
|
Sinaliza::RecordEventJob.perform_later(attributes)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sinaliza
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marcelo Moraes
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- config/routes.rb
|
|
60
60
|
- db/migrate/20260219000000_create_sinaliza_events.rb
|
|
61
61
|
- db/migrate/20260219100000_add_parent_id_to_sinaliza_events.rb
|
|
62
|
+
- db/migrate/20260220000000_add_context_to_sinaliza_events.rb
|
|
62
63
|
- lib/sinaliza.rb
|
|
63
64
|
- lib/sinaliza/configuration.rb
|
|
64
65
|
- lib/sinaliza/engine.rb
|