behavior_analytics 2.1.0 → 2.2.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.
@@ -8,10 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["nerdawey"]
9
9
  spec.email = ["nerdawy@icloud.com"]
10
10
 
11
- spec.summary = "Track user behavior events with flexible context filtering and comprehensive analytics"
12
- spec.description = "A Ruby gem for tracking user behavior events with multi-tenant support, " \
13
- "computing analytics (engagement scores, time-based trends, feature usage), " \
14
- "and supporting API calls, feature usage, and custom events."
11
+ spec.summary = "Track user behavior events with visit tracking, device detection, and comprehensive analytics"
12
+ spec.description = "A comprehensive Ruby gem for tracking user behavior events with multi-tenant support, " \
13
+ "visit/session management (Ahoy-inspired), device & browser detection, geographic analytics, " \
14
+ "referrer tracking, and advanced analytics (engagement scores, funnels, cohorts, retention). " \
15
+ "Supports API calls, feature usage, custom events, and JavaScript client-side tracking."
15
16
  spec.homepage = "https://github.com/nerdawey/behavior_analytics"
16
17
  spec.license = "MIT"
17
18
  spec.required_ruby_version = ">= 3.0.0"
@@ -13,6 +13,8 @@ module BehaviorAnalytics
13
13
  @funnels = nil
14
14
  @cohorts = nil
15
15
  @retention = nil
16
+ @geographic = nil
17
+ @referrer = nil
16
18
  end
17
19
 
18
20
  def event_count(context, options = {})
@@ -139,6 +141,14 @@ module BehaviorAnalytics
139
141
  @retention ||= Retention.new(@storage_adapter)
140
142
  end
141
143
 
144
+ def geographic
145
+ @geographic ||= Geographic.new(storage_adapter: @storage_adapter)
146
+ end
147
+
148
+ def referrer
149
+ @referrer ||= Referrer.new(storage_adapter: @storage_adapter)
150
+ end
151
+
142
152
  private
143
153
 
144
154
  def normalize_context(context)
@@ -7,7 +7,8 @@ module BehaviorAnalytics
7
7
  EVENT_TYPES = %i[api_call feature_usage custom].freeze
8
8
 
9
9
  attr_accessor :id, :tenant_id, :user_id, :user_type, :event_name, :event_type,
10
- :metadata, :session_id, :ip, :user_agent, :duration_ms, :created_at
10
+ :metadata, :session_id, :ip, :user_agent, :duration_ms, :created_at,
11
+ :visit_id, :visitor_id
11
12
 
12
13
  def initialize(attributes = {})
13
14
  @id = attributes[:id] || SecureRandom.uuid
@@ -21,6 +22,8 @@ module BehaviorAnalytics
21
22
  @ip = attributes[:ip]
22
23
  @user_agent = attributes[:user_agent]
23
24
  @duration_ms = attributes[:duration_ms]
25
+ @visit_id = attributes[:visit_id]
26
+ @visitor_id = attributes[:visitor_id]
24
27
  @created_at = attributes[:created_at] || Time.now
25
28
 
26
29
  validate!
@@ -39,6 +42,8 @@ module BehaviorAnalytics
39
42
  ip: ip,
40
43
  user_agent: user_agent,
41
44
  duration_ms: duration_ms,
45
+ visit_id: visit_id,
46
+ visitor_id: visitor_id,
42
47
  created_at: created_at
43
48
  }
44
49
  end
@@ -26,34 +26,6 @@ module BehaviorAnalytics
26
26
 
27
27
  private
28
28
 
29
- def track_behavior_analytics
30
- start_time = Time.current
31
- yield
32
- ensure
33
- if should_track?
34
- context = resolve_tracking_context
35
- if context&.valid?
36
- duration_ms = ((Time.current - start_time) * 1000).to_i
37
-
38
- behavior_tracker.track_api_call(
39
- context: context,
40
- method: request.method,
41
- path: request.path,
42
- status_code: response.status,
43
- duration_ms: duration_ms,
44
- ip: request.remote_ip,
45
- user_agent: request.user_agent,
46
- session_id: session.id,
47
- metadata: {
48
- controller: controller_name,
49
- action: action_name,
50
- format: request.format.to_s
51
- }
52
- )
53
- end
54
- end
55
- end
56
-
57
29
  def behavior_tracker
58
30
  @behavior_tracker ||= BehaviorAnalytics.create_tracker(
59
31
  storage_adapter: BehaviorAnalytics.configuration.storage_adapter
@@ -180,6 +152,25 @@ module BehaviorAnalytics
180
152
  end
181
153
  end
182
154
 
155
+ # Get or create visit if visit tracking is enabled
156
+ visit = nil
157
+ visitor_id = nil
158
+ if BehaviorAnalytics.configuration.track_visits && visit_manager
159
+ begin
160
+ visit = visit_auto_creator.get_or_create_visit(
161
+ request: request,
162
+ tenant_id: context.tenant_id,
163
+ user_id: context.user_id
164
+ )
165
+ visitor_id = visit.visitor_token
166
+
167
+ # Set visitor token cookie
168
+ visit_auto_creator.set_visitor_token_cookie(response, visitor_id) if respond_to?(:response)
169
+ rescue StandardError => e
170
+ BehaviorAnalytics.configuration.log_error(e, context: { action: "visit_creation" })
171
+ end
172
+ end
173
+
183
174
  behavior_tracker.track_api_call(
184
175
  context: context,
185
176
  method: request.method,
@@ -189,6 +180,8 @@ module BehaviorAnalytics
189
180
  ip: request.remote_ip,
190
181
  user_agent: request.user_agent,
191
182
  session_id: session.id,
183
+ visit_id: visit&.visit_token,
184
+ visitor_id: visitor_id,
192
185
  metadata: {
193
186
  controller: controller_name,
194
187
  action: action_name,
@@ -201,6 +194,30 @@ module BehaviorAnalytics
201
194
  end
202
195
  end
203
196
 
197
+ def visit_manager
198
+ return nil unless BehaviorAnalytics.configuration.track_visits
199
+ @visit_manager ||= begin
200
+ device_detector = if BehaviorAnalytics.configuration.track_device_info
201
+ Detection::DeviceDetector.new(strategy: BehaviorAnalytics.configuration.device_detector)
202
+ end
203
+
204
+ geolocation = if BehaviorAnalytics.configuration.track_geolocation
205
+ Detection::Geolocation.new(strategy: :geocoder)
206
+ end
207
+
208
+ Visits::Manager.new(
209
+ storage_adapter: @behavior_tracker&.storage_adapter || BehaviorAnalytics.configuration.storage_adapter,
210
+ visit_duration: BehaviorAnalytics.configuration.visit_duration || 30.minutes,
211
+ device_detector: device_detector,
212
+ geolocation: geolocation
213
+ )
214
+ end
215
+ end
216
+
217
+ def visit_auto_creator
218
+ @visit_auto_creator ||= Visits::AutoCreator.new(manager: visit_manager)
219
+ end
220
+
204
221
  def log_slow_query(duration_ms, path)
205
222
  if defined?(Rails) && Rails.logger
206
223
  Rails.logger.warn("BehaviorAnalytics: Slow query detected: #{path} took #{duration_ms}ms")
@@ -27,6 +27,8 @@ module BehaviorAnalytics
27
27
  ip: event_hash[:ip],
28
28
  user_agent: event_hash[:user_agent],
29
29
  duration_ms: event_hash[:duration_ms],
30
+ visit_id: event_hash[:visit_id],
31
+ visitor_id: event_hash[:visitor_id],
30
32
  created_at: event_hash[:created_at] || (defined?(Time.current) ? Time.current : Time.now)
31
33
  }
32
34
  end
@@ -36,6 +38,69 @@ module BehaviorAnalytics
36
38
  raise Error, "Failed to save events: #{e.message}"
37
39
  end
38
40
 
41
+ def save_visit(visit_data)
42
+ return unless defined?(BehaviorAnalyticsVisit)
43
+
44
+ visit_model = BehaviorAnalyticsVisit
45
+ visit_hash = visit_data.is_a?(Hash) ? visit_data : visit_data.to_h
46
+
47
+ visit = visit_model.find_or_initialize_by(visit_token: visit_hash[:visit_token])
48
+ visit.assign_attributes(visit_hash.except(:visit_token))
49
+ visit.save!
50
+ rescue StandardError => e
51
+ raise Error, "Failed to save visit: #{e.message}"
52
+ end
53
+
54
+ def find_active_visit(visitor_token, user_id = nil, visit_duration = 30.minutes)
55
+ return nil unless defined?(BehaviorAnalyticsVisit)
56
+
57
+ visit_model = BehaviorAnalyticsVisit
58
+ query = visit_model.where(visitor_token: visitor_token)
59
+ .where(ended_at: nil)
60
+ .where("started_at > ?", visit_duration.ago)
61
+
62
+ query = query.where(user_id: user_id) if user_id
63
+ visit = query.order(started_at: :desc).first
64
+
65
+ visit&.attributes&.symbolize_keys
66
+ end
67
+
68
+ def find_visit_by_token(visit_token)
69
+ return nil unless defined?(BehaviorAnalyticsVisit)
70
+
71
+ visit_model = BehaviorAnalyticsVisit
72
+ visit = visit_model.find_by(visit_token: visit_token)
73
+ visit&.attributes&.symbolize_keys
74
+ end
75
+
76
+ def link_user_to_visits(visitor_token, user_id)
77
+ return unless defined?(BehaviorAnalyticsVisit)
78
+
79
+ visit_model = BehaviorAnalyticsVisit
80
+ visit_model.where(visitor_token: visitor_token, user_id: nil)
81
+ .update_all(user_id: user_id, updated_at: Time.current)
82
+ end
83
+
84
+ def find_visits_by_user(user_id, limit: 100)
85
+ return [] unless defined?(BehaviorAnalyticsVisit)
86
+
87
+ visit_model = BehaviorAnalyticsVisit
88
+ visits = visit_model.where(user_id: user_id)
89
+ .order(started_at: :desc)
90
+ .limit(limit)
91
+ visits.map(&:attributes).map(&:symbolize_keys)
92
+ end
93
+
94
+ def find_visits_by_visitor(visitor_token, limit: 100)
95
+ return [] unless defined?(BehaviorAnalyticsVisit)
96
+
97
+ visit_model = BehaviorAnalyticsVisit
98
+ visits = visit_model.where(visitor_token: visitor_token)
99
+ .order(started_at: :desc)
100
+ .limit(limit)
101
+ visits.map(&:attributes).map(&:symbolize_keys)
102
+ end
103
+
39
104
  def events_for_context(context, options = {})
40
105
  context.validate!
41
106
 
@@ -142,6 +207,29 @@ module BehaviorAnalytics
142
207
  @model_class.where("created_at < ?", before_date).delete_all
143
208
  end
144
209
 
210
+ def delete_old_visits(before_date)
211
+ return 0 unless defined?(BehaviorAnalyticsVisit)
212
+
213
+ visit_model = BehaviorAnalyticsVisit
214
+ visit_model.where("started_at < ?", before_date).delete_all
215
+ end
216
+
217
+ def query_visits(context, options = {})
218
+ return [] unless defined?(BehaviorAnalyticsVisit)
219
+
220
+ visit_model = BehaviorAnalyticsVisit
221
+ query = visit_model.all
222
+
223
+ query = query.where(tenant_id: context.tenant_id) if context.has_tenant?
224
+ query = query.where(user_id: context.user_id) if context.has_user?
225
+
226
+ query = query.where("started_at >= ?", options[:since]) if options[:since]
227
+ query = query.where("started_at <= ?", options[:until]) if options[:until]
228
+ query = query.limit(options[:limit]) if options[:limit]
229
+
230
+ query.map(&:attributes).map(&:symbolize_keys)
231
+ end
232
+
145
233
  def event_count(context, options = {})
146
234
  context.validate!
147
235
  query = build_base_query(context, options)
@@ -21,6 +21,7 @@ module BehaviorAnalytics
21
21
  @schema_validator = options[:schema_validator] || BehaviorAnalytics.configuration.schema_validator
22
22
  @metrics = options[:metrics] || BehaviorAnalytics.configuration.metrics || Observability::Metrics.new
23
23
  @tracer = options[:tracer] || BehaviorAnalytics.configuration.tracer
24
+ @visit_manager = options[:visit_manager]
24
25
 
25
26
  @buffer = []
26
27
  @mutex = Mutex.new
@@ -64,7 +65,9 @@ module BehaviorAnalytics
64
65
  session_id: options[:session_id],
65
66
  ip: options[:ip],
66
67
  user_agent: options[:user_agent],
67
- duration_ms: options[:duration_ms]
68
+ duration_ms: options[:duration_ms],
69
+ visit_id: options[:visit_id],
70
+ visitor_id: options[:visitor_id]
68
71
  }
69
72
 
70
73
  # Validate schema if validator is configured
@@ -76,17 +79,6 @@ module BehaviorAnalytics
76
79
  end
77
80
 
78
81
  event = Event.new(event_data)
79
- tenant_id: context.tenant_id,
80
- user_id: context.user_id,
81
- user_type: context.user_type,
82
- event_name: event_name,
83
- event_type: event_type,
84
- metadata: metadata.merge(context.filters),
85
- session_id: options[:session_id],
86
- ip: options[:ip],
87
- user_agent: options[:user_agent],
88
- duration_ms: options[:duration_ms]
89
- )
90
82
 
91
83
  # Execute before_track hooks
92
84
  begin
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BehaviorAnalytics
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.0"
5
5
  end
@@ -27,6 +27,11 @@ require_relative "behavior_analytics/analytics/engine"
27
27
  require_relative "behavior_analytics/analytics/funnels"
28
28
  require_relative "behavior_analytics/analytics/cohorts"
29
29
  require_relative "behavior_analytics/analytics/retention"
30
+ require_relative "behavior_analytics/analytics/geographic"
31
+ require_relative "behavior_analytics/analytics/referrer"
32
+ require_relative "behavior_analytics/identification/user_resolver"
33
+ require_relative "behavior_analytics/cleanup/retention_policy"
34
+ require_relative "behavior_analytics/cleanup/scheduler"
30
35
  require_relative "behavior_analytics/hooks/manager"
31
36
  require_relative "behavior_analytics/hooks/webhook"
32
37
  require_relative "behavior_analytics/hooks/callback"
@@ -46,6 +51,12 @@ require_relative "behavior_analytics/processors/async_processor"
46
51
  require_relative "behavior_analytics/processors/background_job_processor"
47
52
  require_relative "behavior_analytics/streaming/event_stream"
48
53
 
54
+ require_relative "behavior_analytics/visits/visit"
55
+ require_relative "behavior_analytics/visits/manager"
56
+ require_relative "behavior_analytics/visits/auto_creator"
57
+ require_relative "behavior_analytics/detection/device_detector"
58
+ require_relative "behavior_analytics/detection/geolocation"
59
+
49
60
  begin
50
61
  require_relative "behavior_analytics/integrations/rails"
51
62
  rescue LoadError
@@ -95,7 +106,9 @@ module BehaviorAnalytics
95
106
  :hooks_manager, :raise_on_hook_error, :sampling_strategy, :rate_limiter,
96
107
  :schema_validator, :schema_registry, :tracking_whitelist, :tracking_blacklist,
97
108
  :skip_bots, :controller_action_filters, :slow_query_threshold, :track_middleware_requests,
98
- :metrics, :tracer, :debug_mode, :logger, :default_tenant_id
109
+ :metrics, :tracer, :debug_mode, :logger, :default_tenant_id,
110
+ :track_visits, :visit_duration, :track_geolocation, :track_device_info,
111
+ :visit_retention_days, :event_retention_days, :device_detector
99
112
 
100
113
  def initialize
101
114
  @batch_size = 100
@@ -124,6 +137,13 @@ module BehaviorAnalytics
124
137
  @debug_mode = @environment == "development"
125
138
  @logger = nil
126
139
  @default_tenant_id = "default" # Default tenant for single-tenant systems
140
+ @track_visits = false
141
+ @visit_duration = 30.minutes
142
+ @track_geolocation = false
143
+ @track_device_info = false
144
+ @visit_retention_days = 90
145
+ @event_retention_days = 365
146
+ @device_detector = :browser
127
147
  end
128
148
 
129
149
  def debug(message, context: nil)
@@ -184,5 +204,53 @@ module BehaviorAnalytics
184
204
  def create_tracker(options = {})
185
205
  Tracker.new(options)
186
206
  end
207
+
208
+ # Simplified API methods (Ahoy-inspired)
209
+ def track(event_name, properties: {}, event_type: :custom, context: nil, **options)
210
+ # Resolve context automatically if not provided
211
+ context ||= resolve_default_context
212
+
213
+ tracker = create_tracker
214
+ tracker.track(
215
+ context: context,
216
+ event_name: event_name,
217
+ event_type: event_type,
218
+ metadata: properties,
219
+ **options
220
+ )
221
+ end
222
+
223
+ def track_page_view(path:, properties: {}, **options)
224
+ track("page_view", properties: { path: path }.merge(properties), **options)
225
+ end
226
+
227
+ def track_click(element:, properties: {}, **options)
228
+ track("click", properties: { element: element }.merge(properties), **options)
229
+ end
230
+
231
+ def track_conversion(conversion_name:, value: nil, properties: {}, **options)
232
+ props = { conversion_name: conversion_name }
233
+ props[:value] = value if value
234
+ track("conversion", properties: props.merge(properties), **options)
235
+ end
236
+
237
+ def tracker
238
+ @tracker ||= create_tracker
239
+ end
240
+
241
+ private
242
+
243
+ def resolve_default_context
244
+ # Try to resolve from Rails if available
245
+ if defined?(Rails) && defined?(ActionController::Base)
246
+ # This will be called in controller context
247
+ # For now, return a basic context
248
+ Context.new(tenant_id: configuration.default_tenant_id)
249
+ else
250
+ Context.new(tenant_id: configuration.default_tenant_id)
251
+ end
252
+ end
187
253
  end
188
254
  end
255
+
256
+ require_relative "behavior_analytics/helpers/tracking_helper"
@@ -3,7 +3,7 @@
3
3
  class CreateBehaviorEvents < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
4
  def change
5
5
  create_table :behavior_events do |t|
6
- t.string :tenant_id, null: false
6
+ t.string :tenant_id # Nullable to support single-tenant and API-only tracking
7
7
  t.string :user_id
8
8
  t.string :user_type
9
9
  t.string :event_name, null: false
@@ -16,7 +16,7 @@ class CreateBehaviorEvents < ActiveRecord::Migration[<%= ActiveRecord::Migration
16
16
  t.datetime :created_at, null: false
17
17
  end
18
18
 
19
- add_index :behavior_events, :tenant_id
19
+ add_index :behavior_events, :tenant_id # Index even if nullable for multi-tenant queries
20
20
  add_index :behavior_events, :user_id
21
21
  add_index :behavior_events, :user_type
22
22
  add_index :behavior_events, :event_name
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.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nerdawey
@@ -66,9 +66,11 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.6'
69
- description: A Ruby gem for tracking user behavior events with multi-tenant support,
70
- computing analytics (engagement scores, time-based trends, feature usage), and supporting
71
- API calls, feature usage, and custom events.
69
+ description: A comprehensive Ruby gem for tracking user behavior events with multi-tenant
70
+ support, visit/session management (Ahoy-inspired), device & browser detection, geographic
71
+ analytics, referrer tracking, and advanced analytics (engagement scores, funnels,
72
+ cohorts, retention). Supports API calls, feature usage, custom events, and JavaScript
73
+ client-side tracking.
72
74
  email:
73
75
  - nerdawy@icloud.com
74
76
  executables: []
@@ -149,6 +151,6 @@ requirements: []
149
151
  rubygems_version: 3.4.19
150
152
  signing_key:
151
153
  specification_version: 4
152
- summary: Track user behavior events with flexible context filtering and comprehensive
154
+ summary: Track user behavior events with visit tracking, device detection, and comprehensive
153
155
  analytics
154
156
  test_files: []