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,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveSubscriber
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_reader :subscriber_paths, :namespace
|
|
6
|
+
attr_accessor :enabled, :event_filter, :auto_load, :async
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@enabled = true
|
|
10
|
+
@subscriber_paths = ['app/subscribers']
|
|
11
|
+
@event_filter = nil
|
|
12
|
+
@auto_load = true
|
|
13
|
+
@namespace = 'active_subscriber'
|
|
14
|
+
@async = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env.production? : false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def subscriber_paths=(value)
|
|
18
|
+
raise ArgumentError, "subscriber_paths must be an Array, got #{value.class}" unless value.is_a?(Array)
|
|
19
|
+
|
|
20
|
+
@subscriber_paths = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def namespace=(value)
|
|
24
|
+
unless value.nil? || value.is_a?(String)
|
|
25
|
+
raise ArgumentError, "namespace must be a String or nil, got #{value.class}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@namespace = value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def event_allowed?(event_name)
|
|
32
|
+
return true unless event_filter
|
|
33
|
+
|
|
34
|
+
case event_filter
|
|
35
|
+
when Proc
|
|
36
|
+
event_filter.call(event_name)
|
|
37
|
+
when String, Regexp
|
|
38
|
+
event_name.match?(event_filter)
|
|
39
|
+
else
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset!
|
|
45
|
+
@enabled = true
|
|
46
|
+
@subscriber_paths = ['app/subscribers']
|
|
47
|
+
@event_filter = nil
|
|
48
|
+
@auto_load = true
|
|
49
|
+
@namespace = 'active_subscriber'
|
|
50
|
+
@async = false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/engine'
|
|
4
|
+
|
|
5
|
+
module ActiveSubscriber
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace ActiveSubscriber
|
|
8
|
+
|
|
9
|
+
config.active_subscriber = ActiveSupport::OrderedOptions.new
|
|
10
|
+
|
|
11
|
+
initializer 'active_subscriber.configure' do |app|
|
|
12
|
+
ActiveSubscriber.configure do |config|
|
|
13
|
+
# Apply any configuration from Rails config
|
|
14
|
+
config.enabled = app.config.active_subscriber.enabled if app.config.active_subscriber.enabled.present?
|
|
15
|
+
|
|
16
|
+
if app.config.active_subscriber.subscriber_paths.present?
|
|
17
|
+
config.subscriber_paths = app.config.active_subscriber.subscriber_paths
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if app.config.active_subscriber.event_filter.present?
|
|
21
|
+
config.event_filter = app.config.active_subscriber.event_filter
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
config.auto_load = app.config.active_subscriber.auto_load if app.config.active_subscriber.auto_load.present?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
initializer 'active_subscriber.load_subscribers', after: :load_config_initializers do
|
|
29
|
+
ActiveSupport.on_load(:active_record) do
|
|
30
|
+
ActiveSubscriber.loader.load_subscribers
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Reload subscribers in development when files change
|
|
35
|
+
config.to_prepare do
|
|
36
|
+
ActiveSubscriber.loader.reload_subscribers if Rails.env.development?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Add app/subscribers to autoload paths
|
|
40
|
+
config.before_configuration do
|
|
41
|
+
config.autoload_paths << Rails.root.join('app', 'subscribers') if Rails.root
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Ensure cleanup on application shutdown
|
|
45
|
+
at_exit do
|
|
46
|
+
ActiveSubscriber.registry.clear if defined?(ActiveSubscriber)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveSubscriber
|
|
4
|
+
module Helpers
|
|
5
|
+
module AnalyticsHelper
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
include ActiveSubscriber::Publisher
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def track_analytics(event_name, properties = {}, context = {})
|
|
13
|
+
analytics_context = build_analytics_context.merge(context)
|
|
14
|
+
publish_event(event_name, properties, analytics_context)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def track_analytics_with_timing(event_name, properties = {}, context = {}, &block)
|
|
18
|
+
analytics_context = build_analytics_context.merge(context)
|
|
19
|
+
publish_event_with_timing(event_name, properties, analytics_context, &block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_analytics_context
|
|
25
|
+
context = {
|
|
26
|
+
controller: controller_name,
|
|
27
|
+
action: action_name,
|
|
28
|
+
timestamp: Time.current
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Add request information if available
|
|
32
|
+
if defined?(request) && request
|
|
33
|
+
context.merge!(
|
|
34
|
+
user_agent: request.user_agent,
|
|
35
|
+
ip_address: request.remote_ip,
|
|
36
|
+
referer: request.referer,
|
|
37
|
+
request_id: request.request_id
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Add user information if current_user is available
|
|
42
|
+
if respond_to?(:current_user) && current_user
|
|
43
|
+
context[:user_id] = current_user.id
|
|
44
|
+
context[:user_type] = current_user.class.name
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Add session information if available
|
|
48
|
+
context[:session_id] = session.id if respond_to?(:session) && session.respond_to?(:id)
|
|
49
|
+
|
|
50
|
+
context
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module ActiveSubscriber
|
|
6
|
+
class Loader
|
|
7
|
+
def initialize
|
|
8
|
+
@loaded_paths = Set.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def load_subscribers
|
|
12
|
+
return unless ActiveSubscriber.configuration.auto_load
|
|
13
|
+
|
|
14
|
+
ActiveSubscriber.configuration.subscriber_paths.each do |path|
|
|
15
|
+
load_subscribers_from_path(path)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reload_subscribers
|
|
20
|
+
ActiveSubscriber.registry.clear
|
|
21
|
+
@loaded_paths.clear
|
|
22
|
+
load_subscribers
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def load_subscribers_from_path(path)
|
|
28
|
+
return unless Dir.exist?(path)
|
|
29
|
+
return if @loaded_paths.include?(path)
|
|
30
|
+
|
|
31
|
+
@loaded_paths << path
|
|
32
|
+
|
|
33
|
+
Dir.glob(File.join(path, '**', '*.rb')).each do |file|
|
|
34
|
+
load_subscriber_file(file)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def load_subscriber_file(file_path)
|
|
39
|
+
if defined?(Rails) && Rails.respond_to?(:env)
|
|
40
|
+
return if Rails.env.production? && !File.exist?(file_path)
|
|
41
|
+
|
|
42
|
+
if Rails.env.development?
|
|
43
|
+
load file_path
|
|
44
|
+
else
|
|
45
|
+
require file_path
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
require file_path
|
|
49
|
+
end
|
|
50
|
+
rescue LoadError, StandardError => e
|
|
51
|
+
ActiveSubscriber.logger.error "ActiveSubscriber: Failed to load subscriber file #{file_path}: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveSubscriber
|
|
4
|
+
module Publisher
|
|
5
|
+
def publish_event(event_name, payload = {}, context = {})
|
|
6
|
+
return unless ActiveSubscriber.enabled?
|
|
7
|
+
|
|
8
|
+
namespaced_event = namespace_event(event_name)
|
|
9
|
+
return unless ActiveSubscriber.configuration.event_allowed?(namespaced_event)
|
|
10
|
+
|
|
11
|
+
event_data = {
|
|
12
|
+
payload: payload,
|
|
13
|
+
context: context,
|
|
14
|
+
published_at: Time.current,
|
|
15
|
+
publisher: self.class.name || 'Anonymous'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ActiveSupport::Notifications.instrument(namespaced_event, event_data)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def publish_event_with_timing(event_name, payload = {}, context = {})
|
|
22
|
+
return yield unless ActiveSubscriber.enabled?
|
|
23
|
+
|
|
24
|
+
namespaced_event = namespace_event(event_name)
|
|
25
|
+
return yield unless ActiveSubscriber.configuration.event_allowed?(namespaced_event)
|
|
26
|
+
|
|
27
|
+
start_time = Time.current
|
|
28
|
+
result = yield
|
|
29
|
+
end_time = Time.current
|
|
30
|
+
|
|
31
|
+
event_data = {
|
|
32
|
+
payload: payload,
|
|
33
|
+
context: context,
|
|
34
|
+
published_at: start_time,
|
|
35
|
+
completed_at: end_time,
|
|
36
|
+
duration: (end_time - start_time) * 1000,
|
|
37
|
+
publisher: self.class.name || 'Anonymous'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ActiveSupport::Notifications.instrument(namespaced_event, event_data)
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def namespace_event(event_name)
|
|
47
|
+
namespace = ActiveSubscriber.configuration.namespace
|
|
48
|
+
return event_name.to_s if namespace.nil? || namespace.empty?
|
|
49
|
+
|
|
50
|
+
"#{namespace}.#{event_name}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module ClassMethods
|
|
54
|
+
def publish_event(event_name, payload = {}, context = {})
|
|
55
|
+
return unless ActiveSubscriber.enabled?
|
|
56
|
+
|
|
57
|
+
namespaced_event = namespace_event(event_name)
|
|
58
|
+
return unless ActiveSubscriber.configuration.event_allowed?(namespaced_event)
|
|
59
|
+
|
|
60
|
+
event_data = {
|
|
61
|
+
payload: payload,
|
|
62
|
+
context: context,
|
|
63
|
+
published_at: Time.current,
|
|
64
|
+
publisher: name || 'Anonymous'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ActiveSupport::Notifications.instrument(namespaced_event, event_data)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def publish_event_with_timing(event_name, payload = {}, context = {})
|
|
71
|
+
return yield unless ActiveSubscriber.enabled?
|
|
72
|
+
|
|
73
|
+
namespaced_event = namespace_event(event_name)
|
|
74
|
+
return yield unless ActiveSubscriber.configuration.event_allowed?(namespaced_event)
|
|
75
|
+
|
|
76
|
+
start_time = Time.current
|
|
77
|
+
result = yield
|
|
78
|
+
end_time = Time.current
|
|
79
|
+
|
|
80
|
+
event_data = {
|
|
81
|
+
payload: payload,
|
|
82
|
+
context: context,
|
|
83
|
+
published_at: start_time,
|
|
84
|
+
completed_at: end_time,
|
|
85
|
+
duration: (end_time - start_time) * 1000,
|
|
86
|
+
publisher: name || 'Anonymous'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ActiveSupport::Notifications.instrument(namespaced_event, event_data)
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def namespace_event(event_name)
|
|
96
|
+
namespace = ActiveSubscriber.configuration.namespace
|
|
97
|
+
return event_name.to_s if namespace.nil? || namespace.empty?
|
|
98
|
+
|
|
99
|
+
"#{namespace}.#{event_name}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.included(base)
|
|
104
|
+
base.extend(ClassMethods)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveSubscriber
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize
|
|
6
|
+
@subscribers = []
|
|
7
|
+
@subscriptions = {}
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register(subscriber_class)
|
|
12
|
+
@mutex.synchronize do
|
|
13
|
+
if @subscribers.include?(subscriber_class)
|
|
14
|
+
cleanup_subscriptions(subscriber_class)
|
|
15
|
+
else
|
|
16
|
+
@subscribers << subscriber_class
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
setup_subscriptions(subscriber_class)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def unregister(subscriber_class)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
@subscribers.delete(subscriber_class)
|
|
26
|
+
cleanup_subscriptions(subscriber_class)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subscribers
|
|
31
|
+
@subscribers.dup
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@subscriptions.each_value do |subscription|
|
|
37
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
38
|
+
end
|
|
39
|
+
@subscriptions.clear
|
|
40
|
+
@subscribers.clear
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def setup_subscriptions(subscriber_class)
|
|
47
|
+
subscriber_class.subscribed_events.each do |event_name|
|
|
48
|
+
namespaced_event = namespace_event(event_name)
|
|
49
|
+
subscription = ActiveSupport::Notifications.subscribe(namespaced_event) do |name, _started, _finished, _unique_id, data|
|
|
50
|
+
next unless ActiveSubscriber.enabled?
|
|
51
|
+
next unless ActiveSubscriber.configuration.event_allowed?(name)
|
|
52
|
+
|
|
53
|
+
instance = subscriber_class.new
|
|
54
|
+
handle_event(instance, event_name, data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if subscription
|
|
58
|
+
@subscriptions["#{subscriber_class.name}:#{event_name}"] = subscription
|
|
59
|
+
else
|
|
60
|
+
ActiveSubscriber.logger.warn "ActiveSubscriber: Failed to create subscription for #{event_name}"
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
ActiveSubscriber.logger.error "ActiveSubscriber: Error setting up subscription for #{event_name}: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
subscriber_class.subscribed_patterns.each do |pattern|
|
|
67
|
+
namespaced_pattern = namespace_pattern(pattern)
|
|
68
|
+
subscription = ActiveSupport::Notifications.subscribe(namespaced_pattern) do |name, _started, _finished, _unique_id, data|
|
|
69
|
+
next unless ActiveSubscriber.enabled?
|
|
70
|
+
next unless ActiveSubscriber.configuration.event_allowed?(name)
|
|
71
|
+
|
|
72
|
+
instance = subscriber_class.new
|
|
73
|
+
event_name = name.sub(/^#{ActiveSubscriber.configuration.namespace}\./, '')
|
|
74
|
+
handle_event(instance, event_name, data)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if subscription
|
|
78
|
+
@subscriptions["#{subscriber_class.name}:pattern:#{pattern}"] = subscription
|
|
79
|
+
else
|
|
80
|
+
ActiveSubscriber.logger.warn "ActiveSubscriber: Failed to create subscription for pattern #{pattern}"
|
|
81
|
+
end
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
ActiveSubscriber.logger.error "ActiveSubscriber: Error setting up subscription for pattern #{pattern}: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def namespace_event(event_name)
|
|
88
|
+
namespace = ActiveSubscriber.configuration.namespace
|
|
89
|
+
return event_name.to_s if namespace.nil? || namespace.empty?
|
|
90
|
+
|
|
91
|
+
"#{namespace}.#{event_name}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def namespace_pattern(pattern)
|
|
95
|
+
namespace = ActiveSubscriber.configuration.namespace
|
|
96
|
+
return pattern if namespace.nil? || namespace.empty?
|
|
97
|
+
|
|
98
|
+
case pattern
|
|
99
|
+
when Regexp
|
|
100
|
+
Regexp.new("#{Regexp.escape(namespace)}\\.#{pattern.source}", pattern.options)
|
|
101
|
+
else
|
|
102
|
+
Regexp.new("#{Regexp.escape(namespace)}\\.#{pattern}")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def cleanup_subscriptions(subscriber_class)
|
|
107
|
+
keys_to_remove = @subscriptions.keys.select { |key| key.start_with?("#{subscriber_class.name}:") }
|
|
108
|
+
keys_to_remove.each do |key|
|
|
109
|
+
ActiveSupport::Notifications.unsubscribe(@subscriptions[key])
|
|
110
|
+
@subscriptions.delete(key)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_event(instance, event_name, data)
|
|
115
|
+
if ActiveSubscriber.configuration.async
|
|
116
|
+
ActiveSubscriber::SubscriberJob.perform_later(instance.class.name, event_name, data)
|
|
117
|
+
else
|
|
118
|
+
instance.handle_event(event_name, data)
|
|
119
|
+
end
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
ActiveSubscriber.logger.error "ActiveSubscriber: Error handling event '#{event_name}': #{e.message}"
|
|
122
|
+
ActiveSubscriber.logger.error e.backtrace.join("\n") if e.backtrace
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_job'
|
|
4
|
+
|
|
5
|
+
module ActiveSubscriber
|
|
6
|
+
class SubscriberJob < ActiveJob::Base
|
|
7
|
+
queue_as :default
|
|
8
|
+
|
|
9
|
+
def perform(subscriber_class_name, event_name, data)
|
|
10
|
+
subscriber_class = subscriber_class_name.safe_constantize
|
|
11
|
+
|
|
12
|
+
if subscriber_class.nil?
|
|
13
|
+
raise ArgumentError, "ActiveSubscriber::SubscriberJob: Unknown subscriber class '#{subscriber_class_name}'"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
instance = subscriber_class.new
|
|
17
|
+
instance.handle_event(event_name, data)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
ActiveSubscriber.logger.error "ActiveSubscriber::SubscriberJob: Error handling event '#{event_name}' with #{subscriber_class_name}: #{e.message}"
|
|
20
|
+
ActiveSubscriber.logger.error e.backtrace.join("\n") if e.backtrace
|
|
21
|
+
raise e
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/notifications'
|
|
6
|
+
|
|
7
|
+
require_relative 'active_subscriber/version'
|
|
8
|
+
require_relative 'active_subscriber/configuration'
|
|
9
|
+
require_relative 'active_subscriber/subscriber_job'
|
|
10
|
+
require_relative 'active_subscriber/registry'
|
|
11
|
+
require_relative 'active_subscriber/publisher'
|
|
12
|
+
require_relative 'active_subscriber/base'
|
|
13
|
+
require_relative 'active_subscriber/loader'
|
|
14
|
+
require_relative 'active_subscriber/helpers/analytics_helper'
|
|
15
|
+
|
|
16
|
+
if defined?(Rails)
|
|
17
|
+
require_relative 'active_subscriber/engine'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module ActiveSubscriber
|
|
21
|
+
class << self
|
|
22
|
+
def configuration
|
|
23
|
+
@configuration ||= Configuration.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configure
|
|
27
|
+
yield(configuration)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def registry
|
|
31
|
+
@registry ||= Registry.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def loader
|
|
35
|
+
@loader ||= Loader.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def enabled?
|
|
39
|
+
configuration.enabled
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def logger
|
|
43
|
+
@logger ||= if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
44
|
+
Rails.logger
|
|
45
|
+
else
|
|
46
|
+
Logger.new($stdout)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attr_writer :logger
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module ActiveSubscriber
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
9
|
+
|
|
10
|
+
desc 'Install ActiveSubscriber in your Rails application'
|
|
11
|
+
|
|
12
|
+
def create_initializer
|
|
13
|
+
template 'initializer.rb', 'config/initializers/active_subscriber.rb'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_subscribers_directory
|
|
17
|
+
empty_directory 'app/subscribers'
|
|
18
|
+
create_file 'app/subscribers/.keep', ''
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show_readme
|
|
22
|
+
readme 'README' if behavior == :invoke
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def readme(path)
|
|
28
|
+
say File.read(File.join(File.dirname(__FILE__), 'templates', path))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module ActiveSubscriber
|
|
6
|
+
module Generators
|
|
7
|
+
class SubscriberGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
9
|
+
|
|
10
|
+
desc 'Generate a new ActiveSubscriber subscriber'
|
|
11
|
+
|
|
12
|
+
class_option :events, type: :array, default: [], desc: 'Events to subscribe to'
|
|
13
|
+
class_option :patterns, type: :array, default: [], desc: 'Event patterns to subscribe to'
|
|
14
|
+
|
|
15
|
+
def create_subscriber_file
|
|
16
|
+
template 'subscriber.rb', File.join('app/subscribers', class_path, "#{file_name}_subscriber.rb")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def events_list
|
|
22
|
+
options[:events].map { |event| ":#{event}" }.join(', ')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def patterns_list
|
|
26
|
+
options[:patterns].map { |pattern| "\"#{pattern}\"" }.join(', ')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def has_events?
|
|
30
|
+
options[:events].any?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def has_patterns?
|
|
34
|
+
options[:patterns].any?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handler_methods
|
|
38
|
+
options[:events].map do |event|
|
|
39
|
+
method_name = "handle_#{event.gsub('.', '_').gsub('-', '_')}"
|
|
40
|
+
<<~METHOD
|
|
41
|
+
def #{method_name}(data)
|
|
42
|
+
# Handle #{event} event
|
|
43
|
+
Rails.logger.info "#{class_name}Subscriber: Handling #{event} event"
|
|
44
|
+
end
|
|
45
|
+
METHOD
|
|
46
|
+
end.join("\n")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
ActiveSubscriber has been installed!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Configure ActiveSubscriber in config/initializers/active_subscriber.rb
|
|
8
|
+
2. Create your first subscriber:
|
|
9
|
+
|
|
10
|
+
rails generate active_subscriber:subscriber Analytics --events user_signed_in user_signed_up
|
|
11
|
+
|
|
12
|
+
3. Include the Publisher module in your services:
|
|
13
|
+
|
|
14
|
+
class AnalyticsService
|
|
15
|
+
include ActiveSubscriber::Publisher
|
|
16
|
+
|
|
17
|
+
def self.track_event(name, data = {})
|
|
18
|
+
publish_event(name, data)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
4. Include the AnalyticsHelper in your controllers:
|
|
23
|
+
|
|
24
|
+
class ApplicationController < ActionController::Base
|
|
25
|
+
include ActiveSubscriber::Helpers::AnalyticsHelper
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
5. Start publishing events and let your subscribers handle them!
|
|
29
|
+
|
|
30
|
+
For more information, visit: https://github.com/afomera/active_subscriber
|
|
31
|
+
|
|
32
|
+
===============================================================================
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ActiveSubscriber.configure do |config|
|
|
4
|
+
# Enable or disable ActiveSubscriber
|
|
5
|
+
# config.enabled = true
|
|
6
|
+
|
|
7
|
+
# Namespace for all events (recommended to avoid conflicts)
|
|
8
|
+
# config.namespace = "my_app"
|
|
9
|
+
|
|
10
|
+
# Paths where subscribers are located
|
|
11
|
+
# config.subscriber_paths = ["app/subscribers"]
|
|
12
|
+
|
|
13
|
+
# Filter events (optional)
|
|
14
|
+
# config.event_filter = ->(event_name) { !event_name.start_with?("internal.") }
|
|
15
|
+
|
|
16
|
+
# Auto-load subscribers
|
|
17
|
+
# config.auto_load = true
|
|
18
|
+
|
|
19
|
+
# Process events asynchronously using background jobs (defaults to Rails.env.production?)
|
|
20
|
+
# config.async = Rails.env.production?
|
|
21
|
+
end
|