$npx -y skills add ThibautBaissac/rails_ai_agents --skill event-trackingBuilds event tracking, activity feeds, and webhook systems following 37signals patterns with a generic Event model and Eventable concern. Use when implementing audit trails, activity feeds, event recording, webhooks, or when user mentions events, tracking, webhooks, or activity l
| 1 | # Event Tracking |
| 2 | |
| 3 | ## Philosophy: Generic Event Model + Eventable Concern |
| 4 | |
| 5 | - One `Event` model with `action` string and `eventable` polymorphic association |
| 6 | - An `Eventable` concern mixed into models that need tracking |
| 7 | - Models call `track_event("closed", particulars: {...})` in their domain methods |
| 8 | - `particulars` JSON field stores action-specific metadata |
| 9 | - Events drive activity feeds, notifications, and webhook deliveries |
| 10 | - Everything is database-backed (Solid Queue for webhooks, no Redis/Kafka) |
| 11 | |
| 12 | ## Project Knowledge |
| 13 | |
| 14 | **Stack:** Solid Queue for background jobs, Turbo Streams for real-time activity |
| 15 | feed updates, UUIDs for all primary keys, MySQL (SaaS) / SQLite (OSS). |
| 16 | |
| 17 | **Multi-tenancy:** All events scoped to account via `account_id`. Events also |
| 18 | scoped to board via `board_id`. |
| 19 | |
| 20 | **Commands:** |
| 21 | ```bash |
| 22 | # Generate Event model |
| 23 | rails generate model Event action:string eventable:references{polymorphic} \ |
| 24 | board:references creator:references account:references particulars:json |
| 25 | |
| 26 | # Generate Webhook models |
| 27 | rails generate model Webhook url:text name:string board:references \ |
| 28 | account:references subscribed_actions:text signing_secret:string active:boolean |
| 29 | rails generate model Webhook::Delivery webhook:references event:references \ |
| 30 | account:references state:string request:text response:text |
| 31 | rails generate model Webhook::DelinquencyTracker webhook:references \ |
| 32 | account:references consecutive_failures_count:integer first_failure_at:datetime |
| 33 | ``` |
| 34 | |
| 35 | ## Pattern 1: Event Model |
| 36 | |
| 37 | See @references/domain-events.md for full implementation details. |
| 38 | |
| 39 | A single generic `Event` model records all business events: |
| 40 | |
| 41 | ```ruby |
| 42 | # app/models/event.rb |
| 43 | class Event < ApplicationRecord |
| 44 | include Notifiable, Particulars |
| 45 | |
| 46 | belongs_to :account, default: -> { board.account } |
| 47 | belongs_to :board |
| 48 | belongs_to :creator, class_name: "User" |
| 49 | belongs_to :eventable, polymorphic: true |
| 50 | |
| 51 | has_many :webhook_deliveries, class_name: "Webhook::Delivery", dependent: :delete_all |
| 52 | |
| 53 | scope :chronologically, -> { order created_at: :asc, id: :desc } |
| 54 | scope :preloaded, -> { |
| 55 | includes(:creator, :board, { |
| 56 | eventable: [ |
| 57 | :closure, :image_attachment, |
| 58 | { rich_text_body: :embeds_attachments }, |
| 59 | { card: [ :closure, :image_attachment ] } |
| 60 | ] |
| 61 | }) |
| 62 | } |
| 63 | |
| 64 | after_create -> { eventable.event_was_created(self) } |
| 65 | after_create_commit :dispatch_webhooks |
| 66 | |
| 67 | delegate :card, to: :eventable |
| 68 | |
| 69 | def action |
| 70 | super.inquiry |
| 71 | end |
| 72 | |
| 73 | def description_for(user) |
| 74 | Event::Description.new(self, user) |
| 75 | end |
| 76 | |
| 77 | private |
| 78 | def dispatch_webhooks |
| 79 | Event::WebhookDispatchJob.perform_later(self) |
| 80 | end |
| 81 | end |
| 82 | ``` |
| 83 | |
| 84 | Key design decisions: |
| 85 | - `action` is a plain string like `"card_closed"`, `"comment_created"`, |
| 86 | `"card_triaged"`. Calling `.inquiry` lets you do `event.action.card_closed?`. |
| 87 | - `eventable` points to the model that triggered the event (Card, Comment, etc.). |
| 88 | - `particulars` is a JSON column for action-specific metadata (old title, new |
| 89 | board name, assignee IDs, column name, etc.). |
| 90 | - `after_create` (not `after_create_commit`) calls back into the eventable so |
| 91 | it can create system comments or touch timestamps within the same transaction. |
| 92 | - `after_create_commit` dispatches webhooks asynchronously. |
| 93 | |
| 94 | ## Pattern 2: Eventable Concern |
| 95 | |
| 96 | See @references/domain-events.md for the full concern hierarchy. |
| 97 | |
| 98 | The base `Eventable` concern provides `track_event` to any model: |
| 99 | |
| 100 | ```ruby |
| 101 | # app/models/concerns/eventable.rb |
| 102 | module Eventable |
| 103 | extend ActiveSupport::Concern |
| 104 | |
| 105 | included do |
| 106 | has_many :events, as: :eventable, dependent: :destroy |
| 107 | end |
| 108 | |
| 109 | def track_event(action, creator: Current.user, board: self.board, **particulars) |
| 110 | if should_track_event? |
| 111 | board.events.create!( |
| 112 | action: "#{eventable_prefix}_#{action}", |
| 113 | creator:, board:, eventable: self, particulars: |
| 114 | ) |
| 115 | end |
| 116 | end |
| 117 | |
| 118 | def event_was_created(event) |
| 119 | end |
| 120 | |
| 121 | private |
| 122 | def should_track_event? |
| 123 | true |
| 124 | end |
| 125 | |
| 126 | def eventable_prefix |
| 127 | self.class.name.demodulize.underscore |
| 128 | end |
| 129 | end |
| 130 | ``` |
| 131 | |
| 132 | Models override `Eventable` with model-specific concerns that customize behavior: |
| 133 | |
| 134 | ```ruby |
| 135 | # app/models/card/eventable.rb |
| 136 | module Card::Eventable |
| 137 | extend ActiveSupport::Concern |
| 138 | |
| 139 | include ::Eventable |
| 140 | |
| 141 | included do |
| 142 | after_save :track_title_change, if: :saved_change_to_title? |
| 143 | end |
| 144 | |
| 145 | def event_was_created(event) |
| 146 | transaction do |
| 147 | create_system_comment_for(event) |
| 148 | touch_last_active_at |
| 149 | end |
| 150 | end |
| 151 | |
| 152 | private |
| 153 | def should_track_ |