behavior_analytics 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ab1248da55b16fed6161e2d716579fdca58301bebd9336f0a15c3ffa3b036a7
4
- data.tar.gz: 60dfde4fc53044dcf04a6fda27ab6d9caf41bbe3b421e892051e17ada80fbf8c
3
+ metadata.gz: 7a76aa6cdb7e21d5ac45f72e98f1e9b7bb678169eae8c75d0fdf821166986fa0
4
+ data.tar.gz: a80ea11cdfe8b429e4f793736fe756b583ffe3ffc7d8118e551ac68229f10b38
5
5
  SHA512:
6
- metadata.gz: f967ac25119baf6458fd318c8685de334686f0c0577c4ef5956aeef9bec5791e1bcaf9ea6697fdf69aa3040468aa02a3d6afad176dae33ffa0eed958d3d571c1
7
- data.tar.gz: 2b230bb9364e2afcf1f8465b37a287d10a6f48a6ecc5251a152f607eafa1f045eebe5a46f3fdfa4bd3874a4b27039d7d925f5aa42399b4e8535d5a77473e3757
6
+ metadata.gz: 84183ce32e7e7ba31bad6f19ddbaa83b3f761c8546eff06b43a5824bc981b09bce7598d9da8df329be0e3cfec72831607cae3fe1cd1fa5e45292379aa5b00ac2
7
+ data.tar.gz: bb85ca2d9bc6ff08264eb98d0732b3d58d43401a253dbfdbb8b6fb52e68e832f01caf72699b1e71bcf6631bb6aaa450b3d9ee02bb2e95132362193ba0b42d9d8
data/README.md CHANGED
@@ -100,19 +100,97 @@ end
100
100
 
101
101
  ## Usage
102
102
 
103
+ ### Supported Business Cases
104
+
105
+ The gem is flexible and supports different business scenarios:
106
+
107
+ #### 1. Multi-Tenant Systems
108
+ Track events with tenant isolation for SaaS applications:
109
+
110
+ ```ruby
111
+ context = BehaviorAnalytics::Context.new(
112
+ tenant_id: "org_123",
113
+ user_id: "user_456",
114
+ user_type: "premium"
115
+ )
116
+ ```
117
+
118
+ #### 2. Single-Tenant Web Apps
119
+ Track events for regular web applications without tenant concept:
120
+
121
+ ```ruby
122
+ # Option A: Set default tenant (recommended)
123
+ BehaviorAnalytics.configure do |config|
124
+ config.default_tenant_id = "global"
125
+ end
126
+
127
+ context = BehaviorAnalytics::Context.new(
128
+ user_id: current_user.id,
129
+ user_type: "admin"
130
+ )
131
+
132
+ # Option B: Track without tenant_id (uses session_id or user_id as identifier)
133
+ context = BehaviorAnalytics::Context.new(
134
+ user_id: current_user.id
135
+ )
136
+ ```
137
+
138
+ #### 3. API-Only Tracking
139
+ Track API calls without user context (for monitoring, analytics, etc.):
140
+
141
+ ```ruby
142
+ # Track API calls directly without user context
143
+ tracker.track_api_call(
144
+ context: BehaviorAnalytics::Context.new, # Empty context - uses session_id from request
145
+ method: "POST",
146
+ path: "/api/endpoint",
147
+ status_code: 200,
148
+ duration_ms: 150
149
+ )
150
+
151
+ # Or with minimal context
152
+ context = BehaviorAnalytics::Context.new(
153
+ filters: { environment: "production", service: "api" }
154
+ )
155
+ ```
156
+
157
+ #### 4. Anonymous/Public Tracking
158
+ Track events for anonymous users or public pages:
159
+
160
+ ```ruby
161
+ context = BehaviorAnalytics::Context.new(
162
+ filters: { page: "homepage", referrer: request.referer }
163
+ )
164
+
165
+ tracker.track(
166
+ context: context,
167
+ event_name: "page_view",
168
+ metadata: { path: request.path }
169
+ )
170
+ ```
171
+
103
172
  ### Basic Tracking
104
173
 
105
174
  ```ruby
106
175
  # Create a tracker
107
176
  tracker = BehaviorAnalytics.create_tracker
108
177
 
109
- # Create a context
178
+ # Multi-tenant example
110
179
  context = BehaviorAnalytics::Context.new(
111
180
  tenant_id: "org_123",
112
181
  user_id: "user_456",
113
182
  user_type: "trial"
114
183
  )
115
184
 
185
+ # Single-tenant example (with default tenant)
186
+ context = BehaviorAnalytics::Context.new(
187
+ user_id: "user_456",
188
+ user_type: "trial"
189
+ )
190
+
191
+ # API-only example (no user context)
192
+ context = BehaviorAnalytics::Context.new
193
+
116
194
  # Track a custom event
117
195
  tracker.track(
118
196
  context: context,
@@ -215,6 +293,7 @@ tracker = BehaviorAnalytics.create_tracker(
215
293
  - `flush_interval`: Seconds between automatic flushes (default: 300)
216
294
  - `context_resolver`: Lambda/proc to resolve context from requests
217
295
  - `scoring_weights`: Hash of weights for engagement scoring
296
+ - `default_tenant_id`: Default tenant ID for single-tenant systems (default: "default")
218
297
 
219
298
  ## Event Types
220
299
 
@@ -224,12 +303,74 @@ tracker = BehaviorAnalytics.create_tracker(
224
303
 
225
304
  ## Context
226
305
 
227
- The `Context` class encapsulates tracking context:
306
+ The `Context` class encapsulates tracking context and is flexible to support different business cases:
228
307
 
229
- - `tenant_id` (required) - Multi-tenant identifier
230
- - `user_id` (optional) - User identifier
308
+ - `tenant_id` (optional) - Multi-tenant identifier. Only required for multi-tenant systems
309
+ - `user_id` (optional) - User identifier. Useful for user-based analytics
231
310
  - `user_type` (optional) - User type (e.g., "trial", "premium", "admin")
232
- - `filters` (optional) - Hash of custom filter criteria
311
+ - `filters` (optional) - Hash of custom filter criteria for additional context
312
+
313
+ ### Context Validation
314
+
315
+ A context is valid if it has **at least one identifier**:
316
+ - `tenant_id` (for multi-tenant systems)
317
+ - `user_id` (for user-based tracking)
318
+ - `filters` with identifying information (for anonymous/public tracking)
319
+ - `session_id` (automatically added for API calls)
320
+
321
+ This allows the gem to support:
322
+ - ✅ Multi-tenant SaaS applications
323
+ - ✅ Single-tenant web applications
324
+ - ✅ API monitoring without user context
325
+ - ✅ Anonymous/public page tracking
326
+
327
+ ### Examples by Use Case
328
+
329
+ **Multi-Tenant SaaS:**
330
+ ```ruby
331
+ context = BehaviorAnalytics::Context.new(
332
+ tenant_id: "org_123", # Required
333
+ user_id: "user_456",
334
+ user_type: "premium"
335
+ )
336
+ ```
337
+
338
+ **Single-Tenant Web App:**
339
+ ```ruby
340
+ # Set default tenant (optional but recommended)
341
+ BehaviorAnalytics.configure do |config|
342
+ config.default_tenant_id = "global"
343
+ end
344
+
345
+ # Track with just user_id
346
+ context = BehaviorAnalytics::Context.new(
347
+ user_id: current_user.id,
348
+ user_type: current_user.role
349
+ )
350
+ ```
351
+
352
+ **API-Only Tracking:**
353
+ ```ruby
354
+ # Track API calls without user context
355
+ context = BehaviorAnalytics::Context.new # Empty context - session_id will be used
356
+ tracker.track_api_call(
357
+ context: context,
358
+ method: "POST",
359
+ path: "/api/endpoint",
360
+ status_code: 200
361
+ )
362
+ ```
363
+
364
+ **Anonymous/Public Tracking:**
365
+ ```ruby
366
+ context = BehaviorAnalytics::Context.new(
367
+ filters: {
368
+ page_type: "public",
369
+ referrer: request.referer
370
+ }
371
+ )
372
+ tracker.track(context: context, event_name: "page_view")
373
+ ```
233
374
 
234
375
  ## Development
235
376
 
@@ -5,7 +5,11 @@ module BehaviorAnalytics
5
5
  attr_accessor :tenant_id, :user_id, :user_type, :filters
6
6
 
7
7
  def initialize(attributes = {})
8
+ # Only use default_tenant_id if explicitly configured and no tenant_id provided
9
+ # This allows tracking without tenant_id for non-multi-tenant systems
8
10
  @tenant_id = attributes[:tenant_id] || attributes[:tenant]
11
+ @tenant_id ||= default_tenant_id if use_default_tenant?
12
+
9
13
  @user_id = attributes[:user_id] || attributes[:user]
10
14
  @user_type = attributes[:user_type]
11
15
  @filters = attributes[:filters] || {}
@@ -21,11 +25,43 @@ module BehaviorAnalytics
21
25
  end
22
26
 
23
27
  def valid?
24
- !tenant_id.nil? && !tenant_id.empty?
28
+ # Context is valid if it has at least one identifier (tenant_id, user_id, or both)
29
+ # This supports different business cases:
30
+ # - Multi-tenant: tenant_id required
31
+ # - Single-tenant: user_id sufficient
32
+ # - API-only tracking: tenant_id or user_id optional
33
+ has_tenant? || has_user? || has_any_identifier?
34
+ end
35
+
36
+ def has_tenant?
37
+ !tenant_id.nil? && !tenant_id.to_s.empty?
38
+ end
39
+
40
+ def has_user?
41
+ !user_id.nil? && !user_id.to_s.empty?
42
+ end
43
+
44
+ def has_any_identifier?
45
+ # Check if filters contain any identifying information
46
+ filters.is_a?(Hash) && !filters.empty?
25
47
  end
26
48
 
27
49
  def validate!
28
- raise Error, "tenant_id is required in context" unless valid?
50
+ unless valid?
51
+ raise Error, "Context must have at least one identifier (tenant_id, user_id, or filters). " \
52
+ "For single-tenant systems, set default_tenant_id in configuration or provide user_id."
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def default_tenant_id
59
+ BehaviorAnalytics.configuration.default_tenant_id
60
+ end
61
+
62
+ def use_default_tenant?
63
+ # Only use default tenant if it's explicitly set (not nil)
64
+ default_tenant_id && !default_tenant_id.to_s.empty?
29
65
  end
30
66
  end
31
67
  end
@@ -46,7 +46,13 @@ module BehaviorAnalytics
46
46
  private
47
47
 
48
48
  def validate!
49
- raise Error, "tenant_id is required" if tenant_id.nil? || tenant_id.empty?
49
+ # tenant_id is optional - events can be tracked without tenant for non-multi-tenant systems
50
+ # At least one identifier should be present (tenant_id, user_id, or session_id)
51
+ has_identifier = (!tenant_id.nil? && !tenant_id.to_s.empty?) ||
52
+ (!user_id.nil? && !user_id.to_s.empty?) ||
53
+ (!session_id.nil? && !session_id.to_s.empty?)
54
+
55
+ raise Error, "Event must have at least one identifier (tenant_id, user_id, or session_id)" unless has_identifier
50
56
  raise Error, "event_name is required" if event_name.nil? || event_name.empty?
51
57
  raise Error, "event_type must be one of: #{EVENT_TYPES.join(', ')}" unless EVENT_TYPES.include?(event_type)
52
58
  end
@@ -70,14 +70,26 @@ 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?
81
93
 
82
94
  # Check path whitelist/blacklist
83
95
  return false if path_blacklisted?
@@ -155,6 +167,9 @@ module BehaviorAnalytics
155
167
  ensure
156
168
  if should_track?
157
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
+
158
173
  if context&.valid?
159
174
  duration_ms = ((Time.current - start_time) * 1000).to_i
160
175
 
@@ -132,7 +132,7 @@ module BehaviorAnalytics
132
132
  end
133
133
 
134
134
  def execute
135
- raise Error, "Context must have tenant_id" unless @context&.valid?
135
+ raise Error, "Context must be valid (have at least tenant_id, user_id, or filters)" unless @context&.valid?
136
136
 
137
137
  # Merge metadata filters and other options
138
138
  final_options = @options.dup
@@ -146,7 +146,7 @@ module BehaviorAnalytics
146
146
  end
147
147
 
148
148
  def count
149
- raise Error, "Context must have tenant_id" unless @context&.valid?
149
+ raise Error, "Context must be valid (have at least tenant_id, user_id, or filters)" unless @context&.valid?
150
150
 
151
151
  final_options = @options.dup
152
152
  final_options[:metadata_filters] = @metadata_filters unless @metadata_filters.empty?
@@ -41,6 +41,9 @@ module BehaviorAnalytics
41
41
 
42
42
  query = build_base_query(context, options)
43
43
 
44
+ # If no tenant_id, query all events (for non-multi-tenant systems)
45
+ # This allows tracking without tenant isolation
46
+
44
47
  # Apply metadata filters
45
48
  if options[:metadata_filters]
46
49
  options[:metadata_filters].each do |key, value|
@@ -140,6 +143,7 @@ module BehaviorAnalytics
140
143
  end
141
144
 
142
145
  def event_count(context, options = {})
146
+ context.validate!
143
147
  query = build_base_query(context, options)
144
148
 
145
149
  # Apply metadata filters
@@ -211,8 +215,17 @@ module BehaviorAnalytics
211
215
  def build_base_query(context, options)
212
216
  context.validate!
213
217
 
214
- query = @model_class.where(tenant_id: context.tenant_id)
215
- query = query.where(user_id: context.user_id) if context.user_id
218
+ # Support different business cases:
219
+ # - Multi-tenant: filter by tenant_id
220
+ # - Single-tenant: filter by user_id (tenant_id may be nil)
221
+ # - API-only: no filters required
222
+ query = @model_class.all
223
+
224
+ if context.has_tenant?
225
+ query = query.where(tenant_id: context.tenant_id)
226
+ end
227
+
228
+ query = query.where(user_id: context.user_id) if context.has_user?
216
229
  query = query.where(user_type: context.user_type) if context.user_type
217
230
 
218
231
  query = query.where("created_at >= ?", options[:since]) if options[:since]
@@ -125,11 +125,21 @@ module BehaviorAnalytics
125
125
  end
126
126
 
127
127
  def build_query(context, options)
128
- must_clauses = [
129
- { term: { tenant_id: context.tenant_id } }
130
- ]
128
+ must_clauses = []
129
+
130
+ # Support different business cases:
131
+ # - Multi-tenant: filter by tenant_id
132
+ # - Single-tenant: filter by user_id (tenant_id may be nil)
133
+ # - API-only: no strict filters required
134
+
135
+ if context.has_tenant?
136
+ must_clauses << { term: { tenant_id: context.tenant_id } }
137
+ end
138
+
139
+ if context.has_user?
140
+ must_clauses << { term: { user_id: context.user_id } }
141
+ end
131
142
 
132
- must_clauses << { term: { user_id: context.user_id } } if context.user_id
133
143
  must_clauses << { term: { user_type: context.user_type } } if context.user_type
134
144
  must_clauses << { term: { event_name: options[:event_name] } } if options[:event_name]
135
145
  must_clauses << { term: { event_type: options[:event_type].to_s } } if options[:event_type]
@@ -143,9 +143,26 @@ module BehaviorAnalytics
143
143
 
144
144
  def filter_by_context(events, context)
145
145
  events.select do |event|
146
- matches_tenant = event[:tenant_id] == context.tenant_id
147
- matches_user = context.user_id.nil? || event[:user_id] == context.user_id || event[:user_id].nil?
148
- matches_user_type = context.user_type.nil? || event[:user_type] == context.user_type || event[:user_type].nil?
146
+ # Support different business cases:
147
+ # - Multi-tenant: must match tenant_id
148
+ # - Single-tenant: match user_id (tenant_id may be nil)
149
+ # - API-only: no strict matching required
150
+
151
+ matches_tenant = if context.has_tenant?
152
+ event[:tenant_id] == context.tenant_id
153
+ else
154
+ true # No tenant filter if context doesn't have tenant
155
+ end
156
+
157
+ matches_user = if context.has_user?
158
+ event[:user_id] == context.user_id
159
+ else
160
+ true # No user filter if context doesn't have user
161
+ end
162
+
163
+ matches_user_type = context.user_type.nil? ||
164
+ event[:user_type] == context.user_type ||
165
+ event[:user_type].nil?
149
166
 
150
167
  matches_tenant && matches_user && matches_user_type
151
168
  end
@@ -36,7 +36,15 @@ module BehaviorAnalytics
36
36
 
37
37
  # Kafka is primarily for streaming, so we need a consumer
38
38
  # This is a simplified version - in production you'd use a proper consumer group
39
- consumer = @kafka.consumer(group_id: "behavior_analytics_#{context.tenant_id}")
39
+ group_id = if context.has_tenant?
40
+ "behavior_analytics_#{context.tenant_id}"
41
+ elsif context.has_user?
42
+ "behavior_analytics_user_#{context.user_id}"
43
+ else
44
+ "behavior_analytics_global"
45
+ end
46
+
47
+ consumer = @kafka.consumer(group_id: group_id)
40
48
  consumer.subscribe(@topic)
41
49
 
42
50
  events = []
@@ -88,8 +96,15 @@ module BehaviorAnalytics
88
96
  end
89
97
 
90
98
  def matches_context?(event, context, options)
91
- return false unless event[:tenant_id] == context.tenant_id
92
- return false if context.user_id && event[:user_id] != context.user_id
99
+ # Support different business cases
100
+ if context.has_tenant?
101
+ return false unless event[:tenant_id] == context.tenant_id
102
+ end
103
+
104
+ if context.has_user?
105
+ return false unless event[:user_id] == context.user_id
106
+ end
107
+
93
108
  return false if context.user_type && event[:user_type] != context.user_type
94
109
  return false if options[:event_name] && event[:event_name] != options[:event_name]
95
110
  return false if options[:event_type] && event[:event_type] != options[:event_type]
@@ -98,8 +98,13 @@ module BehaviorAnalytics
98
98
  user_id = event_hash[:user_id]
99
99
  event_type = event_hash[:event_type]
100
100
 
101
- @redis.sadd("#{@key_prefix}:tenant:#{tenant_id}", event_hash[:id])
101
+ # Index by tenant if present (multi-tenant)
102
+ @redis.sadd("#{@key_prefix}:tenant:#{tenant_id}", event_hash[:id]) if tenant_id
103
+
104
+ # Index by user if present (single-tenant or multi-tenant)
102
105
  @redis.sadd("#{@key_prefix}:user:#{user_id}", event_hash[:id]) if user_id
106
+
107
+ # Index by event type
103
108
  @redis.sadd("#{@key_prefix}:type:#{event_type}", event_hash[:id]) if event_type
104
109
  end
105
110
 
@@ -114,17 +119,38 @@ module BehaviorAnalytics
114
119
  end
115
120
 
116
121
  def find_event_ids(context, options)
117
- # Start with tenant index
118
- ids = @redis.smembers("#{@key_prefix}:tenant:#{context.tenant_id}").to_a
122
+ # Support different business cases:
123
+ # - Multi-tenant: use tenant index
124
+ # - Single-tenant: use user index
125
+ # - API-only: use event type or all events
126
+
127
+ if context.has_tenant?
128
+ # Start with tenant index
129
+ ids = @redis.smembers("#{@key_prefix}:tenant:#{context.tenant_id}").to_a
130
+ elsif context.has_user?
131
+ # Use user index for single-tenant systems
132
+ ids = @redis.smembers("#{@key_prefix}:user:#{context.user_id}").to_a
133
+ else
134
+ # API-only or anonymous tracking - start with all or event type
135
+ if options[:event_type]
136
+ ids = @redis.smembers("#{@key_prefix}:type:#{options[:event_type]}").to_a
137
+ else
138
+ # Get all event IDs (scan all keys - less efficient but supports API-only tracking)
139
+ ids = []
140
+ @redis.scan_each(match: "#{@key_prefix}:event:*") do |key|
141
+ ids << key.split(":").last
142
+ end
143
+ end
144
+ end
119
145
 
120
- # Intersect with user index if specified
121
- if context.user_id
146
+ # Intersect with user index if specified (in addition to tenant)
147
+ if context.has_tenant? && context.has_user?
122
148
  user_ids = @redis.smembers("#{@key_prefix}:user:#{context.user_id}").to_a
123
149
  ids = ids & user_ids
124
150
  end
125
151
 
126
152
  # Intersect with event type if specified
127
- if options[:event_type]
153
+ if options[:event_type] && context.has_tenant?
128
154
  type_ids = @redis.smembers("#{@key_prefix}:type:#{options[:event_type]}").to_a
129
155
  ids = ids & type_ids
130
156
  end
@@ -136,6 +162,16 @@ module BehaviorAnalytics
136
162
  events.select do |event|
137
163
  matches = true
138
164
 
165
+ # Tenant matching (if context has tenant)
166
+ if context.has_tenant?
167
+ matches &&= event[:tenant_id] == context.tenant_id
168
+ end
169
+
170
+ # User matching (if context has user)
171
+ if context.has_user?
172
+ matches &&= event[:user_id] == context.user_id
173
+ end
174
+
139
175
  matches &&= event[:user_type] == context.user_type if context.user_type
140
176
  matches &&= event[:event_name] == options[:event_name] if options[:event_name]
141
177
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BehaviorAnalytics
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.0"
5
5
  end
@@ -95,7 +95,7 @@ module BehaviorAnalytics
95
95
  :hooks_manager, :raise_on_hook_error, :sampling_strategy, :rate_limiter,
96
96
  :schema_validator, :schema_registry, :tracking_whitelist, :tracking_blacklist,
97
97
  :skip_bots, :controller_action_filters, :slow_query_threshold, :track_middleware_requests,
98
- :metrics, :tracer, :debug_mode, :logger
98
+ :metrics, :tracer, :debug_mode, :logger, :default_tenant_id
99
99
 
100
100
  def initialize
101
101
  @batch_size = 100
@@ -123,6 +123,7 @@ module BehaviorAnalytics
123
123
  @tracer = nil
124
124
  @debug_mode = @environment == "development"
125
125
  @logger = nil
126
+ @default_tenant_id = "default" # Default tenant for single-tenant systems
126
127
  end
127
128
 
128
129
  def debug(message, context: nil)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: behavior_analytics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nerdawey