active_subscriber 0.1.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/.rspec +3 -0
- data/CHANGELOG.md +35 -0
- data/DEVELOPMENT_PLAN.md +275 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +304 -0
- data/INSTALLATION_GUIDE.md +193 -0
- data/LICENSE.txt +21 -0
- data/README.md +287 -0
- data/REQUIREMENTS.md +60 -0
- data/Rakefile +12 -0
- data/lib/active_subscriber/base.rb +138 -0
- data/lib/active_subscriber/configuration.rb +53 -0
- data/lib/active_subscriber/engine.rb +49 -0
- data/lib/active_subscriber/helpers/analytics_helper.rb +54 -0
- data/lib/active_subscriber/loader.rb +54 -0
- data/lib/active_subscriber/publisher.rb +107 -0
- data/lib/active_subscriber/registry.rb +125 -0
- data/lib/active_subscriber/subscriber_job.rb +24 -0
- data/lib/active_subscriber/version.rb +5 -0
- data/lib/active_subscriber.rb +52 -0
- data/lib/generators/active_subscriber/install_generator.rb +32 -0
- data/lib/generators/active_subscriber/subscriber_generator.rb +50 -0
- data/lib/generators/active_subscriber/templates/README +32 -0
- data/lib/generators/active_subscriber/templates/initializer.rb +21 -0
- data/lib/generators/active_subscriber/templates/subscriber.rb +41 -0
- metadata +128 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# ActiveSubscriber Installation Guide for Rails Apps
|
|
2
|
+
|
|
3
|
+
## Step 1: Add to Gemfile
|
|
4
|
+
|
|
5
|
+
Add this line to your Rails application's Gemfile:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# If you're testing locally, use the path to this gem
|
|
9
|
+
gem 'active_subscriber', path: '/path/to/active_subscriber'
|
|
10
|
+
|
|
11
|
+
# Or if you've published it to a git repository
|
|
12
|
+
# gem 'active_subscriber', git: 'https://github.com/afomera/active_subscriber.git'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Step 2: Bundle Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Step 3: Run the Install Generator
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
rails generate active_subscriber:install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This will create:
|
|
28
|
+
|
|
29
|
+
- `config/initializers/active_subscriber.rb` - Configuration file
|
|
30
|
+
- `app/subscribers/` directory - Where your subscribers go
|
|
31
|
+
|
|
32
|
+
## Step 4: Test the Installation
|
|
33
|
+
|
|
34
|
+
### Option A: Use the Rails Console Test Script
|
|
35
|
+
|
|
36
|
+
1. Copy the contents of `rails_console_test.rb`
|
|
37
|
+
2. Open your Rails console: `rails console`
|
|
38
|
+
3. Paste and run the test script
|
|
39
|
+
|
|
40
|
+
### Option B: Manual Testing
|
|
41
|
+
|
|
42
|
+
In your Rails console:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# 1. Check if ActiveSubscriber is loaded
|
|
46
|
+
ActiveSubscriber::VERSION
|
|
47
|
+
|
|
48
|
+
# 2. Create a test subscriber
|
|
49
|
+
class MyTestSubscriber < ActiveSubscriber::Base
|
|
50
|
+
subscribe_to :my_test_event
|
|
51
|
+
|
|
52
|
+
def handle_my_test_event(data)
|
|
53
|
+
puts "Received event: #{data}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 3. Register it
|
|
58
|
+
ActiveSubscriber.registry.register(MyTestSubscriber)
|
|
59
|
+
|
|
60
|
+
# 4. Create a publisher
|
|
61
|
+
class MyPublisher
|
|
62
|
+
include ActiveSubscriber::Publisher
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 5. Publish an event
|
|
66
|
+
MyPublisher.publish_event("my_test_event", { message: "Hello!" })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Step 5: Create Your First Real Subscriber
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
rails generate active_subscriber:subscriber Analytics --events user_signed_in user_signed_up
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This creates `app/subscribers/analytics_subscriber.rb`
|
|
76
|
+
|
|
77
|
+
## Step 6: Use in Your Application
|
|
78
|
+
|
|
79
|
+
### In a Service:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class UserService
|
|
83
|
+
include ActiveSubscriber::Publisher
|
|
84
|
+
|
|
85
|
+
def sign_in_user(user)
|
|
86
|
+
# ... sign in logic ...
|
|
87
|
+
publish_event("user_signed_in", { user_id: user.id })
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### In a Controller:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
class ApplicationController < ActionController::Base
|
|
96
|
+
include ActiveSubscriber::Helpers::AnalyticsHelper
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class PostsController < ApplicationController
|
|
100
|
+
def show
|
|
101
|
+
@post = Post.find(params[:id])
|
|
102
|
+
track_analytics("post.viewed", { post_id: @post.id })
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
### Issue: Events not being received
|
|
110
|
+
|
|
111
|
+
1. **Check if ActiveSubscriber is enabled:**
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
ActiveSubscriber.enabled?
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
2. **Check if subscribers are registered:**
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
ActiveSubscriber.registry.subscribers
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
3. **Check ActiveSupport::Notifications directly:**
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
ActiveSupport::Notifications.instrument("test", { data: "test" })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
4. **Check Rails logs** for any error messages
|
|
130
|
+
|
|
131
|
+
### Issue: Subscribers not auto-loading
|
|
132
|
+
|
|
133
|
+
1. **Check configuration:**
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
ActiveSubscriber.configuration.auto_load
|
|
137
|
+
ActiveSubscriber.configuration.subscriber_paths
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
2. **Manually load subscribers:**
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
ActiveSubscriber.loader.load_subscribers
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
3. **Check if files exist:**
|
|
147
|
+
```ruby
|
|
148
|
+
Dir.glob(Rails.root.join("app/subscribers/**/*.rb"))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Issue: Rails engine not loading
|
|
152
|
+
|
|
153
|
+
1. **Check if the engine is loaded:**
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
Rails.application.railties.map(&:class)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
2. **Manually require the engine:**
|
|
160
|
+
```ruby
|
|
161
|
+
require 'active_subscriber/engine'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Configuration Options
|
|
165
|
+
|
|
166
|
+
In `config/initializers/active_subscriber.rb`:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
ActiveSubscriber.configure do |config|
|
|
170
|
+
# Enable/disable the entire system
|
|
171
|
+
config.enabled = true
|
|
172
|
+
|
|
173
|
+
# Paths to look for subscribers
|
|
174
|
+
config.subscriber_paths = ["app/subscribers", "lib/subscribers"]
|
|
175
|
+
|
|
176
|
+
# Filter events (optional)
|
|
177
|
+
config.event_filter = ->(event_name) {
|
|
178
|
+
!event_name.start_with?("internal.")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Auto-load subscribers on startup
|
|
182
|
+
config.auto_load = true
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Next Steps
|
|
187
|
+
|
|
188
|
+
1. Run the test script to verify everything works
|
|
189
|
+
2. Create your first subscriber using the generator
|
|
190
|
+
3. Start publishing events from your services/controllers
|
|
191
|
+
4. Monitor Rails logs to see events being processed
|
|
192
|
+
|
|
193
|
+
If you encounter any issues, run the Rails console test script first to diagnose the problem.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andrea Fomera
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# ActiveSubscriber
|
|
2
|
+
|
|
3
|
+
ActiveSubscriber provides a clean interface for implementing the publisher-subscriber pattern in Rails applications using ActiveSupport Notifications, with automatic subscriber discovery and lifecycle callbacks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'active_subscriber'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
$ gem install active_subscriber
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
1. **Install ActiveSubscriber:**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
rails generate active_subscriber:install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. **Create a subscriber:**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
rails generate active_subscriber:subscriber Analytics --events user_signed_in user_signed_up
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. **Include the Publisher module in your services:**
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class AnalyticsService
|
|
43
|
+
include ActiveSubscriber::Publisher
|
|
44
|
+
|
|
45
|
+
def self.track_analytics(event_name, properties = {}, context: analytics_context)
|
|
46
|
+
publish_event(event_name, properties, context)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.track_analytics_with_timing(event_name, properties = {}, context = {}, &block)
|
|
50
|
+
publish_event_with_timing(event_name, properties, context, &block)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
4. **Include the helper in your controllers:**
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class PostController < ApplicationController
|
|
59
|
+
include ActiveSubscriber::Helpers::AnalyticsHelper
|
|
60
|
+
|
|
61
|
+
def index
|
|
62
|
+
@posts = Post.all
|
|
63
|
+
track_analytics("posts.viewed", { count: @posts.count })
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### Creating Subscribers
|
|
71
|
+
|
|
72
|
+
Subscribers inherit from `ActiveSubscriber::Base` and can subscribe to specific events or event patterns:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class LoggingSubscriber < ActiveSubscriber::Base
|
|
76
|
+
subscribe_to :user_signed_in, :user_signed_up
|
|
77
|
+
subscribe_to_pattern "analytics.*"
|
|
78
|
+
|
|
79
|
+
# Lifecycle callbacks
|
|
80
|
+
after_handle_event :increment_stats
|
|
81
|
+
around_handle_event :with_timing
|
|
82
|
+
before_handle_event :setup_context
|
|
83
|
+
|
|
84
|
+
def handle_user_signed_in(data)
|
|
85
|
+
Rails.logger.info "User signed in: #{data[:payload][:user_id]}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def handle_user_signed_up(data)
|
|
89
|
+
Rails.logger.info "New user signed up: #{data[:payload][:user_id]}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_all_events(event_name, data)
|
|
93
|
+
Rails.logger.info "Analytics event: #{event_name}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def increment_stats
|
|
99
|
+
# Called after handling any event
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def with_timing
|
|
103
|
+
start_time = Time.current
|
|
104
|
+
yield
|
|
105
|
+
duration = Time.current - start_time
|
|
106
|
+
Rails.logger.debug "Event handled in #{duration * 1000}ms"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def setup_context
|
|
110
|
+
# Called before handling any event
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Publishing Events
|
|
116
|
+
|
|
117
|
+
#### Using the Publisher Module
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class UserService
|
|
121
|
+
include ActiveSubscriber::Publisher
|
|
122
|
+
|
|
123
|
+
def sign_in_user(user)
|
|
124
|
+
# ... sign in logic ...
|
|
125
|
+
|
|
126
|
+
publish_event("user_signed_in", { user_id: user.id }, { ip: request.ip })
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def create_user(params)
|
|
130
|
+
result = publish_event_with_timing("user_creation", { email: params[:email] }) do
|
|
131
|
+
User.create!(params)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
publish_event("user_signed_up", { user_id: result.id })
|
|
135
|
+
result
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Using the Analytics Helper
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class ApplicationController < ActionController::Base
|
|
144
|
+
include ActiveSubscriber::Helpers::AnalyticsHelper
|
|
145
|
+
|
|
146
|
+
def show
|
|
147
|
+
@post = Post.find(params[:id])
|
|
148
|
+
track_analytics("post.viewed", {
|
|
149
|
+
post_id: @post.id,
|
|
150
|
+
category: @post.category
|
|
151
|
+
})
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Event Data Structure
|
|
157
|
+
|
|
158
|
+
Events published through ActiveSubscriber include:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
{
|
|
162
|
+
payload: { /* your custom data */ },
|
|
163
|
+
context: { /* additional context */ },
|
|
164
|
+
published_at: Time.current,
|
|
165
|
+
publisher: "YourServiceClass"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
When using the analytics helper, additional context is automatically added:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
{
|
|
173
|
+
controller: "posts",
|
|
174
|
+
action: "show",
|
|
175
|
+
user_id: current_user&.id,
|
|
176
|
+
user_agent: request.user_agent,
|
|
177
|
+
ip_address: request.remote_ip,
|
|
178
|
+
# ... more request context
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Configuration
|
|
183
|
+
|
|
184
|
+
Configure ActiveSubscriber in `config/initializers/active_subscriber.rb`:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
ActiveSubscriber.configure do |config|
|
|
188
|
+
# Enable or disable ActiveSubscriber (default: true)
|
|
189
|
+
config.enabled = true
|
|
190
|
+
|
|
191
|
+
# Namespace for Notification events
|
|
192
|
+
config.namespace = "my_app"
|
|
193
|
+
|
|
194
|
+
# Paths where subscribers are located (default: ["app/subscribers"])
|
|
195
|
+
config.subscriber_paths = ["app/subscribers", "lib/subscribers"]
|
|
196
|
+
|
|
197
|
+
# Filter events (optional)
|
|
198
|
+
config.event_filter = ->(event_name) { !event_name.start_with?("internal.") }
|
|
199
|
+
|
|
200
|
+
# Auto-load subscribers (default: true)
|
|
201
|
+
config.auto_load = true
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Lifecycle Callbacks
|
|
206
|
+
|
|
207
|
+
Subscribers support three types of lifecycle callbacks:
|
|
208
|
+
|
|
209
|
+
- `before_handle_event` - Called before handling any event
|
|
210
|
+
- `after_handle_event` - Called after handling any event
|
|
211
|
+
- `around_handle_event` - Wraps the event handling (must yield)
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class TimingSubscriber < ActiveSubscriber::Base
|
|
215
|
+
subscribe_to_pattern ".*"
|
|
216
|
+
|
|
217
|
+
around_handle_event :with_timing
|
|
218
|
+
after_handle_event :log_completion
|
|
219
|
+
|
|
220
|
+
def handle_all_events(event_name, data)
|
|
221
|
+
# Handle the event
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def with_timing
|
|
227
|
+
start_time = Time.current
|
|
228
|
+
yield
|
|
229
|
+
@duration = Time.current - start_time
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def log_completion
|
|
233
|
+
Rails.logger.info "Event completed in #{@duration * 1000}ms"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Pattern Matching
|
|
239
|
+
|
|
240
|
+
Subscribe to events using patterns with wildcards:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
class PatternSubscriber < ActiveSubscriber::Base
|
|
244
|
+
subscribe_to_pattern "user.*" # Matches user.created, user.updated, etc.
|
|
245
|
+
subscribe_to_pattern "analytics.*" # Matches analytics.page_view, analytics.click, etc.
|
|
246
|
+
subscribe_to_pattern ".*" # Matches all events
|
|
247
|
+
|
|
248
|
+
def handle_all_events(event_name, data)
|
|
249
|
+
case event_name
|
|
250
|
+
when /^user\./
|
|
251
|
+
handle_user_event(event_name, data)
|
|
252
|
+
when /^analytics\./
|
|
253
|
+
handle_analytics_event(event_name, data)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Error Handling
|
|
260
|
+
|
|
261
|
+
ActiveSubscriber includes built-in error handling. If a subscriber raises an exception, it will be logged but won't prevent other subscribers from processing the event.
|
|
262
|
+
|
|
263
|
+
Errors are logged to `Rails.logger` with the format:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
ActiveSubscriber: Error handling event 'event_name': Error message
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Thread Safety
|
|
270
|
+
|
|
271
|
+
ActiveSubscriber is designed to be thread-safe:
|
|
272
|
+
|
|
273
|
+
- The subscriber registry uses a mutex for thread-safe registration
|
|
274
|
+
- No shared mutable state in core components
|
|
275
|
+
- Each subscriber instance handles events independently
|
|
276
|
+
|
|
277
|
+
## Acknowledgements
|
|
278
|
+
|
|
279
|
+
This gem was inspired by a project at a previous role that needed a clean way to handle pub/sub notifications for third-party analytics. ActiveSubscriber is written entirely on personal time and resources, but the design is heavily informed by the needs of that work — along with my own.
|
|
280
|
+
|
|
281
|
+
## Development
|
|
282
|
+
|
|
283
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
284
|
+
|
|
285
|
+
## Contributing
|
|
286
|
+
|
|
287
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/afomera/active_subscriber.
|
data/REQUIREMENTS.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ActiveSubscriber requirements
|
|
2
|
+
|
|
3
|
+
Implement pub/sub pattern with ActiveSupport Notifications for apps to implement Subscribers to handle event logic.
|
|
4
|
+
|
|
5
|
+
So ideally..
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class AnalyticsService
|
|
9
|
+
include ActiveSubscriber::Publisher
|
|
10
|
+
|
|
11
|
+
def self.track_analytics(event_name, properties = {}, context: analytics_context)
|
|
12
|
+
publish_event(event_name, properties, context)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.track_analytics_with_timing(event_name, properties..., &block)
|
|
16
|
+
# ...
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class PostController
|
|
23
|
+
include AnalyticsHelper
|
|
24
|
+
|
|
25
|
+
def index
|
|
26
|
+
@post = Post.all
|
|
27
|
+
track_analytics("viewed_post", { post_id: @post.id })
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# app/subscribers/logging_subscriber.rb
|
|
34
|
+
class LoggingSubscriber < ActiveSubscriber::Base
|
|
35
|
+
subscribe_to :user_signed_in, :user_signed_up
|
|
36
|
+
subscribe_to_pattern ".*"
|
|
37
|
+
|
|
38
|
+
after_handle_event :increment_stats
|
|
39
|
+
around_handle_event :....
|
|
40
|
+
before_handle_event :....
|
|
41
|
+
|
|
42
|
+
def handle_user_signed_in(context)
|
|
43
|
+
# called only on user_signed_in event automatically if it responds to it
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_all_events(event_name, context)
|
|
47
|
+
# called for all events
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def increment_stats
|
|
53
|
+
puts "Increment stats counter"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
There should be a job that handles sending the handle\_ methods, and it should be able to be put in the background for performance reasons.
|
|
59
|
+
|
|
60
|
+
Also needs a way to configure the namespace for the active support notifications and whatnot. We'd have external subscribers like UserPilot and Mixpanel in the future
|
data/Rakefile
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveSubscriber
|
|
4
|
+
class Base
|
|
5
|
+
class << self
|
|
6
|
+
def subscribed_events
|
|
7
|
+
@subscribed_events ||= []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def subscribed_patterns
|
|
11
|
+
@subscribed_patterns ||= []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def before_handle_callbacks
|
|
15
|
+
@before_handle_callbacks ||= []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def after_handle_callbacks
|
|
19
|
+
@after_handle_callbacks ||= []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def around_handle_callbacks
|
|
23
|
+
@around_handle_callbacks ||= []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def subscribe_to(*event_names)
|
|
27
|
+
@subscribed_events ||= []
|
|
28
|
+
@subscribed_events.concat(event_names.map(&:to_s))
|
|
29
|
+
@subscribed_events.uniq!
|
|
30
|
+
|
|
31
|
+
if defined?(ActiveSubscriber) && ActiveSubscriber.respond_to?(:configuration) && ActiveSubscriber.configuration.auto_load
|
|
32
|
+
ActiveSubscriber.registry.register(self)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def subscribe_to_pattern(pattern)
|
|
37
|
+
@subscribed_patterns ||= []
|
|
38
|
+
pattern_regex = case pattern
|
|
39
|
+
when String
|
|
40
|
+
Regexp.new(pattern.gsub('*', '.*'))
|
|
41
|
+
when Regexp
|
|
42
|
+
pattern
|
|
43
|
+
else
|
|
44
|
+
Regexp.new(pattern.to_s.gsub('*', '.*'))
|
|
45
|
+
end
|
|
46
|
+
@subscribed_patterns << pattern_regex
|
|
47
|
+
@subscribed_patterns.uniq!
|
|
48
|
+
|
|
49
|
+
if defined?(ActiveSubscriber) && ActiveSubscriber.respond_to?(:configuration) && ActiveSubscriber.configuration.auto_load
|
|
50
|
+
ActiveSubscriber.registry.register(self)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def before_handle_event(method_name)
|
|
55
|
+
@before_handle_callbacks ||= []
|
|
56
|
+
@before_handle_callbacks << method_name
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def after_handle_event(method_name)
|
|
60
|
+
@after_handle_callbacks ||= []
|
|
61
|
+
@after_handle_callbacks << method_name
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def around_handle_event(method_name)
|
|
65
|
+
@around_handle_callbacks ||= []
|
|
66
|
+
@around_handle_callbacks << method_name
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_event(event_name, data)
|
|
71
|
+
run_callbacks do
|
|
72
|
+
handler_method = "handle_#{event_name.gsub('.', '_').gsub('-', '_')}"
|
|
73
|
+
|
|
74
|
+
if respond_to?(handler_method, true)
|
|
75
|
+
send(handler_method, data)
|
|
76
|
+
elsif respond_to?(:handle_all_events, true)
|
|
77
|
+
handle_all_events(event_name, data)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def run_callbacks(&block)
|
|
85
|
+
# Run before callbacks
|
|
86
|
+
self.class.before_handle_callbacks.each do |callback|
|
|
87
|
+
next unless respond_to?(callback, true)
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
send(callback)
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
ActiveSubscriber.logger.error "ActiveSubscriber: Error in before callback '#{callback}': #{e.message}"
|
|
93
|
+
ActiveSubscriber.logger.error e.backtrace.join("\n") if e.backtrace
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Run around callbacks or yield directly
|
|
98
|
+
if self.class.around_handle_callbacks.any?
|
|
99
|
+
run_around_callbacks(0, &block)
|
|
100
|
+
else
|
|
101
|
+
yield
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Run after callbacks
|
|
105
|
+
self.class.after_handle_callbacks.each do |callback|
|
|
106
|
+
next unless respond_to?(callback, true)
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
send(callback)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
ActiveSubscriber.logger.error "ActiveSubscriber: Error in after callback '#{callback}': #{e.message}"
|
|
112
|
+
ActiveSubscriber.logger.error e.backtrace.join("\n") if e.backtrace
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def run_around_callbacks(index, &block)
|
|
118
|
+
if index < self.class.around_handle_callbacks.length
|
|
119
|
+
callback = self.class.around_handle_callbacks[index]
|
|
120
|
+
if respond_to?(callback, true)
|
|
121
|
+
begin
|
|
122
|
+
send(callback) do
|
|
123
|
+
run_around_callbacks(index + 1, &block)
|
|
124
|
+
end
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
ActiveSubscriber.logger.error "ActiveSubscriber: Error in around callback '#{callback}': #{e.message}"
|
|
127
|
+
ActiveSubscriber.logger.error e.backtrace.join("\n") if e.backtrace
|
|
128
|
+
run_around_callbacks(index + 1, &block)
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
run_around_callbacks(index + 1, &block)
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
yield
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|