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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Throttling
5
+ class Limiter
6
+ def initialize(options = {})
7
+ @per_tenant_limits = options[:per_tenant] || {}
8
+ @per_user_limits = options[:per_user] || {}
9
+ @global_limit = options[:global]
10
+ @window_size = options[:window_size] || 60 # seconds
11
+ @counters = {}
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def check_limit(context, event = nil)
16
+ return { allowed: true } unless should_check_limits?
17
+
18
+ @mutex.synchronize do
19
+ # Check global limit
20
+ if @global_limit && !check_global_limit
21
+ return { allowed: false, reason: "global_limit_exceeded" }
22
+ end
23
+
24
+ # Check per-tenant limit
25
+ if context.tenant_id && @per_tenant_limits[context.tenant_id]
26
+ limit = @per_tenant_limits[context.tenant_id]
27
+ if !check_limit_for_key("tenant:#{context.tenant_id}", limit)
28
+ return { allowed: false, reason: "tenant_limit_exceeded", tenant_id: context.tenant_id }
29
+ end
30
+ end
31
+
32
+ # Check per-user limit
33
+ if context.user_id && @per_user_limits[context.user_id]
34
+ limit = @per_user_limits[context.user_id]
35
+ if !check_limit_for_key("user:#{context.user_id}", limit)
36
+ return { allowed: false, reason: "user_limit_exceeded", user_id: context.user_id }
37
+ end
38
+ end
39
+
40
+ { allowed: true }
41
+ end
42
+ end
43
+
44
+ def record_event(context)
45
+ return unless should_check_limits?
46
+
47
+ @mutex.synchronize do
48
+ increment_counter("global") if @global_limit
49
+ increment_counter("tenant:#{context.tenant_id}") if context.tenant_id && @per_tenant_limits[context.tenant_id]
50
+ increment_counter("user:#{context.user_id}") if context.user_id && @per_user_limits[context.user_id]
51
+ end
52
+ end
53
+
54
+ def reset_counters
55
+ @mutex.synchronize do
56
+ @counters.clear
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def should_check_limits?
63
+ @global_limit || !@per_tenant_limits.empty? || !@per_user_limits.empty?
64
+ end
65
+
66
+ def check_global_limit
67
+ check_limit_for_key("global", @global_limit)
68
+ end
69
+
70
+ def check_limit_for_key(key, limit)
71
+ counter = get_counter(key)
72
+ counter < limit
73
+ end
74
+
75
+ def get_counter(key)
76
+ counter = @counters[key]
77
+
78
+ # Reset counter if window expired
79
+ if counter && counter[:expires_at] < Time.now
80
+ @counters.delete(key)
81
+ counter = nil
82
+ end
83
+
84
+ counter ||= { count: 0, expires_at: Time.now + @window_size }
85
+ @counters[key] = counter
86
+
87
+ counter[:count]
88
+ end
89
+
90
+ def increment_counter(key)
91
+ counter = get_counter(key)
92
+ @counters[key][:count] += 1
93
+ end
94
+ end
95
+ end
96
+ end
97
+
@@ -5,13 +5,22 @@ require "thread"
5
5
 
6
6
  module BehaviorAnalytics
7
7
  class Tracker
8
- attr_reader :storage_adapter, :batch_size, :flush_interval, :context_resolver
8
+ attr_reader :storage_adapter, :batch_size, :flush_interval, :context_resolver, :async_processor, :event_stream
9
9
 
10
10
  def initialize(options = {})
11
11
  @storage_adapter = options[:storage_adapter] || BehaviorAnalytics.configuration.storage_adapter || Storage::InMemoryAdapter.new
12
12
  @batch_size = options[:batch_size] || BehaviorAnalytics.configuration.batch_size
13
13
  @flush_interval = options[:flush_interval] || BehaviorAnalytics.configuration.flush_interval
14
14
  @context_resolver = options[:context_resolver] || BehaviorAnalytics.configuration.context_resolver
15
+ @async_processor = options[:async_processor] || BehaviorAnalytics.configuration.async_processor
16
+ @use_async = options.fetch(:use_async, BehaviorAnalytics.configuration.use_async || false)
17
+ @event_stream = options[:event_stream] || BehaviorAnalytics.configuration.event_stream || Streaming::EventStream.new
18
+ @hooks_manager = options[:hooks_manager] || BehaviorAnalytics.configuration.hooks_manager || Hooks::Manager.new
19
+ @sampling_strategy = options[:sampling_strategy] || BehaviorAnalytics.configuration.sampling_strategy
20
+ @rate_limiter = options[:rate_limiter] || BehaviorAnalytics.configuration.rate_limiter
21
+ @schema_validator = options[:schema_validator] || BehaviorAnalytics.configuration.schema_validator
22
+ @metrics = options[:metrics] || BehaviorAnalytics.configuration.metrics || Observability::Metrics.new
23
+ @tracer = options[:tracer] || BehaviorAnalytics.configuration.tracer
15
24
 
16
25
  @buffer = []
17
26
  @mutex = Mutex.new
@@ -23,7 +32,50 @@ module BehaviorAnalytics
23
32
  context = normalize_context(context)
24
33
  context.validate!
25
34
 
26
- event = Event.new(
35
+ # Check rate limiting
36
+ if @rate_limiter
37
+ limit_check = @rate_limiter.check_limit(context)
38
+ unless limit_check[:allowed]
39
+ raise Error, "Rate limit exceeded: #{limit_check[:reason]}"
40
+ end
41
+ end
42
+
43
+ # Check sampling
44
+ if @sampling_strategy
45
+ event_data = {
46
+ tenant_id: context.tenant_id,
47
+ user_id: context.user_id,
48
+ event_name: event_name,
49
+ event_type: event_type
50
+ }
51
+ unless @sampling_strategy.should_sample?(event_data, context)
52
+ return # Skip this event
53
+ end
54
+ end
55
+
56
+ # Build event data
57
+ event_data = {
58
+ tenant_id: context.tenant_id,
59
+ user_id: context.user_id,
60
+ user_type: context.user_type,
61
+ event_name: event_name,
62
+ event_type: event_type,
63
+ metadata: metadata.merge(context.filters),
64
+ session_id: options[:session_id],
65
+ ip: options[:ip],
66
+ user_agent: options[:user_agent],
67
+ duration_ms: options[:duration_ms]
68
+ }
69
+
70
+ # Validate schema if validator is configured
71
+ if @schema_validator
72
+ validation_result = @schema_validator.validate(event_data)
73
+ unless validation_result[:valid]
74
+ raise Error, "Event validation failed: #{validation_result[:errors].join(', ')}"
75
+ end
76
+ end
77
+
78
+ event = Event.new(event_data)
27
79
  tenant_id: context.tenant_id,
28
80
  user_id: context.user_id,
29
81
  user_type: context.user_type,
@@ -36,7 +88,56 @@ module BehaviorAnalytics
36
88
  duration_ms: options[:duration_ms]
37
89
  )
38
90
 
39
- add_to_buffer(event)
91
+ # Execute before_track hooks
92
+ begin
93
+ @hooks_manager.execute_before_track(event.to_h, context.to_h)
94
+ rescue StandardError => e
95
+ @hooks_manager.execute_on_error(e, event.to_h, context.to_h)
96
+ raise if BehaviorAnalytics.configuration.raise_on_hook_error
97
+ end
98
+
99
+ begin
100
+ # Start tracing if enabled
101
+ span = @tracer&.start_span("track_event", tags: {
102
+ event_name: event_name,
103
+ event_type: event_type.to_s,
104
+ tenant_id: context.tenant_id
105
+ })
106
+
107
+ add_to_buffer(event)
108
+
109
+ # Record metrics
110
+ @metrics.increment_counter("events.tracked", tags: {
111
+ event_type: event_type.to_s,
112
+ tenant_id: context.tenant_id.to_s
113
+ })
114
+
115
+ # Debug logging
116
+ if BehaviorAnalytics.configuration.debug_mode
117
+ BehaviorAnalytics.configuration.debug("Event tracked: #{event_name}", context: context.to_h)
118
+ end
119
+
120
+ # Publish to event stream
121
+ @event_stream.publish(event.to_h) if @event_stream
122
+
123
+ # Execute after_track hooks
124
+ @hooks_manager.execute_after_track(event.to_h, context.to_h)
125
+
126
+ # Record event for rate limiting
127
+ @rate_limiter.record_event(context) if @rate_limiter
128
+
129
+ # Finish tracing
130
+ @tracer&.finish_span(span[:id]) if span
131
+ rescue StandardError => e
132
+ # Record error metrics
133
+ @metrics.increment_counter("events.errors", tags: {
134
+ event_type: event_type.to_s,
135
+ tenant_id: context.tenant_id.to_s
136
+ })
137
+
138
+ @hooks_manager.execute_on_error(e, event.to_h, context.to_h)
139
+ raise
140
+ end
40
141
  end
41
142
 
42
143
  def track_api_call(context:, method:, path:, status_code:, duration_ms: nil, **options)
@@ -76,7 +177,24 @@ module BehaviorAnalytics
76
177
 
77
178
  return if events_to_flush.empty?
78
179
 
79
- @storage_adapter.save_events(events_to_flush)
180
+ start_time = Time.now
181
+
182
+ begin
183
+ if @use_async && @async_processor
184
+ @async_processor.process_async(events_to_flush)
185
+ else
186
+ @storage_adapter.save_events(events_to_flush)
187
+ end
188
+
189
+ # Record flush metrics
190
+ duration_ms = ((Time.now - start_time) * 1000).to_i
191
+ @metrics.record_histogram("flush.duration_ms", duration_ms)
192
+ @metrics.increment_counter("flush.count", value: events_to_flush.size)
193
+ rescue StandardError => e
194
+ @metrics.increment_counter("flush.errors")
195
+ raise
196
+ end
197
+
80
198
  restart_flush_timer
81
199
  end
82
200
 
@@ -88,6 +206,14 @@ module BehaviorAnalytics
88
206
  Query.new(@storage_adapter)
89
207
  end
90
208
 
209
+ def subscribe_to_stream(filter: nil, &block)
210
+ @event_stream.subscribe(filter: filter, &block)
211
+ end
212
+
213
+ def inspector
214
+ @inspector ||= Debug::Inspector.new(self)
215
+ end
216
+
91
217
  private
92
218
 
93
219
  def normalize_context(context)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BehaviorAnalytics
4
- VERSION = "0.1.0"
4
+ VERSION = "2.1.0"
5
5
  end
@@ -8,28 +8,165 @@ require_relative "behavior_analytics/query"
8
8
  require_relative "behavior_analytics/storage/adapter"
9
9
  require_relative "behavior_analytics/storage/in_memory_adapter"
10
10
  require_relative "behavior_analytics/storage/active_record_adapter"
11
+
12
+ begin
13
+ require_relative "behavior_analytics/storage/redis_adapter"
14
+ rescue LoadError
15
+ end
16
+
17
+ begin
18
+ require_relative "behavior_analytics/storage/elasticsearch_adapter"
19
+ rescue LoadError
20
+ end
21
+
22
+ begin
23
+ require_relative "behavior_analytics/storage/kafka_adapter"
24
+ rescue LoadError
25
+ end
11
26
  require_relative "behavior_analytics/analytics/engine"
27
+ require_relative "behavior_analytics/analytics/funnels"
28
+ require_relative "behavior_analytics/analytics/cohorts"
29
+ require_relative "behavior_analytics/analytics/retention"
30
+ require_relative "behavior_analytics/hooks/manager"
31
+ require_relative "behavior_analytics/hooks/webhook"
32
+ require_relative "behavior_analytics/hooks/callback"
33
+ require_relative "behavior_analytics/replay/engine"
34
+ require_relative "behavior_analytics/replay/processor"
35
+ require_relative "behavior_analytics/sampling/strategy"
36
+ require_relative "behavior_analytics/throttling/limiter"
37
+ require_relative "behavior_analytics/schema/validator"
38
+ require_relative "behavior_analytics/schema/definition"
39
+ require_relative "behavior_analytics/export/csv_exporter"
40
+ require_relative "behavior_analytics/export/json_exporter"
41
+ require_relative "behavior_analytics/reporting/generator"
42
+ require_relative "behavior_analytics/observability/metrics"
43
+ require_relative "behavior_analytics/observability/tracer"
44
+ require_relative "behavior_analytics/debug/inspector"
45
+ require_relative "behavior_analytics/processors/async_processor"
46
+ require_relative "behavior_analytics/processors/background_job_processor"
47
+ require_relative "behavior_analytics/streaming/event_stream"
12
48
 
13
49
  begin
14
50
  require_relative "behavior_analytics/integrations/rails"
15
51
  rescue LoadError
16
52
  end
17
53
 
54
+ begin
55
+ require_relative "behavior_analytics/jobs/active_event_job"
56
+ rescue LoadError
57
+ end
58
+
59
+ begin
60
+ require_relative "behavior_analytics/jobs/sidekiq_event_job"
61
+ rescue LoadError
62
+ end
63
+
64
+ begin
65
+ require_relative "behavior_analytics/jobs/delayed_event_job"
66
+ rescue LoadError
67
+ end
68
+
18
69
  module BehaviorAnalytics
19
- class Error < StandardError; end
70
+ class Error < StandardError
71
+ attr_reader :context
72
+
73
+ def initialize(message, context: nil)
74
+ super(message)
75
+ @context = context
76
+ end
77
+
78
+ def to_s
79
+ base_message = super
80
+ if @context && BehaviorAnalytics.configuration.debug_mode
81
+ "#{base_message} (Context: #{@context.inspect})"
82
+ else
83
+ base_message
84
+ end
85
+ end
86
+ end
87
+
88
+ class ValidationError < Error; end
89
+ class ConfigurationError < Error; end
90
+ class StorageError < Error; end
20
91
 
21
92
  class Configuration
22
- attr_accessor :storage_adapter, :batch_size, :flush_interval, :context_resolver, :scoring_weights
93
+ attr_accessor :storage_adapter, :batch_size, :flush_interval, :context_resolver, :scoring_weights,
94
+ :async_processor, :use_async, :event_stream, :environment, :feature_flags,
95
+ :hooks_manager, :raise_on_hook_error, :sampling_strategy, :rate_limiter,
96
+ :schema_validator, :schema_registry, :tracking_whitelist, :tracking_blacklist,
97
+ :skip_bots, :controller_action_filters, :slow_query_threshold, :track_middleware_requests,
98
+ :metrics, :tracer, :debug_mode, :logger, :default_tenant_id
23
99
 
24
100
  def initialize
25
101
  @batch_size = 100
26
102
  @flush_interval = 300
103
+ @use_async = false
27
104
  @scoring_weights = {
28
105
  activity: 0.4,
29
106
  unique_users: 0.3,
30
107
  feature_diversity: 0.2,
31
108
  time_in_trial: 0.1
32
109
  }
110
+ @environment = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
111
+ @feature_flags = {}
112
+ @event_stream = Streaming::EventStream.new
113
+ @hooks_manager = Hooks::Manager.new
114
+ @raise_on_hook_error = false
115
+ @schema_registry = Schema::Registry.new
116
+ @tracking_whitelist = nil
117
+ @tracking_blacklist = []
118
+ @skip_bots = true
119
+ @controller_action_filters = {}
120
+ @slow_query_threshold = nil
121
+ @track_middleware_requests = false
122
+ @metrics = Observability::Metrics.new
123
+ @tracer = nil
124
+ @debug_mode = @environment == "development"
125
+ @logger = nil
126
+ @default_tenant_id = "default" # Default tenant for single-tenant systems
127
+ end
128
+
129
+ def debug(message, context: nil)
130
+ return unless @debug_mode
131
+
132
+ log_message = "[BehaviorAnalytics] #{message}"
133
+ log_message += " (Context: #{context.inspect})" if context
134
+
135
+ if @logger
136
+ @logger.debug(log_message)
137
+ elsif defined?(Rails) && Rails.logger
138
+ Rails.logger.debug(log_message)
139
+ else
140
+ puts log_message
141
+ end
142
+ end
143
+
144
+ def log_error(error, context: nil)
145
+ error_message = error.message
146
+ error_message += " (Context: #{context.inspect})" if context
147
+
148
+ if @logger
149
+ @logger.error("[BehaviorAnalytics] #{error_message}")
150
+ @logger.error(error.backtrace.join("\n")) if error.backtrace
151
+ elsif defined?(Rails) && Rails.logger
152
+ Rails.logger.error("[BehaviorAnalytics] #{error_message}")
153
+ Rails.logger.error(error.backtrace.join("\n")) if error.backtrace
154
+ else
155
+ puts "[BehaviorAnalytics ERROR] #{error_message}"
156
+ puts error.backtrace.join("\n") if error.backtrace
157
+ end
158
+ end
159
+
160
+ def feature_enabled?(feature)
161
+ @feature_flags.fetch(feature, false)
162
+ end
163
+
164
+ def enable_feature(feature)
165
+ @feature_flags[feature] = true
166
+ end
167
+
168
+ def disable_feature(feature)
169
+ @feature_flags[feature] = false
33
170
  end
34
171
  end
35
172
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: behavior_analytics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nerdawey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-04 00:00:00.000000000 Z
11
+ date: 2026-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -81,22 +81,52 @@ files:
81
81
  - Rakefile
82
82
  - behavior_analytics.gemspec
83
83
  - db/migrate/001_create_behavior_events.rb
84
+ - db/migrate/002_enhance_behavior_events_v2.rb
84
85
  - lib/behavior_analytics.rb
86
+ - lib/behavior_analytics/analytics/cohorts.rb
85
87
  - lib/behavior_analytics/analytics/engine.rb
88
+ - lib/behavior_analytics/analytics/funnels.rb
89
+ - lib/behavior_analytics/analytics/retention.rb
86
90
  - lib/behavior_analytics/context.rb
91
+ - lib/behavior_analytics/debug/inspector.rb
87
92
  - lib/behavior_analytics/event.rb
93
+ - lib/behavior_analytics/export/csv_exporter.rb
94
+ - lib/behavior_analytics/export/json_exporter.rb
95
+ - lib/behavior_analytics/hooks/callback.rb
96
+ - lib/behavior_analytics/hooks/manager.rb
97
+ - lib/behavior_analytics/hooks/webhook.rb
88
98
  - lib/behavior_analytics/integrations/rails.rb
99
+ - lib/behavior_analytics/integrations/rails/middleware.rb
100
+ - lib/behavior_analytics/jobs/active_event_job.rb
101
+ - lib/behavior_analytics/jobs/delayed_event_job.rb
102
+ - lib/behavior_analytics/jobs/sidekiq_event_job.rb
103
+ - lib/behavior_analytics/observability/metrics.rb
104
+ - lib/behavior_analytics/observability/tracer.rb
105
+ - lib/behavior_analytics/processors/async_processor.rb
106
+ - lib/behavior_analytics/processors/background_job_processor.rb
89
107
  - lib/behavior_analytics/query.rb
108
+ - lib/behavior_analytics/replay/engine.rb
109
+ - lib/behavior_analytics/replay/processor.rb
110
+ - lib/behavior_analytics/reporting/generator.rb
111
+ - lib/behavior_analytics/sampling/strategy.rb
112
+ - lib/behavior_analytics/schema/definition.rb
113
+ - lib/behavior_analytics/schema/validator.rb
90
114
  - lib/behavior_analytics/storage/active_record_adapter.rb
91
115
  - lib/behavior_analytics/storage/adapter.rb
116
+ - lib/behavior_analytics/storage/elasticsearch_adapter.rb
92
117
  - lib/behavior_analytics/storage/in_memory_adapter.rb
118
+ - lib/behavior_analytics/storage/kafka_adapter.rb
119
+ - lib/behavior_analytics/storage/redis_adapter.rb
120
+ - lib/behavior_analytics/streaming/event_stream.rb
121
+ - lib/behavior_analytics/throttling/limiter.rb
93
122
  - lib/behavior_analytics/tracker.rb
94
123
  - lib/behavior_analytics/version.rb
95
124
  - lib/generators/behavior_analytics/install_generator.rb
96
125
  - lib/generators/behavior_analytics/templates/create_behavior_events.rb
97
126
  - sig/behavior_analytics.rbs
98
127
  homepage: https://github.com/nerdawey/behavior_analytics
99
- licenses: []
128
+ licenses:
129
+ - MIT
100
130
  metadata:
101
131
  homepage_uri: https://github.com/nerdawey/behavior_analytics
102
132
  source_code_uri: https://github.com/nerdawey/behavior_analytics