ahoy_captain 0.1.0 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +55 -14
  4. data/Rakefile +1 -10
  5. data/app/assets/images/ahoy_captain/apple-touch-icon.png +0 -0
  6. data/app/assets/images/ahoy_captain/favicon-16x16.png +0 -0
  7. data/app/assets/images/ahoy_captain/favicon-32x32.png +0 -0
  8. data/app/assets/images/ahoy_captain/logo.png +0 -0
  9. data/app/assets/images/ahoy_captain/safari-pinned-tab.svg +199 -0
  10. data/app/assets/javascript/ahoy_captain/application.js +4 -0
  11. data/app/assets/javascript/ahoy_captain/controllers/application.js +9 -0
  12. data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +14 -0
  13. data/app/assets/javascript/ahoy_captain/controllers/details_modal_controller.js +18 -0
  14. data/app/assets/javascript/ahoy_captain/controllers/dropdown_label_controller.js +14 -0
  15. data/app/assets/javascript/ahoy_captain/controllers/filter_controller.js +145 -0
  16. data/app/assets/javascript/ahoy_captain/controllers/filter_tag_controller.js +17 -0
  17. data/app/assets/javascript/ahoy_captain/controllers/funnel_chart_controller.js +117 -0
  18. data/app/assets/javascript/ahoy_captain/controllers/index.js +3 -0
  19. data/app/assets/javascript/ahoy_captain/controllers/interval_controller.js +10 -0
  20. data/app/assets/javascript/ahoy_captain/controllers/link_controller.js +43 -0
  21. data/app/assets/javascript/ahoy_captain/controllers/navigation_controller.js +25 -0
  22. data/app/assets/javascript/ahoy_captain/controllers/realtime_controller.js +27 -0
  23. data/app/assets/manifest/ahoy_captain/manifest.js +2 -0
  24. data/app/components/ahoy_captain/dropdown_button_component.html.erb +16 -0
  25. data/app/components/ahoy_captain/dropdown_button_component.rb +14 -0
  26. data/app/components/ahoy_captain/dropdown_link_component.html.erb +19 -0
  27. data/app/components/ahoy_captain/dropdown_link_component.rb +15 -0
  28. data/app/components/ahoy_captain/filter/modal_component.html.erb +13 -0
  29. data/app/components/ahoy_captain/filter/modal_component.rb +13 -0
  30. data/app/components/ahoy_captain/filter/select_component.html.erb +21 -0
  31. data/app/components/ahoy_captain/filter/select_component.rb +32 -0
  32. data/app/components/ahoy_captain/filter/tag_component.html.erb +9 -0
  33. data/app/components/ahoy_captain/filter/tag_component.rb +38 -0
  34. data/app/components/ahoy_captain/filter/tag_container_component.html.erb +5 -0
  35. data/app/components/ahoy_captain/filter/tag_container_component.rb +13 -0
  36. data/app/components/ahoy_captain/sticky_nav_component.html.erb +37 -0
  37. data/app/components/ahoy_captain/sticky_nav_component.rb +5 -0
  38. data/app/components/ahoy_captain/table_component.html.erb +49 -0
  39. data/app/components/ahoy_captain/table_component.rb +28 -0
  40. data/app/components/ahoy_captain/tile_component.html.erb +12 -0
  41. data/app/components/ahoy_captain/tile_component.rb +16 -0
  42. data/app/components/ahoy_captain/tooltip_component.html.erb +3 -0
  43. data/app/components/ahoy_captain/tooltip_component.rb +18 -0
  44. data/app/controllers/ahoy_captain/application_controller.rb +86 -0
  45. data/app/controllers/ahoy_captain/campaigns_controller.rb +27 -0
  46. data/app/controllers/ahoy_captain/cities_controller.rb +24 -0
  47. data/app/controllers/ahoy_captain/countries_controller.rb +24 -0
  48. data/app/controllers/ahoy_captain/devices_controller.rb +23 -0
  49. data/app/controllers/ahoy_captain/entry_pages_controller.rb +21 -0
  50. data/app/controllers/ahoy_captain/exit_pages_controller.rb +20 -0
  51. data/app/controllers/ahoy_captain/filters/base_controller.rb +17 -0
  52. data/app/controllers/ahoy_captain/filters/locations_controller.rb +11 -0
  53. data/app/controllers/ahoy_captain/filters/operating_systems/names_controller.rb +13 -0
  54. data/app/controllers/ahoy_captain/filters/operating_systems/versions_controller.rb +13 -0
  55. data/app/controllers/ahoy_captain/filters/pages/actions_controller.rb +13 -0
  56. data/app/controllers/ahoy_captain/filters/pages/entry_pages_controller.rb +14 -0
  57. data/app/controllers/ahoy_captain/filters/pages/exit_pages_controller.rb +16 -0
  58. data/app/controllers/ahoy_captain/filters/screens_controller.rb +11 -0
  59. data/app/controllers/ahoy_captain/filters/sources_controller.rb +11 -0
  60. data/app/controllers/ahoy_captain/filters/utms_controller.rb +10 -0
  61. data/app/controllers/ahoy_captain/funnels_controller.rb +8 -0
  62. data/app/controllers/ahoy_captain/goals_controller.rb +7 -0
  63. data/app/controllers/ahoy_captain/realtimes_controller.rb +7 -0
  64. data/app/controllers/ahoy_captain/regions_controller.rb +24 -0
  65. data/app/controllers/ahoy_captain/roots_controller.rb +6 -0
  66. data/app/controllers/ahoy_captain/sources_controller.rb +24 -0
  67. data/app/controllers/ahoy_captain/stats/base_controller.rb +67 -0
  68. data/app/controllers/ahoy_captain/stats/bounce_rates_controller.rb +11 -0
  69. data/app/controllers/ahoy_captain/stats/total_pageviews_controller.rb +9 -0
  70. data/app/controllers/ahoy_captain/stats/total_visits_controller.rb +9 -0
  71. data/app/controllers/ahoy_captain/stats/unique_visitors_controller.rb +9 -0
  72. data/app/controllers/ahoy_captain/stats/views_per_visits_controller.rb +19 -0
  73. data/app/controllers/ahoy_captain/stats/visit_durations_controller.rb +9 -0
  74. data/app/controllers/ahoy_captain/stats_controller.rb +7 -0
  75. data/app/controllers/ahoy_captain/top_pages_controller.rb +26 -0
  76. data/app/decorators/ahoy_captain/application_decorator.rb +34 -0
  77. data/app/decorators/ahoy_captain/campaign_decorator.rb +19 -0
  78. data/app/decorators/ahoy_captain/city_decorator.rb +12 -0
  79. data/app/decorators/ahoy_captain/country_decorator.rb +28 -0
  80. data/app/decorators/ahoy_captain/device_decorator.rb +16 -0
  81. data/app/decorators/ahoy_captain/entry_page_decorator.rb +7 -0
  82. data/app/decorators/ahoy_captain/exit_page_decorator.rb +7 -0
  83. data/app/decorators/ahoy_captain/page_decorator.rb +16 -0
  84. data/app/decorators/ahoy_captain/region_decorator.rb +12 -0
  85. data/app/decorators/ahoy_captain/source_decorator.rb +20 -0
  86. data/app/decorators/ahoy_captain/top_page_decorator.rb +7 -0
  87. data/app/helpers/ahoy_captain/application_helper.rb +32 -0
  88. data/app/models/ahoy_captain/current.rb +9 -0
  89. data/app/models/ahoy_captain/rangeable.rb +10 -0
  90. data/app/models/ahoy_captain/url_helpers.rb +6 -0
  91. data/app/models/ahoy_captain/widget.rb +15 -0
  92. data/app/models/concerns/ahoy_captain/range_options.rb +21 -0
  93. data/app/presenters/ahoy_captain/dashboard_presenter.rb +84 -0
  94. data/app/presenters/ahoy_captain/funnel_presenter.rb +68 -0
  95. data/app/presenters/ahoy_captain/goals_presenter.rb +69 -0
  96. data/app/queries/ahoy_captain/application_query.rb +121 -0
  97. data/app/queries/ahoy_captain/entry_pages_query.rb +17 -0
  98. data/app/queries/ahoy_captain/event_query.rb +35 -0
  99. data/app/queries/ahoy_captain/exit_pages_query.rb +17 -0
  100. data/app/queries/ahoy_captain/stats/average_views_per_visit_query.rb +13 -0
  101. data/app/queries/ahoy_captain/stats/average_visit_duration_query.rb +11 -0
  102. data/app/queries/ahoy_captain/stats/bounce_rates_query.rb +17 -0
  103. data/app/queries/ahoy_captain/stats/total_pageviews_query.rb +9 -0
  104. data/app/queries/ahoy_captain/stats/total_visitors_query.rb +9 -0
  105. data/app/queries/ahoy_captain/stats/unique_visitors_query.rb +9 -0
  106. data/app/queries/ahoy_captain/stats/views_per_visit_query.rb +17 -0
  107. data/app/queries/ahoy_captain/stats/visit_duration_query.rb +16 -0
  108. data/app/queries/ahoy_captain/visit_query.rb +32 -0
  109. data/app/views/ahoy_captain/campaigns/index.html+details.erb +4 -0
  110. data/app/views/ahoy_captain/campaigns/index.html.erb +3 -0
  111. data/app/views/ahoy_captain/cities/index.html+details.erb +4 -0
  112. data/app/views/ahoy_captain/cities/index.html.erb +3 -0
  113. data/app/views/ahoy_captain/countries/index.html+details.erb +5 -0
  114. data/app/views/ahoy_captain/countries/index.html.erb +3 -0
  115. data/app/views/ahoy_captain/devices/index.html+details.erb +4 -0
  116. data/app/views/ahoy_captain/devices/index.html.erb +3 -0
  117. data/app/views/ahoy_captain/entry_pages/index.html+details.erb +4 -0
  118. data/app/views/ahoy_captain/entry_pages/index.html.erb +3 -0
  119. data/app/views/ahoy_captain/exit_pages/index.html+details.erb +4 -0
  120. data/app/views/ahoy_captain/exit_pages/index.html.erb +3 -0
  121. data/app/views/ahoy_captain/funnels/index.html.erb +7 -0
  122. data/app/views/ahoy_captain/funnels/show.html.erb +6 -0
  123. data/app/views/ahoy_captain/goals/index.html.erb +39 -0
  124. data/app/views/ahoy_captain/layouts/application.html.erb +127 -0
  125. data/app/views/ahoy_captain/realtimes/show.html.erb +9 -0
  126. data/app/views/ahoy_captain/regions/index.html+details.erb +4 -0
  127. data/app/views/ahoy_captain/regions/index.html.erb +3 -0
  128. data/app/views/ahoy_captain/roots/show.html.erb +179 -0
  129. data/app/views/ahoy_captain/sources/index.html+details.erb +4 -0
  130. data/app/views/ahoy_captain/sources/index.html.erb +3 -0
  131. data/app/views/ahoy_captain/stats/base/index.html.erb +14 -0
  132. data/app/views/ahoy_captain/stats/show.html.erb +57 -0
  133. data/app/views/ahoy_captain/top_pages/index.html+details.erb +4 -0
  134. data/app/views/ahoy_captain/top_pages/index.html.erb +3 -0
  135. data/config/routes.rb +56 -0
  136. data/lib/ahoy_captain/active_record.rb +108 -0
  137. data/lib/ahoy_captain/ahoy/event_methods.rb +114 -0
  138. data/lib/ahoy_captain/ahoy/visit_methods.rb +23 -0
  139. data/lib/ahoy_captain/configuration.rb +43 -0
  140. data/lib/ahoy_captain/engine.rb +24 -0
  141. data/lib/ahoy_captain/funnels.rb +44 -0
  142. data/lib/ahoy_captain/goals.rb +43 -0
  143. data/lib/ahoy_captain/period_collection.rb +115 -0
  144. data/lib/ahoy_captain/railtie.rb +7 -0
  145. data/lib/ahoy_captain/version.rb +1 -3
  146. data/lib/ahoy_captain.rb +50 -4
  147. data/lib/generators/ahoy_captain/install_generator.rb +18 -0
  148. data/lib/generators/ahoy_captain/migration_generator.rb +21 -0
  149. data/lib/generators/ahoy_captain/templates/config.rb.tt +138 -0
  150. data/lib/generators/ahoy_captain/templates/migration.rb.tt +7 -0
  151. metadata +410 -17
  152. data/.rspec +0 -3
  153. data/.rubocop.yml +0 -13
  154. data/CHANGELOG.md +0 -5
  155. data/Gemfile +0 -12
  156. data/ahoy_captain.gemspec +0 -37
  157. data/sig/ahoy_captain.rbs +0 -4
@@ -0,0 +1,179 @@
1
+ <main class='bg-base-300 min-h-screen pb-4 max-w-6xl mx-auto' data-controller="application">
2
+ <%= render AhoyCaptain::StickyNavComponent.new do |nav| %>
3
+ <% nav.with_realtime_update do %>
4
+ <%= turbo_frame_tag :realtime, src: realtime_path, data: { controller: "realtime" }, loading: :lazy %>
5
+ <% end %>
6
+ <% end %>
7
+
8
+ <div class="grid grid-cols-1 lg:grid-cols-2 grid-flow-row gap-4">
9
+ <%= render AhoyCaptain::TileComponent.new(wide: true) do |component| %>
10
+ <% component.with_statistic_display do %>
11
+ <%= turbo_frame_tag :stats, src: stats_path(search_params), loading: :lazy %>
12
+ <% end %>
13
+ <% end %>
14
+
15
+ <%= render AhoyCaptain::TileComponent.new(title: 'Top Sources') do |component| %>
16
+ <% component.with_display_links do %>
17
+ <div data-controller='link' data-link-top_sources-class="text-primary underline">
18
+ <a href="<%= sources_path(search_params) %>" data-turbo-frame="sources" data-action='click->link#changeTopSources' data-link-target='top_sourcesLink'>All</a>
19
+ <%= render AhoyCaptain::DropdownLinkComponent.new(title: "Campaign") do |dropdown| %>
20
+ <% %w{utm_source utm_medium utm_term utm_content utm_campaign}.each do |source| %>
21
+ <% dropdown.with_option do %>
22
+ <a href="<%= public_send("campaign_#{source}_path".to_sym, **search_params) %>" data-turbo-frame="sources" data-action='click->link#changeTopSources' data-link-target='top_sourcesLink'>
23
+ <%= source.titleize.gsub("Utm", "UTM") %>
24
+ </a>
25
+ <% end %>
26
+ <% end %>
27
+ <% end %>
28
+ </div>
29
+ <% end %>
30
+ <% component.with_statistic_display do %>
31
+ <%= turbo_frame_tag :sources, src: sources_path(search_params), loading: :lazy %>
32
+ <% end %>
33
+ <% component.with_details_cta do %>
34
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#sources" class="link no-underline">Details</button>
35
+ <% end %>
36
+ <% end %>
37
+
38
+ <%= render AhoyCaptain::TileComponent.new(title: 'Top Pages') do |component| %>
39
+ <% component.with_display_links do %>
40
+ <div data-controller='link' data-link-top_pages-class="text-primary underline" >
41
+ <a href="<%= top_pages_path(search_params) %>" data-turbo-frame="pages" data-action='click->link#changeTopPages' data-link-target='top_pagesLink'>Top Pages</a>
42
+ <a href="<%= entry_pages_path(search_params) %>" data-turbo-frame="pages" data-action='click->link#changeTopPages' data-link-target='top_pagesLink'>Entry Pages</a>
43
+ <a href="<%= exit_pages_path(search_params) %>" data-turbo-frame="pages" data-action='click->link#changeTopPages' data-link-target='top_pagesLink'>Exit Pages</a>
44
+ </div>
45
+ <% end %>
46
+ <% component.with_statistic_display do %>
47
+ <%= turbo_frame_tag :pages, src: top_pages_path(search_params), loading: :lazy %>
48
+ <% end %>
49
+ <% component.with_details_cta do %>
50
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#pages" class="link no-underline">Details</button>
51
+ <% end %>
52
+ <% end %>
53
+
54
+ <%= render AhoyCaptain::TileComponent.new(title: 'Countries') do |component| %>
55
+ <% component.with_display_links do %>
56
+ <div data-controller='link' data-link-countries-class="text-primary underline">
57
+ <a href="<%= countries_path(search_params) %>" data-turbo-frame="geography" data-action='click->link#changeCountries' data-link-target='countriesLink'>Countries</a>
58
+ <a href="<%= regions_path(search_params) %>" data-turbo-frame="geography" data-action='click->link#changeCountries' data-link-target='countriesLink'>Regions</a>
59
+ <a href="<%= cities_path(search_params) %>" data-turbo-frame="geography" data-action='click->link#changeCountries' data-link-target='countriesLink'>Cities</a>
60
+ </div>
61
+ <% end %>
62
+ <% component.with_statistic_display do %>
63
+ <%= turbo_frame_tag :geography, src: countries_path(search_params), loading: :lazy %>
64
+ <% end %>
65
+ <% component.with_details_cta do %>
66
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#geography" class="link no-underline">Details</button>
67
+ <% end %>
68
+ <% end %>
69
+
70
+ <%= render AhoyCaptain::TileComponent.new(title: 'Devices') do |component| %>
71
+ <% component.with_display_links do %>
72
+ <div data-controller='link' data-link-devices-class="text-primary underline">
73
+ <a href="<%= devices_browsers_path(search_params) %>" data-turbo-frame="devices" data-action='click->link#changeDevices' data-link-target='devicesLink'>Browser</a>
74
+ <a href="<%= devices_operating_systems_path(search_params) %>" data-turbo-frame="devices" data-action='click->link#changeDevices' data-link-target='devicesLink'>OS</a>
75
+ <a href="<%= devices_device_types_path(search_params) %>" data-turbo-frame="devices" data-action='click->link#changeDevices' data-link-target='devicesLink'>Size</a>
76
+ </div>
77
+ <% end %>
78
+ <% component.with_statistic_display do %>
79
+ <%= turbo_frame_tag :devices, src: devices_browsers_path(search_params), loading: :lazy %>
80
+ <% end %>
81
+ <% component.with_details_cta do %>
82
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#devices" class="link no-underline">Details</button>
83
+ <% end %>
84
+ <% end %>
85
+ <%= render AhoyCaptain::TileComponent.new(wide: true) do |component| %>
86
+ <% component.with_display_links do %>
87
+ <a href="<%= goals_path(search_params) %>" data-turbo-frame="goals" class="link link-primary">
88
+ Goals
89
+ </a>
90
+ <%= render AhoyCaptain::DropdownLinkComponent.new(title: "Funnels") do |dropdown| %>
91
+ <% AhoyCaptain.config.funnels.each do |id, funnel| %>
92
+ <% dropdown.with_option do %>
93
+ <a href="<%= funnel_path(id, search_params) %>" data-turbo-frame="goals" class="link link-primary">
94
+ <%= funnel.title %>
95
+ </a>
96
+ <% end %>
97
+ <% end %>
98
+ <% end %>
99
+
100
+ <% end %>
101
+ <% component.with_statistic_display do %>
102
+ <%= turbo_frame_tag :goals, src: goals_path(search_params), loading: :lazy %>
103
+ <% end %>
104
+ <% end %>
105
+ </div>
106
+ </main>
107
+
108
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Filter by Page", id: "pageModal") do |modal| %>
109
+ <% modal.with_modal_display do %>
110
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Action", column: :route, url: filters_actions_path, predicates: [:in, :not_in]) %>
111
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Entry page", column: :entry_page, url: filters_entry_pages_path, predicates: [:in, :not_in]) %>
112
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Exit page", column: :exit_page, url: filters_exit_pages_path, predicates: [:in, :not_in]) %>
113
+ <% end %>
114
+ <% end %>
115
+
116
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Filter by Country", id: "countryModal") do |modal| %>
117
+ <% modal.with_modal_display do %>
118
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Country", column: :country, url: filters_locations_countries_path, predicates: [:in, :not_in]) %>
119
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Region", column: :region, url: filters_locations_regions_path, predicates: [:in, :not_in]) %>
120
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "City", column: :city, url: filters_locations_cities_path, predicates: [:in, :not_in]) %>
121
+ <% end %>
122
+ <% end %>
123
+
124
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Filter by Source", id: "sourceModal") do |modal| %>
125
+ <% modal.with_modal_display do %>
126
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Source", column: :referring_domain, url: filters_sources_path, predicates: [:in, :not_in]) %>
127
+ <% end %>
128
+ <% end %>
129
+
130
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Filter by Screen size", id: "screenModal") do |modal| %>
131
+ <% modal.with_modal_display do %>
132
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Screen size", column: :device_type, url: filters_screens_path, predicates: [:in, :not_in]) %>
133
+ <% end %>
134
+ <% end %>
135
+
136
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Filter by Browser", id: "osModal") do |modal| %>
137
+ <% modal.with_modal_display do %>
138
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Operating System", column: :os, url: filters_names_path, predicates: [:in, :not_in]) %>
139
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "Operating System Version", column: :os_version, url: filters_versions_path, predicates: [:in, :not_in]) %>
140
+ <% end %>
141
+ <% end %>
142
+
143
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Filter by Campaign", id: "utmModal") do |modal| %>
144
+ <% modal.with_modal_display do %>
145
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "UTM Medium", column: :utm_medium, url: filters_utm_mediums_path, predicates: [:in, :not_in]) %>
146
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "UTM Source", column: :utm_source, url: filters_utm_sources_path, predicates: [:in, :not_in]) %>
147
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "UTM Campaign", column: :utm_campaign, url: filters_utm_campaigns_path, predicates: [:in, :not_in]) %>
148
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "UTM Term", column: :utm_term, url: filters_utm_terms_path, predicates: [:in, :not_in]) %>
149
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: "UTM Content", column: :utm_content, url: filters_utm_contents_path, predicates: [:in, :not_in]) %>
150
+ <% end %>
151
+ <% end %>
152
+
153
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Custom Range", id: "customRangeModal") do |modal| %>
154
+ <% modal.with_modal_display do %>
155
+ <div class="flex gap-2 w-full">
156
+ <div class="form-control w-full max-w-xs">
157
+ <label class="label">
158
+ <span class="label-text">Start Date</span>
159
+ </label>
160
+ <input type="datetime-local" name="start_date" class="input input-bordered w-full" value="<%= params[:start_date] %>" />
161
+ </div>
162
+ <div class="form-control w-full max-w-xs">
163
+ <label class="label">
164
+ <span class="label-text">End Date</span>
165
+ </label>
166
+ <input type="datetime-local" name="end_date" class="input input-bordered w-full" value="<%= params[:end_date] %>" />
167
+ </div>
168
+ </div>
169
+ <% end %>
170
+ <% end %>
171
+
172
+ <dialog id="detailsModal" class="modal">
173
+ <div class="modal-box w-11/12 max-w-5xl">
174
+ <%= turbo_frame_tag :details %>
175
+ </div>
176
+ <form method="dialog" class="modal-backdrop">
177
+ <button>close</button>
178
+ </form>
179
+ </dialog>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render AhoyCaptain::TableComponent.new(items: @sources, category_name: 'Sources', unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :sources do %>
2
+ <%= render AhoyCaptain::TableComponent.new(items: @sources, category_name: 'Sources', unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <%= turbo_frame_tag :chart do %>
2
+ <div class="flex justify-end ...">
3
+ <%= form_with url: url_for(params.permit!), method: :get, data: { controller: "interval" } do %>
4
+ <%= select_tag :interval, options_for_select(available_intervals.collect { |interval| [interval.titleize, interval] }, selected: selected_interval), class: "select select-bordered select-sm w-full max-w-sm", 'data-action': "change->interval#handleChange" %>
5
+ <% end %>
6
+
7
+ </div>
8
+
9
+ <div>
10
+
11
+
12
+ <%= line_chart @stats %>
13
+ </div>
14
+ <% end %>
@@ -0,0 +1,57 @@
1
+ <%= turbo_frame_tag :stats do %>
2
+ <dl class="grid grid-cols-1 divide-y divide-base-200 overflow-hidden rounded-lg grid-cols-2 md:grid-cols-6 md:divide-y-0 -mt-4 mb-4">
3
+ <a href="<%= stats_unique_visitors_url(search_params) %>" data-turbo-frame="chart">
4
+ <dt class="text-base font-normal text-base-content">Unique Visitors</dt>
5
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
6
+ <div class="flex items-baseline text-2xl font-semibold text-accent-content">
7
+ <%= number_with_delimiter @presenter.unique_visitors %>
8
+ </div>
9
+ </dd>
10
+ </a>
11
+ <a href="<%= stats_total_visits_path(search_params) %>" data-turbo-frame="chart">
12
+ <dt class="text-base font-normal text-base-content">Total Visits</dt>
13
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
14
+ <div class="flex items-baseline text-2xl font-semibold text-accent-content">
15
+ <%= number_with_delimiter @presenter.total_visits %>
16
+ </div>
17
+ </dd>
18
+ </a>
19
+ <a href="<%= stats_total_pageviews_path(search_params) %>" data-turbo-frame="chart">
20
+ <dt class="text-base font-normal text-base-content">Total Pageviews</dt>
21
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
22
+ <div class="flex items-baseline text-2xl font-semibold text-accent-content">
23
+ <%= number_with_delimiter @presenter.total_pageviews %>
24
+ </div>
25
+ </dd>
26
+ </a>
27
+ <a href="<%= stats_views_per_visits_path(search_params) %>" data-turbo-frame="chart">
28
+
29
+ <dt class="text-base font-normal text-base-content">Views per visit</dt>
30
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
31
+ <div class="flex items-baseline text-2xl font-semibold text-accent-content">
32
+ <%= number_with_delimiter @presenter.views_per_visit %>
33
+ </div>
34
+ </dd>
35
+ </a>
36
+ <a href="<%= stats_bounce_rates_path(search_params) %>" data-turbo-frame="chart">
37
+
38
+ <dt class="text-base font-normal text-base-content">Bounce rate</dt>
39
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
40
+ <div class="flex items-baseline text-2xl font-semibold text-accent-content">
41
+ <%= number_with_delimiter @presenter.bounce_rate %>%
42
+ </div>
43
+ </dd>
44
+ </a>
45
+ <a href="<%= stats_visit_durations_path(search_params) %>" data-turbo-frame="chart">
46
+
47
+ <dt class="text-base font-normal text-base-content">Visit duration</dt>
48
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
49
+ <div class="flex items-baseline text-2xl font-semibold text-accent-content">
50
+ <%= @presenter.visit_duration %>
51
+ </div>
52
+ </dd>
53
+ </a>
54
+ </dl>
55
+ <%= turbo_frame_tag :chart, src: stats_unique_visitors_path(search_params) do %>
56
+ <% end %>
57
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render AhoyCaptain::TableComponent.new(items: @pages, category_name: 'Page', unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :pages do %>
2
+ <%= render AhoyCaptain::TableComponent.new(items: @pages, category_name: 'Page', unit_name: 'Visitors') %>
3
+ <% end %>
data/config/routes.rb ADDED
@@ -0,0 +1,56 @@
1
+ AhoyCaptain::Engine.routes.draw do
2
+ root to: 'roots#show'
3
+ %w{utm_source utm_medium utm_term utm_content utm_campaign}.each do |utm|
4
+ get "campaigns/#{utm}" => "campaigns#index", defaults: { campaigns_type: utm }, as: "campaign_#{utm}"
5
+ end
6
+
7
+ {
8
+ browsers: :browser,
9
+ operating_systems: :os,
10
+ device_types: :device_type
11
+ }.each do |k,v|
12
+ get "/devices/#{k}" => 'devices#index', defaults: { devices_type: v }, as: "devices_#{k}"
13
+ end
14
+
15
+ resource :realtime, only: [:show]
16
+ resources :funnels, only: [:show]
17
+ resources :goals, only: [:index]
18
+ resource :stats, only: [:show]
19
+ resources :countries, only: [:index]
20
+ resources :regions, only: [:index]
21
+ resources :cities, only: [:index]
22
+ resources :campaigns, only: [:index]
23
+ resources :sources, only: [:index]
24
+ resources :exit_pages, only: [:index]
25
+ resources :top_pages, only: [:index]
26
+ resources :entry_pages, only: [:index]
27
+
28
+ namespace :stats do
29
+ resources :unique_visitors, only: [:index]
30
+ resources :total_visits, only: [:index]
31
+ resources :total_pageviews, only: [:index]
32
+ resources :views_per_visits, only: [:index]
33
+ resources :bounce_rates, only: [:index]
34
+ resources :visit_durations, only: [:index]
35
+ end
36
+ namespace :filters do
37
+ %w{source medium term content campaign}.each do |utm|
38
+ get "utm/#{utm}s" => "utms#index", defaults: { type: "utm_#{utm}" }
39
+ end
40
+
41
+ %w{country region city}.each do |type|
42
+ get "locations/#{type.pluralize}" => "locations#index", defaults: { type: type }
43
+ end
44
+ resources :sources, only: [:index]
45
+ resources :screens, only: [:index]
46
+ scope :operating_systems, module: :operating_systems do
47
+ resources :names, only: [:index]
48
+ resources :versions, only: [:index]
49
+ end
50
+ scope :pages, module: :pages do
51
+ resources :actions, only: [:index]
52
+ resources :entry_pages, only: [:index]
53
+ resources :exit_pages, only: [:index]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,108 @@
1
+ module ActiveRecord
2
+ module Querying
3
+ delegate :with, to: :all
4
+ end
5
+
6
+ module WithMerger
7
+ def merge
8
+ super
9
+ merge_withs
10
+ relation
11
+ end
12
+
13
+ private
14
+
15
+ def merge_withs
16
+ relation.recursive_with = true if other.recursive_with?
17
+ other_values = other.with_values.reject { |value| relation.with_values.include?(value) }
18
+ relation.with!(*other_values) if other_values.any?
19
+ end
20
+ end
21
+
22
+ class Relation
23
+ class Merger
24
+ prepend WithMerger
25
+ end
26
+
27
+ def with(opts, *rest)
28
+ spawn.with!(opts, *rest)
29
+ end
30
+
31
+ def with!(opts, *rest)
32
+ if opts == :recursive
33
+ self.recursive_with = true
34
+ self.with_values += rest
35
+ else
36
+ self.with_values += [opts] + rest
37
+ end
38
+ self
39
+ end
40
+
41
+ def with_values
42
+ @values[:with] || []
43
+ end
44
+
45
+ def with_values=(values)
46
+ raise ImmutableRelation if @loaded
47
+
48
+ @values[:with] = values
49
+ end
50
+
51
+ def recursive_with?
52
+ @values[:recursive_with]
53
+ end
54
+
55
+ def recursive_with=(value)
56
+ raise ImmutableRelation if @loaded
57
+
58
+ @values[:recursive_with] = value
59
+ end
60
+
61
+ private
62
+
63
+ def build_arel(*args)
64
+ arel = super
65
+ build_with(arel) if @values[:with]
66
+ arel
67
+ end
68
+
69
+ def build_with(arel) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
70
+ return if with_values.empty?
71
+
72
+ with_statements = with_values.map do |with_value|
73
+ case with_value
74
+ when String then Arel::Nodes::SqlLiteral.new(with_value)
75
+ when Arel::Nodes::As then with_value
76
+ when Hash then build_with_value_from_hash(with_value)
77
+ when Array then build_with_value_from_array(with_value)
78
+ else
79
+ raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}"
80
+ end
81
+ end
82
+
83
+ recursive_with? ? arel.with(:recursive, with_statements) : arel.with(with_statements)
84
+ end
85
+
86
+ def build_with_value_from_array(array)
87
+ unless array.map(&:class).uniq == [Arel::Nodes::As]
88
+ raise ArgumentError, "Unsupported argument type: #{array} #{array.class}"
89
+ end
90
+
91
+ array
92
+ end
93
+
94
+ def build_with_value_from_hash(hash) # rubocop:disable Metrics/MethodLength
95
+ hash.map do |name, value|
96
+ table = Arel::Table.new(name)
97
+ expression = case value
98
+ when String then Arel::Nodes::SqlLiteral.new("(#{value})")
99
+ when ActiveRecord::Relation then value.arel
100
+ when Arel::SelectManager, Arel::Nodes::Union then value
101
+ else
102
+ raise ArgumentError, "Unsupported argument type: #{value} #{value.class}"
103
+ end
104
+ Arel::Nodes::As.new(table, expression)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,114 @@
1
+ module AhoyCaptain
2
+ module Ahoy
3
+ module EventMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ ransacker :route do |parent|
8
+ Arel.sql(AhoyCaptain.config.event[:url_column])
9
+ end
10
+
11
+ scope :with_routes, -> { where(AhoyCaptain.config.event[:url_exists]) }
12
+
13
+ scope :with_url, -> {
14
+ select(Arel.sql("#{AhoyCaptain.config.event.url_column} AS url"))
15
+ }
16
+
17
+ scope :distinct_url, -> {
18
+ distinct(Arel.sql("#{AhoyCaptain.config.event.url_column}"))
19
+ }
20
+
21
+ scope :url_in, ->(*args) {
22
+ where("#{AhoyCaptain.config.event.url_column} IN (?)", args)
23
+ }
24
+
25
+ scope :url_eq, ->(arg) {
26
+ if arg.is_a?(Array)
27
+ arg = arg[0]
28
+ end
29
+ where("#{AhoyCaptain.config.event.url_column} = ?", arg)
30
+ }
31
+
32
+ scope :url_not_in, ->(*args) {
33
+ where("#{AhoyCaptain.config.event.url_column} NOT IN (?)", args)
34
+ }
35
+
36
+ scope :url_i_cont, ->(arg) {
37
+ where("#{AhoyCaptain.config.event.url_column} ILIKE ?", "%#{arg}%")
38
+ }
39
+
40
+ scope :route_eq, ->(arg) {
41
+ url_eq(arg)
42
+ }
43
+
44
+ scope :route_in, ->(*args) {
45
+ url_in(*args)
46
+ }
47
+
48
+ scope :route_not_in, ->(*args) {
49
+ url_not_in(*args)
50
+ }
51
+
52
+ scope :route_i_cont, ->(arg) {
53
+ url_i_cont(arg)
54
+ }
55
+
56
+ scope :entry_page_in, ->(*args) {
57
+ table_alias = "first_events_#{SecureRandom.hex.first(6)}"
58
+
59
+ subquery = self.select("MIN(id) as min_id").where(name: AhoyCaptain.config.event[:view_name]).route_in(*args).group(:visit_id)
60
+ joins("INNER JOIN (#{subquery.to_sql}) #{table_alias} ON #{::AhoyCaptain.event.table_name}.id = #{table_alias}.min_id")
61
+ }
62
+
63
+ scope :entry_page_not_in, ->(*args) {
64
+ table_alias = "first_events_#{SecureRandom.hex.first(6)}"
65
+ subquery = self.select("MIN(id) as min_id").where(name: AhoyCaptain.config.event[:view_name]).route_not_in(*args).group(:visit_id)
66
+ joins("INNER JOIN (#{subquery.to_sql}) #{table_alias} ON #{::AhoyCaptain.event.table_name}.id = #{table_alias}.min_id")
67
+ }
68
+
69
+ scope :entry_page_i_cont, ->(arg) {
70
+ table_alias = "first_events_#{SecureRandom.hex.first(6)}"
71
+ subquery = self.select("MIN(id) as min_id").where(name: AhoyCaptain.config.event[:view_name]).route_i_cont(arg).group(:visit_id)
72
+ joins("INNER JOIN (#{subquery.to_sql}) #{table_alias} ON #{::AhoyCaptain.event.table_name}.id = #{table_alias}.min_id")
73
+ }
74
+
75
+ scope :exit_page_in, ->(*args) {
76
+ table_alias = "last_events_#{SecureRandom.hex.first(6)}"
77
+
78
+ subquery = self.select("MAX(id) as max_id").where(name: AhoyCaptain.config.event[:view_name]).route_in(*args).group(:visit_id)
79
+ joins("INNER JOIN (#{subquery.to_sql}) #{table_alias} ON #{::AhoyCaptain.event.table_name}.id = #{table_alias}.max_id")
80
+ }
81
+
82
+ scope :exit_page_not_in, ->(*args) {
83
+ table_alias = "last_events_#{SecureRandom.hex.first(6)}"
84
+
85
+ subquery = self.select("MAX(id) as max_id").where(name: AhoyCaptain.config.event[:view_name]).route_not_in(*args).group(:visit_id)
86
+ joins("INNER JOIN (#{subquery.to_sql}) #{table_alias} ON #{::AhoyCaptain.event.table_name}.id = #{table_alias}.max_id")
87
+ }
88
+
89
+ scope :exit_page_i_cont, ->(arg) {
90
+ table_alias = "last_events_#{SecureRandom.hex.first(6)}"
91
+
92
+ subquery = self.select("MAX(id) as max_id").where(name: AhoyCaptain.config.event[:view_name]).route_i_cont(*arg).group(:visit_id)
93
+ joins("INNER JOIN (#{subquery.to_sql}) #{table_alias} ON #{::AhoyCaptain.event.table_name}.id = #{table_alias}.max_id")
94
+ }
95
+
96
+ end
97
+
98
+ class_methods do
99
+ def ransackable_attributes(auth_object = nil)
100
+ super + ["action", "controller", "id", "id_property", "name", "name_property", "page", "properties", "time", "url", "user_id", "visit_id"] + self._ransackers.keys
101
+ end
102
+
103
+ def ransackable_scopes(auth_object = nil)
104
+ super + [:entry_page_in, :entry_page_not_in, :exit_page_in, :entry_page_not_in, :route_in, :route_not_in, :route_i_cont, :entry_page_i_cont, :exit_page_i_cont]
105
+ end
106
+
107
+ def ransackable_associations(auth_object = nil)
108
+ super + [:visit]
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
@@ -0,0 +1,23 @@
1
+ module AhoyCaptain
2
+ module Ahoy
3
+ module VisitMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ ransacker :ref_domain do
8
+ Arel.sql("(substring(referring_domain from '(?:.*://)?(?:www\.)?([^/?]*)'))")
9
+ end
10
+ end
11
+
12
+ class_methods do
13
+ def ransackable_attributes(auth = nil)
14
+ columns_hash.keys + ["ref_domain"]
15
+ end
16
+
17
+ def ransackable_associations(auth = nil)
18
+ super + ["events"]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ require 'ahoy_captain/period_collection'
2
+
3
+ module AhoyCaptain
4
+ class Configuration
5
+ attr_accessor :view_name, :theme
6
+ attr_reader :goals, :funnels, :cache, :ranges, :disabled_widgets, :event, :models
7
+ def initialize
8
+ @goals = GoalCollection.new
9
+ @funnels = FunnelCollection.new
10
+ @theme = "dark"
11
+ @ranges = ::AhoyCaptain::PeriodCollection.load_default
12
+ @cache = ActiveSupport::OrderedOptions.new.tap do |option|
13
+ option.enabled = false
14
+ option.store = Rails.cache
15
+ option.ttl = 1.minute
16
+ end
17
+ @event = ActiveSupport::OrderedOptions.new.tap do |option|
18
+ option.view_name = "$view"
19
+ option.url_column = "CONCAT(ahoy_events.properties->>'controller', '#', ahoy_events.properties->>'action')"
20
+ option.url_exists = "JSONB_EXISTS(ahoy_events.properties, 'controller') AND JSONB_EXISTS(ahoy_events.properties, 'action')"
21
+ end
22
+ @models = ActiveSupport::OrderedOptions.new.tap do |option|
23
+ option.event = "::Ahoy::Event"
24
+ option.visit = "::Ahoy::Visit"
25
+ end
26
+ @disabled_widgets = []
27
+ end
28
+
29
+ def goal(id, &block)
30
+ instance = Goal.new
31
+ instance.id = id
32
+ instance.instance_exec(&block)
33
+ @goals.register(instance)
34
+ end
35
+
36
+ def funnel(id, &block)
37
+ instance = Funnel.new
38
+ instance.id = id
39
+ instance.instance_exec(&block)
40
+ @funnels.register(instance)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ require 'ahoy'
2
+ require 'turbo-rails'
3
+ require 'stimulus-rails'
4
+ require 'ransack'
5
+ require 'view_component'
6
+ require 'chartkick'
7
+ require 'groupdate'
8
+ require 'pagy'
9
+
10
+ module AhoyCaptain
11
+ class Engine < Rails::Engine
12
+ isolate_namespace ::AhoyCaptain
13
+
14
+ initializer "ahoy_captain.precompile" do |app|
15
+ app.config.assets.paths << AhoyCaptain::Engine.root.join("app/javascript")
16
+ app.config.assets.paths << AhoyCaptain::Engine.root.join("app/images")
17
+ app.config.assets.precompile << "ahoy_captain/application.js"
18
+ end
19
+
20
+ ActiveSupport.on_load(:active_record) do
21
+ require "ahoy_captain/active_record"
22
+ end
23
+ end
24
+ end