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 +4 -4
- data/README.md +146 -5
- data/lib/behavior_analytics/context.rb +38 -2
- data/lib/behavior_analytics/event.rb +7 -1
- data/lib/behavior_analytics/integrations/rails.rb +17 -2
- data/lib/behavior_analytics/query.rb +2 -2
- data/lib/behavior_analytics/storage/active_record_adapter.rb +15 -2
- data/lib/behavior_analytics/storage/elasticsearch_adapter.rb +14 -4
- data/lib/behavior_analytics/storage/in_memory_adapter.rb +20 -3
- data/lib/behavior_analytics/storage/kafka_adapter.rb +18 -3
- data/lib/behavior_analytics/storage/redis_adapter.rb +42 -6
- data/lib/behavior_analytics/version.rb +1 -1
- data/lib/behavior_analytics.rb +2 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a76aa6cdb7e21d5ac45f72e98f1e9b7bb678169eae8c75d0fdf821166986fa0
|
|
4
|
+
data.tar.gz: a80ea11cdfe8b429e4f793736fe756b583ffe3ffc7d8118e551ac68229f10b38
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
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` (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
118
|
-
|
|
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.
|
|
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
|
|
data/lib/behavior_analytics.rb
CHANGED
|
@@ -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)
|