behavior_analytics 2.1.1 → 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.
- checksums.yaml +4 -4
- data/README.md +861 -8
- data/behavior_analytics.gemspec +5 -4
- data/lib/behavior_analytics/analytics/engine.rb +10 -0
- data/lib/behavior_analytics/event.rb +6 -1
- data/lib/behavior_analytics/integrations/rails.rb +45 -28
- data/lib/behavior_analytics/storage/active_record_adapter.rb +88 -0
- data/lib/behavior_analytics/tracker.rb +4 -1
- data/lib/behavior_analytics/version.rb +1 -1
- data/lib/behavior_analytics.rb +69 -1
- metadata +7 -5
data/behavior_analytics.gemspec
CHANGED
|
@@ -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
|
|
12
|
-
spec.description = "A Ruby gem for tracking user behavior events with multi-tenant support, " \
|
|
13
|
-
"
|
|
14
|
-
"and
|
|
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
|
data/lib/behavior_analytics.rb
CHANGED
|
@@ -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"
|
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.
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
154
|
+
summary: Track user behavior events with visit tracking, device detection, and comprehensive
|
|
153
155
|
analytics
|
|
154
156
|
test_files: []
|