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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a54ea24b56cc1b698c2a65e3cbaff8b1af36c827d596aadfa78206f393df7bd
4
- data.tar.gz: 86114154d0ebf6e8753fbdbb4e364edda9f5b529d681d9f8cfabf52d56de4f48
3
+ metadata.gz: bc89fcab6f596adbd552ed8b28da09bba68fc38afc1d784855ebe95361b077ce
4
+ data.tar.gz: eb6b974f6c9701a96b8aca56364e562ace77804aa939c87e5ca808a26efbe7e9
5
5
  SHA512:
6
- metadata.gz: e269b6b55f5dcd9206d98f3a4e7ba366e032c1542fa794ac8313596c11aa3a29ebac6073d261e5ab16d5d310299dc8d5f79e1095d49ec8a90067341cb37e0dee
7
- data.tar.gz: 65b77aeb058512973a1128c96ebaede39a82f044363d5e961203ed80f2db01b0a3d51f093d2514d8dad101865154e7d604bf57163ff6a95eb13184cb0d32f7bf
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 # events where user is the actor
68
- user.events_as_target # events where user is the 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
- Events are recorded with `source: "model"`. When an actor or target is destroyed, associated events are preserved with nullified references (`dependent: :nullify`).
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 `target` columns. The `metadata` column uses `json` type for cross-database compatibility (SQLite, PostgreSQL, MySQL).
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>
@@ -0,0 +1,5 @@
1
+ class AddContextToSinalizaEvents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_reference :sinaliza_events, :context, polymorphic: true, index: true
4
+ end
5
+ end
@@ -1,3 +1,3 @@
1
1
  module Sinaliza
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.3
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