behavior_analytics 0.1.0 → 2.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +146 -5
  3. data/behavior_analytics.gemspec +3 -1
  4. data/db/migrate/002_enhance_behavior_events_v2.rb +46 -0
  5. data/lib/behavior_analytics/analytics/cohorts.rb +242 -0
  6. data/lib/behavior_analytics/analytics/engine.rb +15 -0
  7. data/lib/behavior_analytics/analytics/funnels.rb +176 -0
  8. data/lib/behavior_analytics/analytics/retention.rb +186 -0
  9. data/lib/behavior_analytics/context.rb +38 -2
  10. data/lib/behavior_analytics/debug/inspector.rb +82 -0
  11. data/lib/behavior_analytics/event.rb +7 -1
  12. data/lib/behavior_analytics/export/csv_exporter.rb +102 -0
  13. data/lib/behavior_analytics/export/json_exporter.rb +55 -0
  14. data/lib/behavior_analytics/hooks/callback.rb +50 -0
  15. data/lib/behavior_analytics/hooks/manager.rb +106 -0
  16. data/lib/behavior_analytics/hooks/webhook.rb +114 -0
  17. data/lib/behavior_analytics/integrations/rails/middleware.rb +99 -0
  18. data/lib/behavior_analytics/integrations/rails.rb +123 -2
  19. data/lib/behavior_analytics/jobs/active_event_job.rb +37 -0
  20. data/lib/behavior_analytics/jobs/delayed_event_job.rb +29 -0
  21. data/lib/behavior_analytics/jobs/sidekiq_event_job.rb +37 -0
  22. data/lib/behavior_analytics/observability/metrics.rb +112 -0
  23. data/lib/behavior_analytics/observability/tracer.rb +85 -0
  24. data/lib/behavior_analytics/processors/async_processor.rb +24 -0
  25. data/lib/behavior_analytics/processors/background_job_processor.rb +72 -0
  26. data/lib/behavior_analytics/query.rb +89 -4
  27. data/lib/behavior_analytics/replay/engine.rb +108 -0
  28. data/lib/behavior_analytics/replay/processor.rb +107 -0
  29. data/lib/behavior_analytics/reporting/generator.rb +125 -0
  30. data/lib/behavior_analytics/sampling/strategy.rb +54 -0
  31. data/lib/behavior_analytics/schema/definition.rb +71 -0
  32. data/lib/behavior_analytics/schema/validator.rb +113 -0
  33. data/lib/behavior_analytics/storage/active_record_adapter.rb +183 -10
  34. data/lib/behavior_analytics/storage/elasticsearch_adapter.rb +185 -0
  35. data/lib/behavior_analytics/storage/in_memory_adapter.rb +234 -5
  36. data/lib/behavior_analytics/storage/kafka_adapter.rb +127 -0
  37. data/lib/behavior_analytics/storage/redis_adapter.rb +211 -0
  38. data/lib/behavior_analytics/streaming/event_stream.rb +77 -0
  39. data/lib/behavior_analytics/throttling/limiter.rb +97 -0
  40. data/lib/behavior_analytics/tracker.rb +130 -4
  41. data/lib/behavior_analytics/version.rb +1 -1
  42. data/lib/behavior_analytics.rb +139 -2
  43. metadata +33 -3
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module BehaviorAnalytics
8
+ module Hooks
9
+ class Webhook
10
+ attr_reader :url, :secret, :filter, :retry_count, :timeout
11
+
12
+ def initialize(url:, secret: nil, filter: nil, retry_count: 3, timeout: 5)
13
+ @url = URI(url)
14
+ @secret = secret
15
+ @filter = filter
16
+ @retry_count = retry_count
17
+ @timeout = timeout
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ def deliver(event, context = nil)
22
+ return unless should_deliver?(event)
23
+
24
+ payload = build_payload(event, context)
25
+ signature = generate_signature(payload) if @secret
26
+
27
+ headers = {
28
+ "Content-Type" => "application/json",
29
+ "User-Agent" => "BehaviorAnalytics/2.0"
30
+ }
31
+ headers["X-Webhook-Signature"] = signature if signature
32
+
33
+ deliver_with_retry(payload, headers)
34
+ end
35
+
36
+ private
37
+
38
+ def should_deliver?(event)
39
+ return true unless @filter
40
+
41
+ case @filter
42
+ when Proc
43
+ @filter.call(event)
44
+ when Hash
45
+ @filter.all? { |key, value| matches?(event, key, value) }
46
+ when Symbol, String
47
+ event[:event_type] == @filter || event[:event_type].to_s == @filter.to_s
48
+ else
49
+ true
50
+ end
51
+ end
52
+
53
+ def matches?(event, key, value)
54
+ event_value = event[key.to_sym] || event[key.to_s] || get_metadata_value(event, key.to_s)
55
+ event_value == value || event_value.to_s == value.to_s
56
+ end
57
+
58
+ def get_metadata_value(event, key)
59
+ metadata = event[:metadata] || event["metadata"] || {}
60
+ metadata[key.to_sym] || metadata[key.to_s] || metadata[key]
61
+ end
62
+
63
+ def build_payload(event, context)
64
+ {
65
+ event: event.is_a?(Hash) ? event : event.to_h,
66
+ context: context ? (context.is_a?(Hash) ? context : context.to_h) : nil,
67
+ timestamp: Time.now.iso8601
68
+ }
69
+ end
70
+
71
+ def generate_signature(payload)
72
+ require "openssl" unless defined?(OpenSSL)
73
+ payload_json = JSON.generate(payload)
74
+ OpenSSL::HMAC.hexdigest("SHA256", @secret, payload_json)
75
+ end
76
+
77
+ def deliver_with_retry(payload, headers)
78
+ last_error = nil
79
+
80
+ (@retry_count + 1).times do |attempt|
81
+ begin
82
+ http = Net::HTTP.new(@url.host, @url.port)
83
+ http.use_ssl = @url.scheme == "https"
84
+ http.read_timeout = @timeout
85
+ http.open_timeout = @timeout
86
+
87
+ request = Net::HTTP::Post.new(@url.path)
88
+ headers.each { |key, value| request[key] = value }
89
+ request.body = JSON.generate(payload)
90
+
91
+ response = http.request(request)
92
+
93
+ if response.code.to_i >= 200 && response.code.to_i < 300
94
+ return { success: true, response_code: response.code.to_i }
95
+ else
96
+ last_error = "HTTP #{response.code}: #{response.message}"
97
+ end
98
+ rescue StandardError => e
99
+ last_error = e.message
100
+ sleep(calculate_backoff(attempt)) if attempt < @retry_count
101
+ end
102
+ end
103
+
104
+ { success: false, error: last_error }
105
+ end
106
+
107
+ def calculate_backoff(attempt)
108
+ # Exponential backoff: 1s, 2s, 4s, etc.
109
+ 2 ** attempt
110
+ end
111
+ end
112
+ end
113
+ end
114
+
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Integrations
5
+ module Rails
6
+ class Middleware
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ start_time = Time.now
13
+ status, headers, response = @app.call(env)
14
+
15
+ # Track request if enabled
16
+ if should_track_request?(env)
17
+ track_request(env, status, start_time)
18
+ end
19
+
20
+ [status, headers, response]
21
+ end
22
+
23
+ private
24
+
25
+ def should_track_request?(env)
26
+ return false unless BehaviorAnalytics.configuration.storage_adapter
27
+ return false unless BehaviorAnalytics.configuration.track_middleware_requests
28
+
29
+ path = env["PATH_INFO"]
30
+ return false if path_blacklisted?(path)
31
+ return false if path_not_whitelisted?(path)
32
+
33
+ true
34
+ end
35
+
36
+ def path_blacklisted?(path)
37
+ blacklist = BehaviorAnalytics.configuration.tracking_blacklist || []
38
+ return false if blacklist.empty?
39
+
40
+ blacklist.any? { |pattern| matches_pattern?(path, pattern) }
41
+ end
42
+
43
+ def path_not_whitelisted?(path)
44
+ whitelist = BehaviorAnalytics.configuration.tracking_whitelist
45
+ return false unless whitelist && !whitelist.empty?
46
+
47
+ !whitelist.any? { |pattern| matches_pattern?(path, pattern) }
48
+ end
49
+
50
+ def matches_pattern?(path, pattern)
51
+ case pattern
52
+ when Regexp
53
+ pattern.match?(path)
54
+ when String
55
+ path.include?(pattern) || File.fnmatch?(pattern, path)
56
+ else
57
+ false
58
+ end
59
+ end
60
+
61
+ def track_request(env, status, start_time)
62
+ duration_ms = ((Time.now - start_time) * 1000).to_i
63
+
64
+ # Try to extract context from env
65
+ context = extract_context_from_env(env)
66
+ return unless context&.valid?
67
+
68
+ tracker = BehaviorAnalytics.create_tracker
69
+ tracker.track_api_call(
70
+ context: context,
71
+ method: env["REQUEST_METHOD"],
72
+ path: env["PATH_INFO"],
73
+ status_code: status,
74
+ duration_ms: duration_ms,
75
+ ip: env["REMOTE_ADDR"],
76
+ user_agent: env["HTTP_USER_AGENT"]
77
+ )
78
+ rescue StandardError => e
79
+ # Don't let tracking errors break the request
80
+ if defined?(Rails) && Rails.logger
81
+ Rails.logger.error("BehaviorAnalytics: Middleware tracking error: #{e.message}")
82
+ end
83
+ end
84
+
85
+ def extract_context_from_env(env)
86
+ # Try to get context from request store or session
87
+ if defined?(ActionDispatch::Request)
88
+ request = ActionDispatch::Request.new(env)
89
+ # This would need to be customized based on your app's context resolution
90
+ nil
91
+ else
92
+ nil
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
@@ -70,21 +70,142 @@ module BehaviorAnalytics
70
70
  user_id: current_user&.id,
71
71
  user_type: current_user&.account_type || current_user&.user_type
72
72
  )
73
+ elsif respond_to?(:current_user, true)
74
+ # Single-tenant system - use default tenant
75
+ Context.new(
76
+ tenant_id: BehaviorAnalytics.configuration.default_tenant_id,
77
+ user_id: current_user&.id,
78
+ user_type: current_user&.account_type || current_user&.user_type
79
+ )
73
80
  else
74
- nil
81
+ # No user context - use default tenant
82
+ Context.new(
83
+ tenant_id: BehaviorAnalytics.configuration.default_tenant_id
84
+ )
75
85
  end
76
86
  end
77
87
 
78
88
  def should_track?
79
89
  context = resolve_tracking_context
80
- return false unless context&.valid?
90
+ # Allow tracking even without context for API-only tracking
91
+ # Context validation will handle required fields
92
+ return false if context && !context.valid?
93
+
94
+ # Check path whitelist/blacklist
95
+ return false if path_blacklisted?
96
+ return false if path_not_whitelisted?
97
+
98
+ # Check user agent filtering
99
+ return false if bot_user_agent?
100
+
101
+ # Check controller/action filtering
102
+ return false if controller_action_filtered?
81
103
 
82
104
  true
83
105
  end
84
106
 
107
+ def path_blacklisted?
108
+ blacklist = BehaviorAnalytics.configuration.tracking_blacklist || []
109
+ return false if blacklist.empty?
110
+
111
+ blacklist.any? { |pattern| matches_pattern?(request.path, pattern) }
112
+ end
113
+
114
+ def path_not_whitelisted?
115
+ whitelist = BehaviorAnalytics.configuration.tracking_whitelist
116
+ return false unless whitelist && !whitelist.empty?
117
+
118
+ !whitelist.any? { |pattern| matches_pattern?(request.path, pattern) }
119
+ end
120
+
121
+ def bot_user_agent?
122
+ return false unless BehaviorAnalytics.configuration.skip_bots
123
+
124
+ user_agent = request.user_agent.to_s.downcase
125
+ bot_patterns = %w[bot crawler spider crawlerbot googlebot bingbot yandex]
126
+ bot_patterns.any? { |pattern| user_agent.include?(pattern) }
127
+ end
128
+
129
+ def controller_action_filtered?
130
+ filters = BehaviorAnalytics.configuration.controller_action_filters || {}
131
+ return false if filters.empty?
132
+
133
+ controller_filter = filters[:controllers] || []
134
+ action_filter = filters[:actions] || []
135
+
136
+ return true if controller_filter.include?(controller_name)
137
+ return true if action_filter.include?("#{controller_name}##{action_name}")
138
+
139
+ false
140
+ end
141
+
142
+ def matches_pattern?(path, pattern)
143
+ case pattern
144
+ when Regexp
145
+ pattern.match?(path)
146
+ when String
147
+ path.include?(pattern) || File.fnmatch?(pattern, path)
148
+ else
149
+ false
150
+ end
151
+ end
152
+
85
153
  def behavior_analytics_enabled?
86
154
  BehaviorAnalytics.configuration.storage_adapter.present?
87
155
  end
156
+
157
+ def track_behavior_analytics
158
+ start_time = Time.current
159
+ error_occurred = false
160
+ error_message = nil
161
+
162
+ yield
163
+ rescue StandardError => e
164
+ error_occurred = true
165
+ error_message = e.message
166
+ raise
167
+ ensure
168
+ if should_track?
169
+ context = resolve_tracking_context
170
+ # Create context if it doesn't exist (for API-only tracking)
171
+ context ||= Context.new(tenant_id: BehaviorAnalytics.configuration.default_tenant_id)
172
+
173
+ if context&.valid?
174
+ duration_ms = ((Time.current - start_time) * 1000).to_i
175
+
176
+ # Check for slow queries
177
+ if BehaviorAnalytics.configuration.slow_query_threshold
178
+ if duration_ms > BehaviorAnalytics.configuration.slow_query_threshold
179
+ log_slow_query(duration_ms, request.path)
180
+ end
181
+ end
182
+
183
+ behavior_tracker.track_api_call(
184
+ context: context,
185
+ method: request.method,
186
+ path: request.path,
187
+ status_code: response.status,
188
+ duration_ms: duration_ms,
189
+ ip: request.remote_ip,
190
+ user_agent: request.user_agent,
191
+ session_id: session.id,
192
+ metadata: {
193
+ controller: controller_name,
194
+ action: action_name,
195
+ format: request.format.to_s,
196
+ error: error_occurred,
197
+ error_message: error_message
198
+ }.compact
199
+ )
200
+ end
201
+ end
202
+ end
203
+
204
+ def log_slow_query(duration_ms, path)
205
+ if defined?(Rails) && Rails.logger
206
+ Rails.logger.warn("BehaviorAnalytics: Slow query detected: #{path} took #{duration_ms}ms")
207
+ end
208
+ end
88
209
  end
89
210
  end
90
211
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "active_job"
5
+ rescue LoadError
6
+ raise LoadError, "ActiveJob is required for ActiveEventJob. Please add 'activejob' to your Gemfile."
7
+ end
8
+
9
+ module BehaviorAnalytics
10
+ module Jobs
11
+ class ActiveEventJob < ActiveJob::Base
12
+ queue_as :default
13
+
14
+ retry_on StandardError, wait: :exponentially_longer, attempts: 3
15
+
16
+ def perform(events_data, storage_adapter_class = nil)
17
+ storage_adapter = resolve_storage_adapter(storage_adapter_class)
18
+ events = events_data.map { |data| Event.new(data) }
19
+ storage_adapter.save_events(events)
20
+ rescue StandardError => e
21
+ Rails.logger.error("BehaviorAnalytics: Failed to process events: #{e.message}") if defined?(Rails)
22
+ raise
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_storage_adapter(storage_adapter_class)
28
+ if storage_adapter_class
29
+ storage_adapter_class.constantize.new
30
+ else
31
+ BehaviorAnalytics.configuration.storage_adapter
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "delayed_job"
5
+ rescue LoadError
6
+ raise LoadError, "DelayedJob is required for DelayedEventJob. Please add 'delayed_job' to your Gemfile."
7
+ end
8
+
9
+ module BehaviorAnalytics
10
+ module Jobs
11
+ class DelayedEventJob
12
+ attr_reader :events_data, :storage_adapter
13
+
14
+ def initialize(events_data, storage_adapter = nil)
15
+ @events_data = events_data
16
+ @storage_adapter = storage_adapter || BehaviorAnalytics.configuration.storage_adapter
17
+ end
18
+
19
+ def perform
20
+ events = @events_data.map { |data| Event.new(data) }
21
+ @storage_adapter.save_events(events)
22
+ rescue StandardError => e
23
+ Delayed::Worker.logger.error("BehaviorAnalytics: Failed to process events: #{e.message}") if defined?(Delayed::Worker)
24
+ raise
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "sidekiq"
5
+ rescue LoadError
6
+ raise LoadError, "Sidekiq is required for SidekiqEventJob. Please add 'sidekiq' to your Gemfile."
7
+ end
8
+
9
+ module BehaviorAnalytics
10
+ module Jobs
11
+ class SidekiqEventJob
12
+ include Sidekiq::Job
13
+
14
+ sidekiq_options retry: 3, backtrace: true
15
+
16
+ def perform(events_data, storage_adapter_class = nil)
17
+ storage_adapter = resolve_storage_adapter(storage_adapter_class)
18
+ events = events_data.map { |data| Event.new(data) }
19
+ storage_adapter.save_events(events)
20
+ rescue StandardError => e
21
+ Sidekiq.logger.error("BehaviorAnalytics: Failed to process events: #{e.message}")
22
+ raise
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_storage_adapter(storage_adapter_class)
28
+ if storage_adapter_class
29
+ storage_adapter_class.constantize.new
30
+ else
31
+ BehaviorAnalytics.configuration.storage_adapter
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Observability
5
+ class Metrics
6
+ def initialize
7
+ @counters = {}
8
+ @gauges = {}
9
+ @histograms = {}
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def increment_counter(name, value: 1, tags: {})
14
+ @mutex.synchronize do
15
+ key = metric_key(name, tags)
16
+ @counters[key] ||= 0
17
+ @counters[key] += value
18
+ end
19
+ end
20
+
21
+ def set_gauge(name, value, tags: {})
22
+ @mutex.synchronize do
23
+ key = metric_key(name, tags)
24
+ @gauges[key] = value
25
+ end
26
+ end
27
+
28
+ def record_histogram(name, value, tags: {})
29
+ @mutex.synchronize do
30
+ key = metric_key(name, tags)
31
+ @histograms[key] ||= []
32
+ @histograms[key] << value
33
+ # Keep only last 1000 values
34
+ @histograms[key] = @histograms[key].last(1000) if @histograms[key].size > 1000
35
+ end
36
+ end
37
+
38
+ def get_counter(name, tags: {})
39
+ key = metric_key(name, tags)
40
+ @counters[key] || 0
41
+ end
42
+
43
+ def get_gauge(name, tags: {})
44
+ key = metric_key(name, tags)
45
+ @gauges[key]
46
+ end
47
+
48
+ def get_histogram_stats(name, tags: {})
49
+ key = metric_key(name, tags)
50
+ values = @histograms[key] || []
51
+ return {} if values.empty?
52
+
53
+ sorted = values.sort
54
+ {
55
+ count: values.size,
56
+ min: sorted.first,
57
+ max: sorted.last,
58
+ sum: values.sum,
59
+ avg: values.sum.to_f / values.size,
60
+ p50: percentile(sorted, 50),
61
+ p95: percentile(sorted, 95),
62
+ p99: percentile(sorted, 99)
63
+ }
64
+ end
65
+
66
+ def all_metrics
67
+ {
68
+ counters: @counters.dup,
69
+ gauges: @gauges.dup,
70
+ histograms: @histograms.keys.map { |k| [k, get_histogram_stats(*parse_key(k))] }.to_h
71
+ }
72
+ end
73
+
74
+ def reset
75
+ @mutex.synchronize do
76
+ @counters.clear
77
+ @gauges.clear
78
+ @histograms.clear
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def metric_key(name, tags)
85
+ if tags.empty?
86
+ name.to_s
87
+ else
88
+ tag_str = tags.map { |k, v| "#{k}:#{v}" }.join(",")
89
+ "#{name}[#{tag_str}]"
90
+ end
91
+ end
92
+
93
+ def parse_key(key)
94
+ if key.include?("[")
95
+ name, tag_str = key.split("[", 2)
96
+ tag_str = tag_str.chomp("]")
97
+ tags = tag_str.split(",").map { |t| t.split(":") }.to_h
98
+ [name, tags]
99
+ else
100
+ [key, {}]
101
+ end
102
+ end
103
+
104
+ def percentile(sorted_array, percentile)
105
+ return nil if sorted_array.empty?
106
+ index = (percentile / 100.0) * (sorted_array.size - 1)
107
+ sorted_array[index.floor]
108
+ end
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module BehaviorAnalytics
6
+ module Observability
7
+ class Tracer
8
+ attr_reader :correlation_id
9
+
10
+ def initialize(correlation_id: nil)
11
+ @correlation_id = correlation_id || generate_correlation_id
12
+ @spans = []
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def start_span(name, tags: {})
17
+ span = {
18
+ id: SecureRandom.uuid,
19
+ name: name,
20
+ start_time: Time.now,
21
+ tags: tags,
22
+ correlation_id: @correlation_id
23
+ }
24
+
25
+ @mutex.synchronize do
26
+ @spans << span
27
+ end
28
+
29
+ span
30
+ end
31
+
32
+ def finish_span(span_id, tags: {})
33
+ @mutex.synchronize do
34
+ span = @spans.find { |s| s[:id] == span_id }
35
+ return unless span
36
+
37
+ span[:end_time] = Time.now
38
+ span[:duration_ms] = ((span[:end_time] - span[:start_time]) * 1000).to_i
39
+ span[:tags].merge!(tags)
40
+ end
41
+ end
42
+
43
+ def add_tags_to_span(span_id, tags)
44
+ @mutex.synchronize do
45
+ span = @spans.find { |s| s[:id] == span_id }
46
+ return unless span
47
+
48
+ span[:tags].merge!(tags)
49
+ end
50
+ end
51
+
52
+ def get_spans
53
+ @spans.dup
54
+ end
55
+
56
+ def get_trace
57
+ {
58
+ correlation_id: @correlation_id,
59
+ spans: @spans,
60
+ total_duration_ms: calculate_total_duration
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def generate_correlation_id
67
+ "#{Time.now.to_i}-#{SecureRandom.hex(8)}"
68
+ end
69
+
70
+ def calculate_total_duration
71
+ return 0 if @spans.empty?
72
+
73
+ start_times = @spans.map { |s| s[:start_time] }.compact
74
+ end_times = @spans.map { |s| s[:end_time] || Time.now }.compact
75
+
76
+ return 0 if start_times.empty? || end_times.empty?
77
+
78
+ earliest_start = start_times.min
79
+ latest_end = end_times.max
80
+ ((latest_end - earliest_start) * 1000).to_i
81
+ end
82
+ end
83
+ end
84
+ end
85
+
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Processors
5
+ class AsyncProcessor
6
+ attr_reader :storage_adapter, :queue_name, :priority
7
+
8
+ def initialize(storage_adapter:, queue_name: "default", priority: 0)
9
+ @storage_adapter = storage_adapter
10
+ @queue_name = queue_name
11
+ @priority = priority
12
+ end
13
+
14
+ def process_async(events)
15
+ raise NotImplementedError, "#{self.class} must implement #process_async"
16
+ end
17
+
18
+ def process_sync(events)
19
+ @storage_adapter.save_events(events)
20
+ end
21
+ end
22
+ end
23
+ end
24
+