ahoy_analytics 0.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.
Files changed (198) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +163 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/ahoy_analytics/build/assets/Combination-BpSXUjp9.js +41 -0
  6. data/app/assets/ahoy_analytics/build/assets/analytics-5KyfCxh6.css +1 -0
  7. data/app/assets/ahoy_analytics/build/assets/analytics-dashboard-uOXx8zYZ.js +1 -0
  8. data/app/assets/ahoy_analytics/build/assets/analytics-layout-ClAft5OU.js +1 -0
  9. data/app/assets/ahoy_analytics/build/assets/analytics-tracker-B3f8P98z.js +1 -0
  10. data/app/assets/ahoy_analytics/build/assets/analytics-ui-DMSkNqd6.js +90 -0
  11. data/app/assets/ahoy_analytics/build/assets/behaviors-panel-ChNGYbdH.js +1 -0
  12. data/app/assets/ahoy_analytics/build/assets/button-JVCrlR4s.js +1 -0
  13. data/app/assets/ahoy_analytics/build/assets/cable-DO-7y1-E.js +1 -0
  14. data/app/assets/ahoy_analytics/build/assets/createLucideIcon-BGzacY2v.js +1 -0
  15. data/app/assets/ahoy_analytics/build/assets/date-range-dialog-DWDp3cLG.js +1 -0
  16. data/app/assets/ahoy_analytics/build/assets/details-button-NqKfSGEG.js +1 -0
  17. data/app/assets/ahoy_analytics/build/assets/devices-panel-cXvlmNBY.js +1 -0
  18. data/app/assets/ahoy_analytics/build/assets/dialog-path-BBPNlB4Z.js +1 -0
  19. data/app/assets/ahoy_analytics/build/assets/dropdown-menu-Adj3O5fh.js +1 -0
  20. data/app/assets/ahoy_analytics/build/assets/filter-dialog-BN-rf4lp.js +1 -0
  21. data/app/assets/ahoy_analytics/build/assets/index-B1K1NTKT.js +3 -0
  22. data/app/assets/ahoy_analytics/build/assets/index-BcHeb-Rh.js +1 -0
  23. data/app/assets/ahoy_analytics/build/assets/index-DzpzLoG4.js +1 -0
  24. data/app/assets/ahoy_analytics/build/assets/index-vX97OY1J.js +1 -0
  25. data/app/assets/ahoy_analytics/build/assets/input-e4v_v0kE.js +1 -0
  26. data/app/assets/ahoy_analytics/build/assets/jsx-runtime-u17CrQMm.js +1 -0
  27. data/app/assets/ahoy_analytics/build/assets/last-load-context-De5uA95L.js +1 -0
  28. data/app/assets/ahoy_analytics/build/assets/list-table-ChHEzzF9.js +1 -0
  29. data/app/assets/ahoy_analytics/build/assets/live-Cp2MHECh.js +2 -0
  30. data/app/assets/ahoy_analytics/build/assets/locations-panel-BaISRmaQ.js +1 -0
  31. data/app/assets/ahoy_analytics/build/assets/mercator-BnxX5RzL.js +1 -0
  32. data/app/assets/ahoy_analytics/build/assets/pages-panel-Bh25L8mP.js +1 -0
  33. data/app/assets/ahoy_analytics/build/assets/panel-tabs-B2kvGFJx.js +1 -0
  34. data/app/assets/ahoy_analytics/build/assets/query-context-B-PgE00D.js +1 -0
  35. data/app/assets/ahoy_analytics/build/assets/remote-details-dialog-DDTcKaM5.js +1 -0
  36. data/app/assets/ahoy_analytics/build/assets/show-CCRicksg.js +1 -0
  37. data/app/assets/ahoy_analytics/build/assets/simple-tabs-D6G6Bs0k.js +1 -0
  38. data/app/assets/ahoy_analytics/build/assets/site-context-BNteYRlR.js +1 -0
  39. data/app/assets/ahoy_analytics/build/assets/sources-panel-DyB21hxD.js +1 -0
  40. data/app/assets/ahoy_analytics/build/assets/top-bar-FSiLBjq6.js +1 -0
  41. data/app/assets/ahoy_analytics/build/assets/top-stats-context-DU15P9jS.js +1 -0
  42. data/app/assets/ahoy_analytics/build/assets/use-debounce-VBpXQRL8.js +1 -0
  43. data/app/assets/ahoy_analytics/build/assets/user-context-DbYteluY.js +1 -0
  44. data/app/assets/ahoy_analytics/build/assets/visitor-globe-BWLDihid.js +4789 -0
  45. data/app/assets/ahoy_analytics/build/assets/visitor-graph-uKXjLvcu.js +1 -0
  46. data/app/assets/ahoy_analytics/images/icon/browser/brave.svg +1 -0
  47. data/app/assets/ahoy_analytics/images/icon/browser/chrome.svg +1 -0
  48. data/app/assets/ahoy_analytics/images/icon/browser/chromium.svg +1 -0
  49. data/app/assets/ahoy_analytics/images/icon/browser/duckduckgo.svg +2151 -0
  50. data/app/assets/ahoy_analytics/images/icon/browser/edge.svg +1 -0
  51. data/app/assets/ahoy_analytics/images/icon/browser/fallback.svg +5 -0
  52. data/app/assets/ahoy_analytics/images/icon/browser/firefox.svg +1 -0
  53. data/app/assets/ahoy_analytics/images/icon/browser/opera.svg +1 -0
  54. data/app/assets/ahoy_analytics/images/icon/browser/safari.png +0 -0
  55. data/app/assets/ahoy_analytics/images/icon/browser/samsung-internet.svg +1 -0
  56. data/app/assets/ahoy_analytics/images/icon/browser/uc.svg +1 -0
  57. data/app/assets/ahoy_analytics/images/icon/browser/vivaldi.svg +1 -0
  58. data/app/assets/ahoy_analytics/images/icon/browser/yandex.png +0 -0
  59. data/app/assets/ahoy_analytics/images/icon/os/android.png +0 -0
  60. data/app/assets/ahoy_analytics/images/icon/os/chrome_os.png +0 -0
  61. data/app/assets/ahoy_analytics/images/icon/os/fallback.svg +5 -0
  62. data/app/assets/ahoy_analytics/images/icon/os/fedora.png +0 -0
  63. data/app/assets/ahoy_analytics/images/icon/os/freebsd.png +0 -0
  64. data/app/assets/ahoy_analytics/images/icon/os/gnu_linux.png +0 -0
  65. data/app/assets/ahoy_analytics/images/icon/os/ios.png +0 -0
  66. data/app/assets/ahoy_analytics/images/icon/os/ipad_os.png +0 -0
  67. data/app/assets/ahoy_analytics/images/icon/os/mac.png +0 -0
  68. data/app/assets/ahoy_analytics/images/icon/os/ubuntu.png +0 -0
  69. data/app/assets/ahoy_analytics/images/icon/os/windows.png +0 -0
  70. data/app/assets/stylesheets/ahoy_analytics/application.css +15 -0
  71. data/app/channels/ahoy_analytics/analytics_channel.rb +9 -0
  72. data/app/controllers/ahoy_analytics/analytics_controller.rb +9 -0
  73. data/app/controllers/ahoy_analytics/application_controller.rb +8 -0
  74. data/app/controllers/ahoy_analytics/assets_controller.rb +46 -0
  75. data/app/controllers/ahoy_analytics/base_controller.rb +285 -0
  76. data/app/controllers/ahoy_analytics/behaviors_controller.rb +14 -0
  77. data/app/controllers/ahoy_analytics/devices_controller.rb +14 -0
  78. data/app/controllers/ahoy_analytics/export_controller.rb +12 -0
  79. data/app/controllers/ahoy_analytics/live_controller.rb +9 -0
  80. data/app/controllers/ahoy_analytics/locations_controller.rb +14 -0
  81. data/app/controllers/ahoy_analytics/main_graph_controller.rb +10 -0
  82. data/app/controllers/ahoy_analytics/pages_controller.rb +14 -0
  83. data/app/controllers/ahoy_analytics/referrers_controller.rb +34 -0
  84. data/app/controllers/ahoy_analytics/search_terms_controller.rb +47 -0
  85. data/app/controllers/ahoy_analytics/sources_controller.rb +14 -0
  86. data/app/controllers/ahoy_analytics/top_stats_controller.rb +13 -0
  87. data/app/controllers/concerns/ahoy_analytics/set_current_request.rb +17 -0
  88. data/app/frontend/components/analytics/hex-highlights.tsx +165 -0
  89. data/app/frontend/components/analytics/hex-land-layer.tsx +61 -0
  90. data/app/frontend/components/analytics/metric-card.tsx +138 -0
  91. data/app/frontend/components/analytics/sessions-by-location.tsx +62 -0
  92. data/app/frontend/components/analytics/visitor-globe.tsx +424 -0
  93. data/app/frontend/components/ui/accordion.tsx +64 -0
  94. data/app/frontend/components/ui/alert.tsx +66 -0
  95. data/app/frontend/components/ui/avatar.tsx +53 -0
  96. data/app/frontend/components/ui/badge.tsx +46 -0
  97. data/app/frontend/components/ui/button.tsx +62 -0
  98. data/app/frontend/components/ui/calendar.tsx +212 -0
  99. data/app/frontend/components/ui/card.tsx +91 -0
  100. data/app/frontend/components/ui/checkbox.tsx +32 -0
  101. data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
  102. data/app/frontend/components/ui/input.tsx +21 -0
  103. data/app/frontend/components/ui/label.tsx +22 -0
  104. data/app/frontend/components/ui/popover.tsx +46 -0
  105. data/app/frontend/components/ui/select.tsx +183 -0
  106. data/app/frontend/components/ui/separator.tsx +26 -0
  107. data/app/frontend/components/ui/sheet.tsx +139 -0
  108. data/app/frontend/components/ui/sidebar.tsx +726 -0
  109. data/app/frontend/components/ui/skeleton.tsx +13 -0
  110. data/app/frontend/components/ui/sonner.tsx +33 -0
  111. data/app/frontend/components/ui/tooltip.tsx +59 -0
  112. data/app/frontend/data/countries-110m.json +1 -0
  113. data/app/frontend/data/globe-data.json +1 -0
  114. data/app/frontend/entrypoints/analytics-tracker.ts +680 -0
  115. data/app/frontend/entrypoints/analytics-ui.tsx +26 -0
  116. data/app/frontend/entrypoints/analytics.css +77 -0
  117. data/app/frontend/layouts/analytics-layout.tsx +28 -0
  118. data/app/frontend/lib/cable.ts +13 -0
  119. data/app/frontend/lib/geocode.ts +65 -0
  120. data/app/frontend/lib/utils.ts +6 -0
  121. data/app/frontend/pages/admin/analytics/api.ts +221 -0
  122. data/app/frontend/pages/admin/analytics/hooks/use-debounce.ts +36 -0
  123. data/app/frontend/pages/admin/analytics/last-load-context.tsx +29 -0
  124. data/app/frontend/pages/admin/analytics/lib/base-path.ts +28 -0
  125. data/app/frontend/pages/admin/analytics/lib/dialog-path.ts +242 -0
  126. data/app/frontend/pages/admin/analytics/lib/number-formatter.ts +100 -0
  127. data/app/frontend/pages/admin/analytics/live.tsx +608 -0
  128. data/app/frontend/pages/admin/analytics/query-context.tsx +61 -0
  129. data/app/frontend/pages/admin/analytics/show.tsx +40 -0
  130. data/app/frontend/pages/admin/analytics/site-context.tsx +22 -0
  131. data/app/frontend/pages/admin/analytics/top-stats-context.tsx +37 -0
  132. data/app/frontend/pages/admin/analytics/types.ts +161 -0
  133. data/app/frontend/pages/admin/analytics/ui/analytics-dashboard.tsx +60 -0
  134. data/app/frontend/pages/admin/analytics/ui/behaviors-panel.tsx +456 -0
  135. data/app/frontend/pages/admin/analytics/ui/date-range-dialog.tsx +173 -0
  136. data/app/frontend/pages/admin/analytics/ui/details-button.tsx +33 -0
  137. data/app/frontend/pages/admin/analytics/ui/devices-panel.tsx +474 -0
  138. data/app/frontend/pages/admin/analytics/ui/filter-dialog.tsx +558 -0
  139. data/app/frontend/pages/admin/analytics/ui/list-table.tsx +346 -0
  140. data/app/frontend/pages/admin/analytics/ui/locations-panel.tsx +566 -0
  141. data/app/frontend/pages/admin/analytics/ui/pages-panel.tsx +207 -0
  142. data/app/frontend/pages/admin/analytics/ui/panel-tabs.tsx +65 -0
  143. data/app/frontend/pages/admin/analytics/ui/remote-details-dialog.tsx +356 -0
  144. data/app/frontend/pages/admin/analytics/ui/simple-tabs.tsx +54 -0
  145. data/app/frontend/pages/admin/analytics/ui/sources-panel.tsx +771 -0
  146. data/app/frontend/pages/admin/analytics/ui/top-bar.tsx +793 -0
  147. data/app/frontend/pages/admin/analytics/ui/visitor-graph.tsx +891 -0
  148. data/app/frontend/pages/admin/analytics/user-context.tsx +22 -0
  149. data/app/frontend/styles/shared.css +156 -0
  150. data/app/helpers/ahoy_analytics/application_helper.rb +96 -0
  151. data/app/jobs/ahoy_analytics/application_job.rb +4 -0
  152. data/app/jobs/ahoy_analytics/update_job.rb +12 -0
  153. data/app/mailers/ahoy_analytics/application_mailer.rb +6 -0
  154. data/app/models/ahoy/event/filters.rb +7 -0
  155. data/app/models/ahoy/event.rb +9 -0
  156. data/app/models/ahoy/visit/cache_key.rb +15 -0
  157. data/app/models/ahoy/visit/constants.rb +11 -0
  158. data/app/models/ahoy/visit/devices.rb +144 -0
  159. data/app/models/ahoy/visit/export.rb +24 -0
  160. data/app/models/ahoy/visit/filters.rb +286 -0
  161. data/app/models/ahoy/visit/imports.rb +36 -0
  162. data/app/models/ahoy/visit/locations.rb +276 -0
  163. data/app/models/ahoy/visit/metrics.rb +473 -0
  164. data/app/models/ahoy/visit/ordering.rb +110 -0
  165. data/app/models/ahoy/visit/pages.rb +533 -0
  166. data/app/models/ahoy/visit/pagination.rb +17 -0
  167. data/app/models/ahoy/visit/ranges.rb +227 -0
  168. data/app/models/ahoy/visit/series.rb +177 -0
  169. data/app/models/ahoy/visit/sources.rb +418 -0
  170. data/app/models/ahoy/visit/url_labels.rb +32 -0
  171. data/app/models/ahoy/visit.rb +143 -0
  172. data/app/models/ahoy_analytics/application_record.rb +5 -0
  173. data/app/models/ahoy_analytics/current.rb +8 -0
  174. data/app/models/ahoy_analytics/funnel.rb +16 -0
  175. data/app/models/ahoy_analytics/imported_entry_page.rb +5 -0
  176. data/app/models/ahoy_analytics/imported_exit_page.rb +5 -0
  177. data/app/models/ahoy_analytics/imported_page.rb +5 -0
  178. data/app/models/ahoy_analytics/live_stats.rb +152 -0
  179. data/app/models/ahoy_analytics/setting.rb +19 -0
  180. data/app/models/analytics/source_catalog.rb +48 -0
  181. data/app/views/layouts/ahoy_analytics/application.html.erb +15 -0
  182. data/config/routes.rb +21 -0
  183. data/config/vite.json +22 -0
  184. data/db/migrate/20251006104056_create_ahoy_visits_and_events.rb +62 -0
  185. data/db/migrate/20251006105012_add_analytics_fields_to_ahoy_visits.rb +11 -0
  186. data/db/migrate/20251012090000_create_analytics_funnels_and_imports.rb +52 -0
  187. data/db/migrate/20251013021500_add_analytics_indexes.rb +14 -0
  188. data/lib/ahoy_analytics/ahoy_store.rb +429 -0
  189. data/lib/ahoy_analytics/asset_manifest.rb +56 -0
  190. data/lib/ahoy_analytics/device_bucket.rb +39 -0
  191. data/lib/ahoy_analytics/engine.rb +55 -0
  192. data/lib/ahoy_analytics/maxmind_geo.rb +77 -0
  193. data/lib/ahoy_analytics/version.rb +3 -0
  194. data/lib/ahoy_analytics.rb +52 -0
  195. data/lib/generators/ahoy_analytics/install/install_generator.rb +111 -0
  196. data/lib/generators/ahoy_analytics/install/templates/initializer.rb +28 -0
  197. data/lib/tasks/ahoy_analytics_tasks.rake +4 -0
  198. metadata +352 -0
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhoyAnalytics
4
+ class LiveStats
5
+ LIVE_WINDOW = 5.minutes
6
+
7
+ def self.build(now: Time.zone.now)
8
+ new(now).build
9
+ end
10
+
11
+ def initialize(now)
12
+ @now = now
13
+ end
14
+
15
+ def build
16
+ today_range = today
17
+ yesterday_range = yesterday
18
+
19
+ current_visitors = Ahoy::Visit.live_visitors_count
20
+
21
+ today_sessions = Ahoy::Visit.where(started_at: today_range).count
22
+ yesterday_sessions = Ahoy::Visit.where(started_at: yesterday_range).count
23
+
24
+ today_visitors = Ahoy::Visit.where(started_at: today_range).distinct.count(:visitor_token)
25
+ yesterday_visitors = Ahoy::Visit.where(started_at: yesterday_range).distinct.count(:visitor_token)
26
+
27
+ today_pageviews = Ahoy::Event.where(name: "pageview", time: today_range).count
28
+ yesterday_pageviews = Ahoy::Event.where(name: "pageview", time: yesterday_range).count
29
+
30
+ buckets = 1.hour
31
+ session_spark = Ahoy::Visit.sparkline_today_vs_yesterday(bucket: buckets, now: now, yesterday_full_day: true)
32
+ pageview_spark = sparkline_events_today_vs_yesterday(bucket: buckets, now: now, yesterday_full_day: true)
33
+
34
+ {
35
+ currentVisitors: current_visitors,
36
+ todayVisitors: {
37
+ count: today_visitors,
38
+ change: pct_change(yesterday_visitors, today_visitors)
39
+ },
40
+ todaySessions: {
41
+ count: today_sessions,
42
+ change: pct_change(yesterday_sessions, today_sessions),
43
+ sparkline: session_spark
44
+ },
45
+ todayPageviews: {
46
+ count: today_pageviews,
47
+ change: pct_change(yesterday_pageviews, today_pageviews),
48
+ sparkline: pageview_spark
49
+ },
50
+ sessionsByLocation: sessions_by_location(today_range),
51
+ visitorDots: visitor_dots
52
+ }
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :now
58
+
59
+ def today
60
+ now.beginning_of_day..now
61
+ end
62
+
63
+ def yesterday
64
+ (today.begin - 1.day)..today.begin
65
+ end
66
+
67
+ def sessions_by_location(range)
68
+ Ahoy::Visit
69
+ .where(started_at: range)
70
+ .group(:country, :region, :city)
71
+ .order(Arel.sql("COUNT(*) DESC"))
72
+ .limit(5)
73
+ .count
74
+ .map do |(country, region, city), count|
75
+ {
76
+ country: country.to_s,
77
+ region: region.to_s.presence,
78
+ city: city.to_s.presence,
79
+ countryCode: country.to_s,
80
+ visitors: count
81
+ }
82
+ end
83
+ end
84
+
85
+ def visitor_dots
86
+ Ahoy::Visit.live_dots(limit: 200, window: LIVE_WINDOW, now: now).sort_by { |dot| -dot[:ts].to_i }
87
+ end
88
+
89
+ def sparkline_events_today_vs_yesterday(bucket:, now:, yesterday_full_day: true)
90
+ current_time = now || Time.zone.now
91
+ start_today = current_time.beginning_of_day
92
+ bucket_seconds = bucket.to_i
93
+ bucket_count_today = (((current_time - start_today) / bucket).floor + 1)
94
+ .clamp(1, 24 * 60 * 60 / bucket_seconds)
95
+ full_day_buckets = (24 * 60 * 60) / bucket_seconds
96
+ bucket_count_yday = yesterday_full_day ? full_day_buckets : bucket_count_today
97
+
98
+ today_series = event_series_counts(
99
+ start_at: start_today,
100
+ buckets: bucket_count_today,
101
+ bucket_seconds: bucket_seconds
102
+ )
103
+
104
+ yesterday_series = event_series_counts(
105
+ start_at: start_today - 1.day,
106
+ buckets: bucket_count_yday,
107
+ bucket_seconds: bucket_seconds
108
+ )
109
+
110
+ { today: today_series, yesterday: yesterday_series }
111
+ end
112
+
113
+ def event_series_counts(start_at:, buckets:, bucket_seconds:)
114
+ finish = start_at + (buckets - 1) * bucket_seconds
115
+ sec = bucket_seconds.to_i
116
+ sec = 1 if sec <= 0
117
+ sec = 86_400 if sec > 86_400
118
+ start_ts = ActiveRecord::Base.connection.quote("#{start_at.utc.strftime('%Y-%m-%d %H:%M:%S')}+00")
119
+ finish_ts = ActiveRecord::Base.connection.quote("#{finish.utc.strftime('%Y-%m-%d %H:%M:%S')}+00")
120
+ sql = <<~SQL.squish
121
+ WITH series AS (
122
+ SELECT generate_series(
123
+ TIMESTAMPTZ #{start_ts},
124
+ TIMESTAMPTZ #{finish_ts},
125
+ INTERVAL '#{sec} seconds'
126
+ ) AS bucket
127
+ )
128
+ SELECT
129
+ s.bucket AS bucket,
130
+ COUNT(e.id) AS value
131
+ FROM series s
132
+ LEFT JOIN ahoy_events e
133
+ ON e.time >= s.bucket
134
+ AND e.time < s.bucket + INTERVAL '#{sec} seconds'
135
+ AND e.name = 'pageview'
136
+ GROUP BY s.bucket
137
+ ORDER BY s.bucket ASC
138
+ SQL
139
+
140
+ rows = ActiveRecord::Base.connection.exec_query(sql)
141
+ rows.rows.map { |(_, value)| value.to_i }
142
+ end
143
+
144
+ def pct_change(previous, current)
145
+ previous = previous.to_f
146
+ current = current.to_f
147
+ return 0 if previous <= 0
148
+
149
+ (((current - previous) / previous) * 100).round
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,19 @@
1
+ module AhoyAnalytics
2
+ class Setting < ApplicationRecord
3
+ self.table_name = "analytics_settings"
4
+
5
+ validates :key, presence: true, uniqueness: true
6
+
7
+ def self.get_bool(key, fallback: false)
8
+ rec = find_by(key: key)
9
+ return fallback if rec.nil?
10
+ ActiveModel::Type::Boolean.new.cast(rec.value)
11
+ end
12
+
13
+ def self.set_bool(key, value)
14
+ rec = find_or_initialize_by(key: key)
15
+ rec.value = ActiveModel::Type::Boolean.new.cast(value) ? "true" : "false"
16
+ rec.save!
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ module Analytics
2
+ module SourceCatalog
3
+ SOURCE_MAP = {
4
+ "Gmail" => /(^|\.)mail\.google\.com$|(^|\.)gmail\./i,
5
+ "Outlook.com" => /(^|\.)outlook\.|(^|\.)live\.|(^|\.)office\.com/i,
6
+ "Yahoo! Mail" => /(^|\.)mail\.yahoo\./i,
7
+ "Proton Mail" => /(^|\.)mail\.proton\.me$/i,
8
+ "iCloud Mail" => /(^|\.)mail\.icloud\.com$/i,
9
+ "Google" => /(^|\.)google\./i,
10
+ "Bing" => /(^|\.)bing\./i,
11
+ "DuckDuckGo" => /duckduckgo/i,
12
+ "Yahoo!" => /(^|\.)yahoo\./i,
13
+ "Baidu" => /(^|\.)baidu\./i,
14
+ "Yandex" => /(^|\.)yandex\./i,
15
+ "Ecosia" => /(^|\.)ecosia\./i,
16
+ "Qwant" => /(^|\.)qwant\./i,
17
+ "Brave" => /(^|\.)search\.brave\./i,
18
+ "AOL" => /(^|\.)aol\./i,
19
+ "Ask" => /(^|\.)ask\./i,
20
+ "Naver" => /(^|\.)naver\./i,
21
+ "Seznam" => /(^|\.)seznam\./i,
22
+ "Sogou" => /(^|\.)sogou\./i,
23
+ "Startpage" => /(^|\.)startpage\./i,
24
+ "Perplexity" => /(^|\.)perplexity\./i,
25
+ "ChatGPT" => /(^|\.)chatgpt\./i,
26
+ "Facebook" => /(^|\.)facebook\./i,
27
+ "Instagram" => /(^|\.)instagram\./i,
28
+ "Twitter" => /(^|\.)twitter\.|(^|\.)x\.com/i,
29
+ "LinkedIn" => /(^|\.)linkedin\./i,
30
+ "Pinterest" => /(^|\.)pinterest\./i,
31
+ "Reddit" => /(^|\.)reddit\./i,
32
+ "YouTube" => /(^|\.)youtube\.|(^|\.)youtu\.be/i,
33
+ "TikTok" => /(^|\.)tiktok\./i,
34
+ "WhatsApp" => /(^|\.)whatsapp\./i,
35
+ "Telegram" => /(^|\.)telegram\./i,
36
+ "Snapchat" => /(^|\.)snapchat\./i,
37
+ "Threads" => /(^|\.)threads\.net/i,
38
+ "Discord" => /(^|\.)discord\./i,
39
+ "Quora" => /(^|\.)quora\./i,
40
+ "VK" => /(^|\.)vk\.com/i,
41
+ "Weibo" => /(^|\.)weibo\./i,
42
+ "GitHub" => /(^|\.)github\./i,
43
+ "StackOverflow" => /(^|\.)stackoverflow\./i,
44
+ "Stack Overflow" => /(^|\.)stackoverflow\./i,
45
+ "Hacker News" => /(^|\.)news\.ycombinator\.com$/i
46
+ }.freeze
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Analytics</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <%= inertia_ssr_head %>
8
+ <%= ahoy_analytics_head_tags("analytics-ui") %>
9
+ </head>
10
+ <body class="min-h-dvh bg-background text-foreground">
11
+ <%= ahoy_analytics_window_config_tag %>
12
+ <%= yield %>
13
+ <%= ahoy_analytics_body_tags("analytics-ui") %>
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,21 @@
1
+ AhoyAnalytics::Engine.routes.draw do
2
+ root to: "analytics#show"
3
+ get "/live", to: "live#show", as: :live
4
+ get "/_/referrers/:source", to: "analytics#show", as: :referrers_dialog
5
+ get "/_/:dialog", to: "analytics#show", as: :dialog
6
+
7
+ resource :top_stats, only: [ :show ]
8
+ resource :main_graph, only: [ :show ], controller: "main_graph"
9
+ resources :sources, only: [ :index ]
10
+ resources :search_terms, only: [ :index ]
11
+ resources :referrers, only: [ :index ]
12
+ resources :pages, only: [ :index ]
13
+ resources :locations, only: [ :index ]
14
+ resources :devices, only: [ :index ]
15
+ resources :behaviors, only: [ :index ]
16
+ resource :export, only: [ :show ]
17
+
18
+ get "/assets/*path", to: "assets#show", as: :engine_asset
19
+ get "/build/*path", to: "assets#show", as: :engine_build_asset
20
+ get "/images/*path", to: "assets#show", as: :engine_image_asset
21
+ end
data/config/vite.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "all": {
3
+ "sourceCodeDir": "app/frontend",
4
+ "entrypointsDir": "entrypoints",
5
+ "publicDir": "app/assets/ahoy_analytics",
6
+ "publicOutputDir": "build",
7
+ "base": "/admin/analytics",
8
+ "watchAdditionalPaths": []
9
+ },
10
+ "development": {
11
+ "autoBuild": true,
12
+ "port": 3038
13
+ },
14
+ "test": {
15
+ "autoBuild": true,
16
+ "publicOutputDir": "build-test",
17
+ "port": 3039
18
+ },
19
+ "production": {
20
+ "ssrBuildEnabled": false
21
+ }
22
+ }
@@ -0,0 +1,62 @@
1
+ class CreateAhoyVisitsAndEvents < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :ahoy_visits do |t|
4
+ t.string :visit_token
5
+ t.string :visitor_token
6
+
7
+ # the rest are recommended but optional
8
+ # simply remove any you don't want
9
+
10
+ # user
11
+ t.references :user
12
+
13
+ # standard
14
+ t.string :ip
15
+ t.text :user_agent
16
+ t.text :referrer
17
+ t.string :referring_domain
18
+ t.text :landing_page
19
+
20
+ # technology
21
+ t.string :browser
22
+ t.string :os
23
+ t.string :device_type
24
+
25
+ # location
26
+ t.string :country
27
+ t.string :region
28
+ t.string :city
29
+ t.float :latitude
30
+ t.float :longitude
31
+
32
+ # utm parameters
33
+ t.string :utm_source
34
+ t.string :utm_medium
35
+ t.string :utm_term
36
+ t.string :utm_content
37
+ t.string :utm_campaign
38
+
39
+ # native apps
40
+ t.string :app_version
41
+ t.string :os_version
42
+ t.string :platform
43
+
44
+ t.datetime :started_at
45
+ end
46
+
47
+ add_index :ahoy_visits, :visit_token, unique: true
48
+ add_index :ahoy_visits, [ :visitor_token, :started_at ]
49
+
50
+ create_table :ahoy_events do |t|
51
+ t.references :visit
52
+ t.references :user
53
+
54
+ t.string :name
55
+ t.jsonb :properties
56
+ t.datetime :time
57
+ end
58
+
59
+ add_index :ahoy_events, [ :name, :time ]
60
+ add_index :ahoy_events, :properties, using: :gin, opclass: :jsonb_path_ops
61
+ end
62
+ end
@@ -0,0 +1,11 @@
1
+ class AddAnalyticsFieldsToAhoyVisits < ActiveRecord::Migration[8.1]
2
+ def change
3
+ change_table :ahoy_visits, bulk: true do |t|
4
+ t.string :screen_size
5
+ t.string :browser_version
6
+ t.string :hostname
7
+ end
8
+
9
+ add_index :ahoy_visits, :started_at unless index_exists?(:ahoy_visits, :started_at)
10
+ end
11
+ end
@@ -0,0 +1,52 @@
1
+ class CreateAnalyticsFunnelsAndImports < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :analytics_funnels do |t|
4
+ t.string :name, null: false
5
+ t.jsonb :steps, null: false, default: []
6
+ t.integer :created_by_id
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :analytics_funnels, :name, unique: true
11
+
12
+ create_table :analytics_settings do |t|
13
+ t.string :key, null: false
14
+ t.text :value
15
+ t.timestamps
16
+ end
17
+ add_index :analytics_settings, :key, unique: true
18
+
19
+ # Imported aggregates for optional GA/CSV imports. Minimal columns to support Pages/Entry/Exit parity
20
+ create_table :imported_pages do |t|
21
+ t.date :date, null: false
22
+ t.text :page, null: false
23
+ t.integer :visitors, default: 0, null: false
24
+ t.integer :pageviews, default: 0, null: false
25
+ t.integer :bounces, default: 0, null: false
26
+ t.bigint :total_time_on_page, default: 0, null: false # seconds
27
+ t.bigint :total_time_on_page_visits, default: 0, null: false
28
+ end
29
+ add_index :imported_pages, [ :date, :page ]
30
+
31
+ create_table :imported_entry_pages do |t|
32
+ t.date :date, null: false
33
+ t.text :entry_page, null: false
34
+ t.integer :visitors, default: 0, null: false
35
+ t.integer :entrances, default: 0, null: false
36
+ t.integer :bounces, default: 0, null: false
37
+ t.integer :visit_duration, default: 0, null: false # average seconds rounded
38
+ end
39
+ add_index :imported_entry_pages, [ :date, :entry_page ]
40
+
41
+ create_table :imported_exit_pages do |t|
42
+ t.date :date, null: false
43
+ t.text :exit_page, null: false
44
+ t.integer :visitors, default: 0, null: false
45
+ t.integer :exits, default: 0, null: false
46
+ t.integer :pageviews, default: 0, null: false
47
+ t.integer :bounces, default: 0, null: false
48
+ t.integer :visit_duration, default: 0, null: false # average seconds rounded
49
+ end
50
+ add_index :imported_exit_pages, [ :date, :exit_page ]
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ class AddAnalyticsIndexes < ActiveRecord::Migration[8.1]
2
+ def change
3
+ # Accelerate equality lookups on page path stored in event properties
4
+ add_index :ahoy_events,
5
+ "LOWER((properties->>'page'))",
6
+ name: "index_ahoy_events_on_lower_page"
7
+
8
+ # Speed up live map queries that require coordinates
9
+ add_index :ahoy_visits,
10
+ [ :latitude, :longitude ],
11
+ where: "latitude IS NOT NULL AND longitude IS NOT NULL",
12
+ name: "index_ahoy_visits_on_coordinates"
13
+ end
14
+ end