behavior_analytics 2.2.0 → 2.2.2

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: f432cc493196bc1c00cbdcdbc7d8e2de54271391012d138a20c7920f7126f981
4
- data.tar.gz: 0bef407d3e3d30d70449b20071b54cbf43aae1cb099f2f156d22dc1a3e41a359
3
+ metadata.gz: 658a8852d807dd03e050e0c773ff213459e168dfd37ecb14d707d381ada8362b
4
+ data.tar.gz: c7398303e1217051c62384c7ac78e6c43dc5da35ed689917d7b5bab99b4f4b41
5
5
  SHA512:
6
- metadata.gz: 05e3d818e1b1b4e431ba31e379ac7c8d79492219a0cf9504848084f938f3ce41332b4989c658d6a9d748a35d3716b231460712bc331b9f607473d552ab309b7d
7
- data.tar.gz: fa8aa9ce4b51f5a8741140803ff4f8d2b37d4843ba6d2c28088b3e1bb390fa1d47fd9e89c59cf57869d8e54168fa76bd265f45b8293dd78fadcfea27736234bb
6
+ metadata.gz: a795b6a6ca62eebe154b73a7a5b3377e1b305b7c1fce76e8da2acecc4535a69e34fe39914b4491a014929d55121848a06706f0d2c111262f4661453b08b7c4f9
7
+ data.tar.gz: 237f9c73911e9c42cf1922158e9724f094a32fffee340077046509f3b966495af9e4d1f4bf3db442e4f9ac0ccb8a688927f9bf3db5b099d0be434ff5255bb52d
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Behavior Analytics
2
2
 
3
- A comprehensive Ruby gem for tracking user behavior events with multi-tenant support, visit/session management, device detection, geographic analytics, and advanced querying capabilities. Inspired by Ahoy analytics with enterprise-grade features.
3
+ A comprehensive Ruby gem for tracking user behavior events with multi-tenant support, visit/session management, device detection, geographic analytics, and advanced querying capabilities with enterprise-grade features.
4
4
 
5
5
  ## Features
6
6
 
@@ -35,7 +35,7 @@ A comprehensive Ruby gem for tracking user behavior events with multi-tenant sup
35
35
  - **Event Streaming**: Real-time event pub/sub system
36
36
 
37
37
  ### Developer Experience
38
- - **Simplified API**: Ahoy-inspired simple tracking API with automatic context resolution
38
+ - **Simplified API**: Simple tracking API with automatic context resolution
39
39
  - **Rails Integration**: Automatic API call tracking via middleware with selective tracking
40
40
  - **Query Interface**: Fluent query builder for filtering events with advanced aggregations
41
41
  - **JavaScript Client**: Frontend tracking with automatic page views and click tracking
@@ -117,7 +117,7 @@ BehaviorAnalytics.configure do |config|
117
117
  time_in_trial: 0.1
118
118
  }
119
119
 
120
- # Visit/Session Management (Ahoy-inspired features)
120
+ # Visit/Session Management
121
121
  config.track_visits = true # Enable visit tracking
122
122
  config.visit_duration = 30.minutes # Visit expires after 30 min of inactivity
123
123
  config.track_device_info = true # Auto-detect device, browser, OS
@@ -167,9 +167,9 @@ Or use the inline script generator:
167
167
 
168
168
  ## Usage
169
169
 
170
- ### Simplified API (Ahoy-Inspired)
170
+ ### Simplified API
171
171
 
172
- The gem provides a simplified API similar to Ahoy for easy tracking:
172
+ The gem provides a simplified API for easy tracking:
173
173
 
174
174
  ```ruby
175
175
  # Simple event tracking with automatic context resolution
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = "Track user behavior events with visit tracking, device detection, and comprehensive analytics"
12
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, " \
13
+ "visit/session management, device & browser detection, geographic analytics, " \
14
14
  "referrer tracking, and advanced analytics (engagement scores, funnels, cohorts, retention). " \
15
15
  "Supports API calls, feature usage, custom events, and JavaScript client-side tracking."
16
16
  spec.homepage = "https://github.com/nerdawey/behavior_analytics"
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBehaviorVisits < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :behavior_visits do |t|
6
+ t.string :visit_token, null: false, index: true
7
+ t.string :visitor_token, null: false, index: true
8
+ t.string :tenant_id, index: true
9
+ t.string :user_id, index: true
10
+ t.string :ip
11
+ t.text :user_agent
12
+ t.string :referrer
13
+ t.string :landing_page
14
+ t.string :browser
15
+ t.string :os
16
+ t.string :device_type
17
+ t.string :country
18
+ t.string :city
19
+ t.string :utm_source
20
+ t.string :utm_medium
21
+ t.string :utm_campaign
22
+ t.string :utm_term
23
+ t.string :utm_content
24
+ t.string :referring_domain
25
+ t.string :search_keyword
26
+ t.datetime :started_at, null: false
27
+ t.datetime :ended_at
28
+ t.datetime :created_at, null: false
29
+ t.datetime :updated_at, null: false
30
+ end
31
+
32
+ add_index :behavior_visits, [:tenant_id, :started_at]
33
+ add_index :behavior_visits, [:visitor_token, :started_at]
34
+ add_index :behavior_visits, [:user_id, :started_at]
35
+ add_index :behavior_visits, [:visit_token, :visitor_token]
36
+ end
37
+ end
38
+
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddVisitFieldsToEvents < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :behavior_events, :visit_id, :string, index: true unless column_exists?(:behavior_events, :visit_id)
6
+ add_column :behavior_events, :visitor_id, :string, index: true unless column_exists?(:behavior_events, :visitor_id)
7
+
8
+ add_index :behavior_events, [:visit_id, :created_at] unless index_exists?(:behavior_events, [:visit_id, :created_at])
9
+ add_index :behavior_events, [:visitor_id, :created_at] unless index_exists?(:behavior_events, [:visitor_id, :created_at])
10
+ end
11
+ end
12
+
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Analytics
5
+ class Geographic
6
+ attr_reader :storage_adapter
7
+
8
+ def initialize(storage_adapter:)
9
+ @storage_adapter = storage_adapter
10
+ end
11
+
12
+ def country_breakdown(context, options = {})
13
+ return [] unless storage_adapter.respond_to?(:query_visits)
14
+
15
+ visits = storage_adapter.query_visits(context, options)
16
+ visits.group_by { |v| v[:country] }
17
+ .map { |country, visits| { country: country, count: visits.size } }
18
+ .sort_by { |r| -r[:count] }
19
+ end
20
+
21
+ def city_breakdown(context, options = {})
22
+ return [] unless storage_adapter.respond_to?(:query_visits)
23
+
24
+ visits = storage_adapter.query_visits(context, options)
25
+ visits.group_by { |v| v[:city] }
26
+ .map { |city, visits| { city: city, count: visits.size } }
27
+ .sort_by { |r| -r[:count] }
28
+ end
29
+
30
+ def country_city_breakdown(context, options = {})
31
+ return [] unless storage_adapter.respond_to?(:query_visits)
32
+
33
+ visits = storage_adapter.query_visits(context, options)
34
+ visits.group_by { |v| [v[:country], v[:city]] }
35
+ .map { |(country, city), visits| { country: country, city: city, count: visits.size } }
36
+ .sort_by { |r| -r[:count] }
37
+ end
38
+
39
+ def device_breakdown(context, options = {})
40
+ return [] unless storage_adapter.respond_to?(:query_visits)
41
+
42
+ visits = storage_adapter.query_visits(context, options)
43
+ visits.group_by { |v| v[:device_type] }
44
+ .map { |device, visits| { device: device, count: visits.size } }
45
+ .sort_by { |r| -r[:count] }
46
+ end
47
+
48
+ def browser_breakdown(context, options = {})
49
+ return [] unless storage_adapter.respond_to?(:query_visits)
50
+
51
+ visits = storage_adapter.query_visits(context, options)
52
+ visits.group_by { |v| v[:browser] }
53
+ .map { |browser, visits| { browser: browser, count: visits.size } }
54
+ .sort_by { |r| -r[:count] }
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BehaviorAnalytics
6
+ module Analytics
7
+ class Referrer
8
+ attr_reader :storage_adapter
9
+
10
+ def initialize(storage_adapter:)
11
+ @storage_adapter = storage_adapter
12
+ end
13
+
14
+ def source_breakdown(context, options = {})
15
+ return [] unless storage_adapter.respond_to?(:query_visits)
16
+
17
+ visits = storage_adapter.query_visits(context, options)
18
+ visits.group_by { |v| extract_source(v[:referrer]) }
19
+ .map { |source, visits| { source: source || "direct", count: visits.size } }
20
+ .sort_by { |r| -r[:count] }
21
+ end
22
+
23
+ def utm_source_breakdown(context, options = {})
24
+ return [] unless storage_adapter.respond_to?(:query_visits)
25
+
26
+ visits = storage_adapter.query_visits(context, options)
27
+ visits.group_by { |v| v[:utm_source] }
28
+ .map { |source, visits| { utm_source: source || "none", count: visits.size } }
29
+ .sort_by { |r| -r[:count] }
30
+ end
31
+
32
+ def utm_campaign_breakdown(context, options = {})
33
+ return [] unless storage_adapter.respond_to?(:query_visits)
34
+
35
+ visits = storage_adapter.query_visits(context, options)
36
+ visits.group_by { |v| v[:utm_campaign] }
37
+ .map { |campaign, visits| { utm_campaign: campaign || "none", count: visits.size } }
38
+ .sort_by { |r| -r[:count] }
39
+ end
40
+
41
+ def search_keyword_breakdown(context, options = {})
42
+ return [] unless storage_adapter.respond_to?(:query_visits)
43
+
44
+ visits = storage_adapter.query_visits(context, options)
45
+ visits.select { |v| v[:search_keyword] }
46
+ .group_by { |v| v[:search_keyword] }
47
+ .map { |keyword, visits| { keyword: keyword, count: visits.size } }
48
+ .sort_by { |r| -r[:count] }
49
+ end
50
+
51
+ def referring_domain_breakdown(context, options = {})
52
+ return [] unless storage_adapter.respond_to?(:query_visits)
53
+
54
+ visits = storage_adapter.query_visits(context, options)
55
+ visits.group_by { |v| v[:referring_domain] }
56
+ .map { |domain, visits| { domain: domain || "direct", count: visits.size } }
57
+ .sort_by { |r| -r[:count] }
58
+ end
59
+
60
+ private
61
+
62
+ def extract_source(referrer)
63
+ return nil unless referrer
64
+
65
+ uri = URI.parse(referrer) rescue nil
66
+ return nil unless uri
67
+
68
+ host = uri.host&.downcase
69
+ return "google" if host&.include?("google")
70
+ return "bing" if host&.include?("bing")
71
+ return "yahoo" if host&.include?("yahoo")
72
+ return "facebook" if host&.include?("facebook")
73
+ return "twitter" if host&.include?("twitter") || host&.include?("x.com")
74
+ return "linkedin" if host&.include?("linkedin")
75
+ return "reddit" if host&.include?("reddit")
76
+
77
+ host
78
+ end
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Cleanup
5
+ class RetentionPolicy
6
+ attr_reader :visit_retention_days, :event_retention_days
7
+
8
+ def initialize(visit_retention_days: 90, event_retention_days: 365)
9
+ @visit_retention_days = visit_retention_days
10
+ @event_retention_days = event_retention_days
11
+ end
12
+
13
+ def should_delete_visit?(visit)
14
+ return false unless visit[:started_at] || visit[:created_at]
15
+
16
+ visit_date = visit[:started_at] || visit[:created_at]
17
+ cutoff_date = visit_retention_days.days.ago
18
+
19
+ visit_date < cutoff_date
20
+ end
21
+
22
+ def should_delete_event?(event)
23
+ return false unless event[:created_at]
24
+
25
+ cutoff_date = event_retention_days.days.ago
26
+ event[:created_at] < cutoff_date
27
+ end
28
+
29
+ def visits_cutoff_date
30
+ visit_retention_days.days.ago
31
+ end
32
+
33
+ def events_cutoff_date
34
+ event_retention_days.days.ago
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Cleanup
5
+ class Scheduler
6
+ attr_reader :storage_adapter, :retention_policy
7
+
8
+ def initialize(storage_adapter:, retention_policy:)
9
+ @storage_adapter = storage_adapter
10
+ @retention_policy = retention_policy
11
+ end
12
+
13
+ def cleanup_visits
14
+ return unless storage_adapter.respond_to?(:delete_old_visits)
15
+
16
+ cutoff_date = retention_policy.visits_cutoff_date
17
+ deleted_count = storage_adapter.delete_old_visits(cutoff_date)
18
+
19
+ BehaviorAnalytics.configuration.debug("Deleted #{deleted_count} old visits", context: { cutoff_date: cutoff_date })
20
+ deleted_count
21
+ end
22
+
23
+ def cleanup_events
24
+ return unless storage_adapter.respond_to?(:delete_old_events)
25
+
26
+ cutoff_date = retention_policy.events_cutoff_date
27
+ deleted_count = storage_adapter.delete_old_events(cutoff_date)
28
+
29
+ BehaviorAnalytics.configuration.debug("Deleted #{deleted_count} old events", context: { cutoff_date: cutoff_date })
30
+ deleted_count
31
+ end
32
+
33
+ def cleanup_all
34
+ {
35
+ visits: cleanup_visits,
36
+ events: cleanup_events
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Detection
5
+ class DeviceDetector
6
+ def initialize(strategy: :simple)
7
+ @strategy = strategy
8
+ end
9
+
10
+ def detect(user_agent)
11
+ return {} unless user_agent
12
+
13
+ case @strategy
14
+ when :browser
15
+ detect_with_browser_gem(user_agent)
16
+ when :user_agent_parser
17
+ detect_with_user_agent_parser(user_agent)
18
+ else
19
+ detect_simple(user_agent)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def detect_simple(user_agent)
26
+ ua = user_agent.downcase
27
+ result = {
28
+ browser: detect_browser(ua),
29
+ os: detect_os(ua),
30
+ device_type: detect_device_type(ua)
31
+ }
32
+ result.compact
33
+ end
34
+
35
+ def detect_browser(ua)
36
+ return "Chrome" if ua.include?("chrome") && !ua.include?("edg")
37
+ return "Safari" if ua.include?("safari") && !ua.include?("chrome")
38
+ return "Firefox" if ua.include?("firefox")
39
+ return "Edge" if ua.include?("edg")
40
+ return "Opera" if ua.include?("opera") || ua.include?("opr")
41
+ return "Internet Explorer" if ua.include?("msie") || ua.include?("trident")
42
+ "Unknown"
43
+ end
44
+
45
+ def detect_os(ua)
46
+ return "iOS" if ua.include?("iphone") || ua.include?("ipad") || ua.include?("ipod")
47
+ return "Android" if ua.include?("android")
48
+ return "Windows" if ua.include?("windows")
49
+ return "Mac OS" if ua.include?("mac os") || ua.include?("macintosh")
50
+ return "Linux" if ua.include?("linux")
51
+ return "Unix" if ua.include?("unix")
52
+ "Unknown"
53
+ end
54
+
55
+ def detect_device_type(ua)
56
+ ua_lower = ua.downcase
57
+ return "mobile" if ua_lower.include?("mobile") || ua_lower.include?("iphone") || ua_lower.include?("android")
58
+ return "tablet" if ua_lower.include?("tablet") || ua_lower.include?("ipad")
59
+ return "desktop" if ua_lower.include?("windows") || ua_lower.include?("mac") || ua_lower.include?("linux")
60
+ "unknown"
61
+ end
62
+
63
+ def detect_with_browser_gem(user_agent)
64
+ begin
65
+ require "browser"
66
+ browser = Browser.new(user_agent)
67
+ {
68
+ browser: browser.name,
69
+ browser_version: browser.version,
70
+ os: browser.platform.name,
71
+ os_version: browser.platform.version,
72
+ device_type: browser.device.mobile? ? "mobile" : (browser.device.tablet? ? "tablet" : "desktop")
73
+ }
74
+ rescue LoadError, StandardError
75
+ detect_simple(user_agent)
76
+ end
77
+ end
78
+
79
+ def detect_with_user_agent_parser(user_agent)
80
+ begin
81
+ require "user_agent_parser"
82
+ parser = UserAgentParser.parse(user_agent)
83
+ {
84
+ browser: parser.family,
85
+ browser_version: parser.version.to_s,
86
+ os: parser.os&.family,
87
+ os_version: parser.os&.version&.to_s,
88
+ device_type: detect_device_type_from_parser(parser)
89
+ }
90
+ rescue LoadError, StandardError
91
+ detect_simple(user_agent)
92
+ end
93
+ end
94
+
95
+ def detect_device_type_from_parser(parser)
96
+ device = parser.device
97
+ return "mobile" if device&.family&.downcase&.include?("mobile")
98
+ return "tablet" if device&.family&.downcase&.include?("tablet")
99
+ "desktop"
100
+ end
101
+ end
102
+ end
103
+ end
104
+
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Detection
5
+ class Geolocation
6
+ def initialize(strategy: :simple)
7
+ @strategy = strategy
8
+ end
9
+
10
+ def detect(ip_address)
11
+ return {} unless ip_address
12
+ return {} if ip_address == "127.0.0.1" || ip_address == "::1" || ip_address.start_with?("192.168.") || ip_address.start_with?("10.")
13
+
14
+ case @strategy
15
+ when :geocoder
16
+ detect_with_geocoder(ip_address)
17
+ when :maxmind
18
+ detect_with_maxmind(ip_address)
19
+ else
20
+ detect_simple(ip_address)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def detect_simple(ip_address)
27
+ # Simple detection - returns empty for now
28
+ # In production, you'd want to use a proper geolocation service
29
+ {}
30
+ end
31
+
32
+ def detect_with_geocoder(ip_address)
33
+ begin
34
+ require "geocoder"
35
+ result = Geocoder.search(ip_address).first
36
+ return {} unless result
37
+
38
+ {
39
+ country: result.country,
40
+ country_code: result.country_code,
41
+ city: result.city,
42
+ region: result.region,
43
+ latitude: result.latitude,
44
+ longitude: result.longitude,
45
+ timezone: result.data&.dig("timezone")
46
+ }.compact
47
+ rescue LoadError, StandardError => e
48
+ BehaviorAnalytics.configuration.log_error(e, context: { ip: ip_address }) if BehaviorAnalytics.configuration.debug_mode
49
+ {}
50
+ end
51
+ end
52
+
53
+ def detect_with_maxmind(ip_address)
54
+ begin
55
+ require "maxminddb"
56
+ # This would require MaxMind GeoIP2 database
57
+ # For now, return empty - users can implement their own
58
+ {}
59
+ rescue LoadError, StandardError => e
60
+ BehaviorAnalytics.configuration.log_error(e, context: { ip: ip_address }) if BehaviorAnalytics.configuration.debug_mode
61
+ {}
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Helpers
5
+ module TrackingHelper
6
+ def track_event(event_name, properties: {}, event_type: :custom, **options)
7
+ context = resolve_tracking_context
8
+ return unless context&.valid?
9
+
10
+ tracker.track(
11
+ context: context,
12
+ event_name: event_name,
13
+ event_type: event_type,
14
+ metadata: properties,
15
+ **options
16
+ )
17
+ end
18
+
19
+ def track_page_view(path: nil, properties: {}, **options)
20
+ path ||= request.path if respond_to?(:request)
21
+ track_event("page_view", properties: { path: path }.merge(properties), **options)
22
+ end
23
+
24
+ def track_click(element:, properties: {}, **options)
25
+ track_event("click", properties: { element: element }.merge(properties), **options)
26
+ end
27
+
28
+ def track_form_submit(form_name:, properties: {}, **options)
29
+ track_event("form_submit", properties: { form_name: form_name }.merge(properties), **options)
30
+ end
31
+
32
+ def track_conversion(conversion_name:, value: nil, properties: {}, **options)
33
+ props = { conversion_name: conversion_name }
34
+ props[:value] = value if value
35
+ track_event("conversion", properties: props.merge(properties), **options)
36
+ end
37
+
38
+ private
39
+
40
+ def resolve_tracking_context
41
+ # Try to resolve context from current request/controller
42
+ if respond_to?(:current_user, true)
43
+ Context.new(
44
+ tenant_id: respond_to?(:current_tenant, true) ? current_tenant&.id : BehaviorAnalytics.configuration.default_tenant_id,
45
+ user_id: current_user&.id,
46
+ user_type: current_user&.account_type || current_user&.user_type
47
+ )
48
+ elsif respond_to?(:request, true) && request.respond_to?(:remote_ip)
49
+ # Anonymous tracking
50
+ Context.new(
51
+ tenant_id: BehaviorAnalytics.configuration.default_tenant_id
52
+ )
53
+ else
54
+ # Fallback to default context
55
+ Context.new(
56
+ tenant_id: BehaviorAnalytics.configuration.default_tenant_id
57
+ )
58
+ end
59
+ end
60
+
61
+ def tracker
62
+ @tracker ||= BehaviorAnalytics.create_tracker
63
+ end
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorAnalytics
4
+ module Identification
5
+ class UserResolver
6
+ attr_reader :visit_manager
7
+
8
+ def initialize(visit_manager:)
9
+ @visit_manager = visit_manager
10
+ end
11
+
12
+ def identify_user(user_id, visitor_token: nil, request: nil)
13
+ return unless user_id
14
+
15
+ # If visitor_token is provided, link all anonymous visits to this user
16
+ if visitor_token
17
+ visit_manager.link_user_to_visits(visitor_token, user_id)
18
+ elsif request
19
+ # Try to get visitor token from request
20
+ visitor_token = get_visitor_token_from_request(request)
21
+ visit_manager.link_user_to_visits(visitor_token, user_id) if visitor_token
22
+ end
23
+ end
24
+
25
+ def merge_visits(visitor_token, user_id)
26
+ visit_manager.link_user_to_visits(visitor_token, user_id)
27
+ end
28
+
29
+ def get_visitor_token_from_request(request)
30
+ # Try cookie first
31
+ if request.respond_to?(:cookies)
32
+ token = request.cookies["behavior_visitor_token"]
33
+ return token if token && !token.empty?
34
+ end
35
+
36
+ # Try header
37
+ if request.respond_to?(:headers)
38
+ token = request.headers["X-Visitor-Token"]
39
+ return token if token && !token.empty?
40
+ end
41
+
42
+ nil
43
+ end
44
+
45
+ def get_user_visits(user_id, limit: 100)
46
+ visit_manager.find_visits_by_user(user_id, limit: limit)
47
+ end
48
+
49
+ def get_visitor_visits(visitor_token, limit: 100)
50
+ visit_manager.find_visits_by_visitor(visitor_token, limit: limit)
51
+ end
52
+ end
53
+ end
54
+ end
55
+