lookout-ahoy 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 (223) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +99 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/images/lookout/apple-touch-icon.png +0 -0
  6. data/app/assets/images/lookout/favicon-16x16.png +0 -0
  7. data/app/assets/images/lookout/favicon-32x32.png +0 -0
  8. data/app/assets/images/lookout/logo.png +0 -0
  9. data/app/assets/images/lookout/safari-pinned-tab.png +0 -0
  10. data/app/assets/images/lookout/safari-pinned-tab.svg +199 -0
  11. data/app/assets/javascript/lookout/application.js +2 -0
  12. data/app/assets/javascript/lookout/controllers/application.js +9 -0
  13. data/app/assets/javascript/lookout/controllers/application_controller.js +33 -0
  14. data/app/assets/javascript/lookout/controllers/combobox_controller.js +371 -0
  15. data/app/assets/javascript/lookout/controllers/details_modal_controller.js +18 -0
  16. data/app/assets/javascript/lookout/controllers/dropdown_label_controller.js +39 -0
  17. data/app/assets/javascript/lookout/controllers/filter/item_controller.js +12 -0
  18. data/app/assets/javascript/lookout/controllers/filter_form_controller.js +13 -0
  19. data/app/assets/javascript/lookout/controllers/filter_modal_controller.js +45 -0
  20. data/app/assets/javascript/lookout/controllers/frame_link_controller.js +20 -0
  21. data/app/assets/javascript/lookout/controllers/funnel_chart_controller.js +159 -0
  22. data/app/assets/javascript/lookout/controllers/index.js +4 -0
  23. data/app/assets/javascript/lookout/controllers/interval_controller.js +15 -0
  24. data/app/assets/javascript/lookout/controllers/line_chart_controller.js +251 -0
  25. data/app/assets/javascript/lookout/controllers/predicate_select_controller.js +10 -0
  26. data/app/assets/javascript/lookout/controllers/properties_controller.js +8 -0
  27. data/app/assets/javascript/lookout/controllers/property_filter_controller.js +45 -0
  28. data/app/assets/javascript/lookout/controllers/realtime_controller.js +30 -0
  29. data/app/assets/javascript/lookout/controllers/sparkline_controller.js +64 -0
  30. data/app/assets/javascript/lookout/controllers/tile_controller.js +33 -0
  31. data/app/assets/javascript/lookout/controllers/toggle_controller.js +17 -0
  32. data/app/assets/javascript/lookout/helpers/chart_utils.js +156 -0
  33. data/app/assets/javascript/lookout/helpers/countries.js +2261 -0
  34. data/app/assets/javascript/lookout/helpers/number_formatters.js +55 -0
  35. data/app/assets/manifest/lookout/manifest.js +2 -0
  36. data/app/components/lookout/combobox_component.html.erb +33 -0
  37. data/app/components/lookout/combobox_component.rb +13 -0
  38. data/app/components/lookout/comparison_link_component.html.erb +17 -0
  39. data/app/components/lookout/comparison_link_component.rb +44 -0
  40. data/app/components/lookout/dropdown_button_component.html.erb +16 -0
  41. data/app/components/lookout/dropdown_button_component.rb +14 -0
  42. data/app/components/lookout/dropdown_link_component.html.erb +17 -0
  43. data/app/components/lookout/dropdown_link_component.rb +19 -0
  44. data/app/components/lookout/filter/dropdown_component.html.erb +50 -0
  45. data/app/components/lookout/filter/dropdown_component.rb +51 -0
  46. data/app/components/lookout/filter/modal_component.html.erb +16 -0
  47. data/app/components/lookout/filter/modal_component.rb +13 -0
  48. data/app/components/lookout/filter/select_component.html.erb +25 -0
  49. data/app/components/lookout/filter/select_component.rb +64 -0
  50. data/app/components/lookout/filter/tag_component.html.erb +13 -0
  51. data/app/components/lookout/filter/tag_component.rb +14 -0
  52. data/app/components/lookout/filter/tag_container_component.html.erb +4 -0
  53. data/app/components/lookout/filter/tag_container_component.rb +6 -0
  54. data/app/components/lookout/previous_next_component.html.erb +8 -0
  55. data/app/components/lookout/previous_next_component.rb +11 -0
  56. data/app/components/lookout/stats/comparable_container_component.html.erb +25 -0
  57. data/app/components/lookout/stats/comparable_container_component.rb +103 -0
  58. data/app/components/lookout/stats/container_component.html.erb +23 -0
  59. data/app/components/lookout/stats/container_component.rb +28 -0
  60. data/app/components/lookout/sticky_nav_component.html.erb +32 -0
  61. data/app/components/lookout/sticky_nav_component.rb +24 -0
  62. data/app/components/lookout/table_component.html.erb +16 -0
  63. data/app/components/lookout/table_component.rb +48 -0
  64. data/app/components/lookout/tables/devices_table_component.rb +11 -0
  65. data/app/components/lookout/tables/dynamic_table.rb +13 -0
  66. data/app/components/lookout/tables/dynamic_table_component.rb +207 -0
  67. data/app/components/lookout/tables/goals_table_component.rb +17 -0
  68. data/app/components/lookout/tables/header_component.html.erb +6 -0
  69. data/app/components/lookout/tables/header_component.rb +18 -0
  70. data/app/components/lookout/tables/headers/header_component.html.erb +5 -0
  71. data/app/components/lookout/tables/headers/header_component.rb +16 -0
  72. data/app/components/lookout/tables/properties_table_component.rb +27 -0
  73. data/app/components/lookout/tables/row_component.html.erb +4 -0
  74. data/app/components/lookout/tables/rows/row_component.html.erb +6 -0
  75. data/app/components/lookout/tables/rows/row_component.rb +40 -0
  76. data/app/components/lookout/tile_component.html.erb +24 -0
  77. data/app/components/lookout/tile_component.rb +24 -0
  78. data/app/components/lookout/tooltip_component.html.erb +3 -0
  79. data/app/components/lookout/tooltip_component.rb +18 -0
  80. data/app/controllers/lookout/application_controller.rb +83 -0
  81. data/app/controllers/lookout/campaigns_controller.rb +19 -0
  82. data/app/controllers/lookout/devices_controller.rb +20 -0
  83. data/app/controllers/lookout/entry_pages_controller.rb +19 -0
  84. data/app/controllers/lookout/exit_pages_controller.rb +19 -0
  85. data/app/controllers/lookout/exports_controller.rb +14 -0
  86. data/app/controllers/lookout/filters/base_controller.rb +15 -0
  87. data/app/controllers/lookout/filters/goals_controller.rb +9 -0
  88. data/app/controllers/lookout/filters/locations_controller.rb +11 -0
  89. data/app/controllers/lookout/filters/operating_systems/names_controller.rb +13 -0
  90. data/app/controllers/lookout/filters/operating_systems/versions_controller.rb +13 -0
  91. data/app/controllers/lookout/filters/pages/actions_controller.rb +13 -0
  92. data/app/controllers/lookout/filters/pages/entry_pages_controller.rb +14 -0
  93. data/app/controllers/lookout/filters/pages/exit_pages_controller.rb +15 -0
  94. data/app/controllers/lookout/filters/properties/names_controller.rb +29 -0
  95. data/app/controllers/lookout/filters/properties/values_controller.rb +15 -0
  96. data/app/controllers/lookout/filters/screens_controller.rb +11 -0
  97. data/app/controllers/lookout/filters/sources_controller.rb +11 -0
  98. data/app/controllers/lookout/filters/utms_controller.rb +10 -0
  99. data/app/controllers/lookout/funnels_controller.rb +8 -0
  100. data/app/controllers/lookout/goals_controller.rb +7 -0
  101. data/app/controllers/lookout/locations/cities_controller.rb +22 -0
  102. data/app/controllers/lookout/locations/countries_controller.rb +22 -0
  103. data/app/controllers/lookout/locations/maps_controller.rb +24 -0
  104. data/app/controllers/lookout/locations/regions_controller.rb +22 -0
  105. data/app/controllers/lookout/properties_controller.rb +73 -0
  106. data/app/controllers/lookout/realtimes_controller.rb +7 -0
  107. data/app/controllers/lookout/roots_controller.rb +6 -0
  108. data/app/controllers/lookout/sources_controller.rb +21 -0
  109. data/app/controllers/lookout/stats/base_controller.rb +148 -0
  110. data/app/controllers/lookout/stats/bounce_rates_controller.rb +12 -0
  111. data/app/controllers/lookout/stats/total_pageviews_controller.rb +10 -0
  112. data/app/controllers/lookout/stats/total_visits_controller.rb +10 -0
  113. data/app/controllers/lookout/stats/unique_visitors_controller.rb +11 -0
  114. data/app/controllers/lookout/stats/views_per_visits_controller.rb +11 -0
  115. data/app/controllers/lookout/stats/visit_durations_controller.rb +10 -0
  116. data/app/controllers/lookout/stats_controller.rb +7 -0
  117. data/app/controllers/lookout/top_pages_controller.rb +20 -0
  118. data/app/decorators/lookout/application_decorator.rb +58 -0
  119. data/app/decorators/lookout/campaign_decorator.rb +27 -0
  120. data/app/decorators/lookout/city_decorator.rb +24 -0
  121. data/app/decorators/lookout/country_decorator.rb +38 -0
  122. data/app/decorators/lookout/device_decorator.rb +27 -0
  123. data/app/decorators/lookout/entry_page_decorator.rb +7 -0
  124. data/app/decorators/lookout/exit_page_decorator.rb +7 -0
  125. data/app/decorators/lookout/page_decorator.rb +27 -0
  126. data/app/decorators/lookout/region_decorator.rb +28 -0
  127. data/app/decorators/lookout/source_decorator.rb +27 -0
  128. data/app/decorators/lookout/top_page_decorator.rb +7 -0
  129. data/app/helpers/lookout/application_helper.rb +124 -0
  130. data/app/models/concerns/lookout/compare_mode.rb +19 -0
  131. data/app/models/concerns/lookout/limitable.rb +17 -0
  132. data/app/models/concerns/lookout/range_options.rb +8 -0
  133. data/app/models/lookout/comparison_mode.rb +72 -0
  134. data/app/models/lookout/export.rb +48 -0
  135. data/app/models/lookout/filter_parser.rb +82 -0
  136. data/app/models/lookout/range_from_params.rb +78 -0
  137. data/app/models/lookout/rangeable.rb +7 -0
  138. data/app/models/lookout/widget.rb +15 -0
  139. data/app/presenters/lookout/dashboard_presenter.rb +53 -0
  140. data/app/presenters/lookout/funnel_presenter.rb +75 -0
  141. data/app/presenters/lookout/goals_presenter.rb +72 -0
  142. data/app/queries/concerns/lookout/comparable_queries.rb +25 -0
  143. data/app/queries/concerns/lookout/comparable_query.rb +152 -0
  144. data/app/queries/concerns/lookout/lazy_comparable_query.rb +42 -0
  145. data/app/queries/lookout/application_query.rb +186 -0
  146. data/app/queries/lookout/campaign_query.rb +14 -0
  147. data/app/queries/lookout/city_query.rb +14 -0
  148. data/app/queries/lookout/country_query.rb +10 -0
  149. data/app/queries/lookout/device_query.rb +10 -0
  150. data/app/queries/lookout/entry_pages_query.rb +18 -0
  151. data/app/queries/lookout/event_query.rb +42 -0
  152. data/app/queries/lookout/exit_pages_query.rb +19 -0
  153. data/app/queries/lookout/region_query.rb +14 -0
  154. data/app/queries/lookout/source_query.rb +11 -0
  155. data/app/queries/lookout/stats/average_views_per_visit_query.rb +20 -0
  156. data/app/queries/lookout/stats/average_visit_duration_query.rb +34 -0
  157. data/app/queries/lookout/stats/base_query.rb +18 -0
  158. data/app/queries/lookout/stats/bounce_rates_query.rb +33 -0
  159. data/app/queries/lookout/stats/total_pageviews_query.rb +9 -0
  160. data/app/queries/lookout/stats/total_visitors_query.rb +9 -0
  161. data/app/queries/lookout/stats/unique_visitors_query.rb +9 -0
  162. data/app/queries/lookout/stats/views_per_visit_query.rb +17 -0
  163. data/app/queries/lookout/stats/visit_duration_query.rb +19 -0
  164. data/app/queries/lookout/top_page_query.rb +13 -0
  165. data/app/queries/lookout/visit_query.rb +42 -0
  166. data/app/views/lookout/campaigns/index.html+details.erb +4 -0
  167. data/app/views/lookout/campaigns/index.html.erb +3 -0
  168. data/app/views/lookout/devices/_table.html.erb +2 -0
  169. data/app/views/lookout/devices/index.html+details.erb +4 -0
  170. data/app/views/lookout/devices/index.html.erb +3 -0
  171. data/app/views/lookout/entry_pages/index.html+details.erb +4 -0
  172. data/app/views/lookout/entry_pages/index.html.erb +3 -0
  173. data/app/views/lookout/exit_pages/index.html+details.erb +4 -0
  174. data/app/views/lookout/exit_pages/index.html.erb +3 -0
  175. data/app/views/lookout/funnels/index.html.erb +7 -0
  176. data/app/views/lookout/funnels/show.html.erb +15 -0
  177. data/app/views/lookout/goals/index.html.erb +4 -0
  178. data/app/views/lookout/layouts/application.html.erb +144 -0
  179. data/app/views/lookout/layouts/shared/_tile_loader.html.erb +5 -0
  180. data/app/views/lookout/layouts/shared/_widget_disabled.html+details.erb +3 -0
  181. data/app/views/lookout/layouts/shared/_widget_disabled.html.erb +3 -0
  182. data/app/views/lookout/locations/cities/index.html+details.erb +4 -0
  183. data/app/views/lookout/locations/cities/index.html.erb +3 -0
  184. data/app/views/lookout/locations/countries/index.html+details.erb +5 -0
  185. data/app/views/lookout/locations/countries/index.html.erb +3 -0
  186. data/app/views/lookout/locations/maps/_simple_map.html.erb +26 -0
  187. data/app/views/lookout/locations/maps/show.html.erb +106 -0
  188. data/app/views/lookout/locations/regions/index.html+details.erb +4 -0
  189. data/app/views/lookout/locations/regions/index.html.erb +3 -0
  190. data/app/views/lookout/properties/_form.html.erb +6 -0
  191. data/app/views/lookout/properties/index.html.erb +3 -0
  192. data/app/views/lookout/properties/show.html.erb +6 -0
  193. data/app/views/lookout/realtimes/show.html.erb +9 -0
  194. data/app/views/lookout/roots/_filters.html.erb +80 -0
  195. data/app/views/lookout/roots/show.html.erb +191 -0
  196. data/app/views/lookout/sources/index.html+details.erb +4 -0
  197. data/app/views/lookout/sources/index.html.erb +3 -0
  198. data/app/views/lookout/stats/base/index.html.erb +40 -0
  199. data/app/views/lookout/stats/show.html.erb +15 -0
  200. data/app/views/lookout/top_pages/index.html+details.erb +4 -0
  201. data/app/views/lookout/top_pages/index.html.erb +3 -0
  202. data/config/routes.rb +69 -0
  203. data/lib/generators/lookout/install_generator.rb +31 -0
  204. data/lib/generators/lookout/migration_generator.rb +21 -0
  205. data/lib/generators/lookout/templates/config.rb.tt +185 -0
  206. data/lib/generators/lookout/templates/migration.rb.tt +7 -0
  207. data/lib/lookout/active_record.rb +108 -0
  208. data/lib/lookout/ahoy/event_methods.rb +75 -0
  209. data/lib/lookout/ahoy/visit_methods.rb +24 -0
  210. data/lib/lookout/configuration.rb +58 -0
  211. data/lib/lookout/database_adapter.rb +168 -0
  212. data/lib/lookout/engine.rb +47 -0
  213. data/lib/lookout/filter_configuration/filter.rb +16 -0
  214. data/lib/lookout/filter_configuration/filter_collection.rb +48 -0
  215. data/lib/lookout/filters_configuration.rb +77 -0
  216. data/lib/lookout/funnels.rb +44 -0
  217. data/lib/lookout/goals.rb +51 -0
  218. data/lib/lookout/period_collection.rb +115 -0
  219. data/lib/lookout/predicate_label.rb +7 -0
  220. data/lib/lookout/railtie.rb +9 -0
  221. data/lib/lookout/version.rb +3 -0
  222. data/lib/lookout.rb +78 -0
  223. metadata +673 -0
@@ -0,0 +1,191 @@
1
+ <main class='w-screen overflow-hidden' data-action="combobox:init@window->application#comboboxInit">
2
+ <script id="tile-loader-template" type="template/html">
3
+ <%= render '/lookout/layouts/shared/tile_loader' %>
4
+ </script>
5
+ <%= render Lookout::StickyNavComponent.new do |nav| %>
6
+ <% nav.with_realtime_update do %>
7
+ <%= turbo_frame_tag :realtime, src: realtime_path, data: { controller: "realtime", "realtime-interval-value" => Lookout.config.realtime_interval.to_i }, loading: :lazy %>
8
+ <% end %>
9
+ <% end %>
10
+
11
+ <div class="grid grid-cols-1 lg:grid-cols-2 grid-flow-row gap-4 min-h-screen pb-4 max-w-6xl mx-auto">
12
+ <%= render Lookout::TileComponent.new(wide: true, classes: "p-4 m-2") do |component| %>
13
+ <% component.with_statistic_display do %>
14
+ <%= turbo_frame_tag :stats, src: stats_path(search_params), loading: :lazy, skeleton: false do %>
15
+ <div 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">
16
+ <% 6.times do %>
17
+ <div class="relative px-4 md:px-6 w-1/2 my-4 w-auto group cursor-pointer" >
18
+ <div role="status" class="max-w-sm animate-pulse opacity-40">
19
+ <div class="h-2 bg-gray-100 rounded-full dark:bg-gray-800 max-w-[120px] mb-2.5"></div>
20
+ <div class="h-2 bg-gray-100 rounded-full dark:bg-gray-800 mb-2.5 max-w-[60px]"></div>
21
+ </div>
22
+ </div>
23
+ <% end %>
24
+ </div>
25
+ <% end %>
26
+ <%= turbo_frame_tag :chart, src: stats_unique_visitors_path(search_params) do %>
27
+ <div role="status" class="p-4 animate-pulse opacity-30 md:p-6">
28
+ <div class="flex items-baseline mt-4 space-x-6">
29
+ <div class="w-full bg-gray-100 rounded-t-lg h-72 dark:bg-gray-800"></div>
30
+ <div class="w-full h-56 bg-gray-100 rounded-t-lg dark:bg-gray-800"></div>
31
+ <div class="w-full bg-gray-100 rounded-t-lg h-72 dark:bg-gray-800"></div>
32
+ <div class="w-full h-64 bg-gray-100 rounded-t-lg dark:bg-gray-800"></div>
33
+ <div class="w-full bg-gray-100 rounded-t-lg h-80 dark:bg-gray-800"></div>
34
+ <div class="w-full bg-gray-100 rounded-t-lg h-72 dark:bg-gray-800"></div>
35
+ <div class="w-full bg-gray-100 rounded-t-lg h-80 dark:bg-gray-800"></div>
36
+ </div>
37
+ <span class="sr-only">Loading...</span>
38
+ </div>
39
+ <% end %>
40
+ <% end %>
41
+
42
+ <% end %>
43
+
44
+ <%= render Lookout::TileComponent.new(title: 'Top Sources') do |component| %>
45
+ <% component.with_display_links do %>
46
+ <div class="flex text-xs font-medium text-gray-400 space-x-2">
47
+ <div class="relative inline-block text-left">
48
+ <%= component.link_to "All", sources_path(search_params), data: { turbo_frame: "sources" } %>
49
+ <%= render Lookout::DropdownLinkComponent.new(title: "Campaign") do |dropdown| %>
50
+ <% %w{utm_source utm_medium utm_term utm_content utm_campaign}.each do |source| %>
51
+ <%= dropdown.link_to source.titleize.gsub("Utm", "UTM"), public_send("campaign_#{source}_path".to_sym, **search_params), data: { turbo_frame: "sources" } %>
52
+ <% end %>
53
+ <% end %>
54
+ </div>
55
+ </div>
56
+
57
+
58
+ <% end %>
59
+ <% component.with_statistic_display do %>
60
+ <%= turbo_frame_tag :sources, src: sources_path(search_params), loading: :lazy do %>
61
+ <%= render '/lookout/layouts/shared/tile_loader' %>
62
+ <% end %>
63
+ <% end %>
64
+ <% component.with_details_cta do %>
65
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#sources" class="no-underline text-xs text-gray-400 font-normal hover:text-gray-500">Details</button>
66
+ <% end %>
67
+ <% end %>
68
+
69
+ <%= render Lookout::TileComponent.new(title: 'Top Pages') do |component| %>
70
+ <% component.with_display_links do %>
71
+ <div class="flex text-xs font-medium text-gray-400 space-x-2">
72
+ <div class="relative inline-block text-left"><div>
73
+ <%= component.link_to "Top Pages", top_pages_path(search_params), data: { action: "click->tile#setTitle", turbo_frame: "pages" } %>
74
+ <%= component.link_to "Entry Pages", entry_pages_path(search_params), data: { action: "click->tile#setTitle", turbo_frame: "pages" } %>
75
+ <%= component.link_to "Exit Pages", exit_pages_path(search_params), data: { action: "click->tile#setTitle", turbo_frame: "pages" } %>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <% end %>
81
+ <% component.with_statistic_display do %>
82
+ <%= turbo_frame_tag :pages, src: top_pages_path(search_params), loading: :lazy do %>
83
+ <%= render '/lookout/layouts/shared/tile_loader' %>
84
+ <% end %>
85
+ <% end %>
86
+ <% component.with_details_cta do %>
87
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#pages" class="no-underline text-xs text-gray-400 font-normal hover:text-gray-500">Details</button>
88
+ <% end %>
89
+ <% end %>
90
+
91
+ <%= render Lookout::TileComponent.new(title: 'Map') do |component| %>
92
+ <% component.with_display_links do %>
93
+ <div class="flex text-xs font-medium text-gray-400 space-x-2">
94
+ <div class="relative inline-block text-left">
95
+ <%= component.link_to "Map", locations_map_path(search_params), data: { action: "click->tile#setTitle", turbo_frame: "geography" } %>
96
+ <%= component.link_to "Countries", locations_countries_path(search_params), data: { action: "click->tile#setTitle", turbo_frame: "geography" } %>
97
+ <%= component.link_to "Regions", locations_regions_path(search_params), data: { action: "click->tile#setTitle", turbo_frame: "geography" } %>
98
+ <%= component.link_to "Cities", locations_cities_path(search_params), data: { action: "click->tile#setTitle", turbo_frame: "geography" } %>
99
+ </div>
100
+ </div>
101
+ <% end %>
102
+ <% component.with_statistic_display do %>
103
+ <%= turbo_frame_tag :geography, src: locations_map_path(search_params), loading: :lazy do %>
104
+ <%= render '/lookout/layouts/shared/tile_loader' %>
105
+
106
+ <% end %>
107
+ <% end %>
108
+ <% component.with_details_cta do %>
109
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#geography" class="no-underline text-xs text-gray-400 font-normal hover:text-gray-500">Details</button>
110
+ <% end %>
111
+ <% end %>
112
+
113
+ <%= render Lookout::TileComponent.new(title: 'Devices') do |component| %>
114
+ <% component.with_display_links do %>
115
+ <div class="flex text-xs font-medium text-gray-400 space-x-2">
116
+ <div class="relative inline-block text-left">
117
+ <%= component.link_to "Browser", devices_browsers_path(search_params), data: { turbo_frame: "devices" } %>
118
+ <%= component.link_to "OS", devices_operating_systems_path(search_params), data: { turbo_frame: "devices" } %>
119
+ <%= component.link_to "Size", devices_device_types_path(search_params), data: { turbo_frame: "devices" } %>
120
+ </div>
121
+ </div>
122
+ <% end %>
123
+ <% component.with_statistic_display do %>
124
+ <%= turbo_frame_tag :devices, src: devices_browsers_path(search_params), loading: :lazy do %>
125
+ <%= render '/lookout/layouts/shared/tile_loader' %>
126
+
127
+ <% end %>
128
+ <% end %>
129
+ <% component.with_details_cta do %>
130
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#devices" class="no-underline text-xs text-gray-400 font-normal hover:text-gray-500">Details</button>
131
+ <% end %>
132
+ <% end %>
133
+ <%= render Lookout::TileComponent.new(title: "Funnels", wide: true, classes: "p-4 m-2") do |component| %>
134
+ <% component.with_display_links do %>
135
+ <div>
136
+ <div >
137
+ <div class="flex text-xs font-medium text-gray-400 space-x-2">
138
+ <div class="relative inline-block text-left">
139
+ <a href="<%= goals_path(search_params) %>" data-turbo-frame="goals" class="inline-block h-5 font-semibold" data-controller="frame-link" data-action="click->tile#setTitle">Goals</a>
140
+ <a href="<%= properties_path(search_params) %>" data-turbo-frame="goals" class="inline-block h-5 font-semibold" data-controller="frame-link" data-action="click->tile#setTitle">Properties</a>
141
+ <div class="dropdown dropdown-end inline-block" data-controller='dropdown-label'>
142
+ <label
143
+ tabindex="0"
144
+ class="cursor-pointer inline-block h-5 font-semibold"
145
+ data-action='click->dropdown-label#removeHidden'
146
+ data-funnel-dropdown-label
147
+ data-turbo-frame="goals"
148
+ >
149
+ <span data-dropdown-label-target="label">Funnels</span>
150
+ </label>
151
+ <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" data-dropdown-label-target="close">
152
+ <% Lookout.config.funnels.each do |id, funnel| %>
153
+ <li data-action="click->dropdown-label#setLabel">
154
+ <a href="<%= funnel_path(id, search_params) %>" data-turbo-frame="goals" class="link" data-action="click->tile#setTitle" data-funnel-link title="<%= funnel.title %>">
155
+ <%= funnel.title %>
156
+ </a>
157
+ </li>
158
+ <% end %>
159
+ </ul>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ </div>
165
+ </div>
166
+ <% end %>
167
+ <% component.with_statistic_display do %>
168
+ <div class="pl-4 pb-4">
169
+ <%= turbo_frame_tag :goals, src: goals_path(search_params), loading: :lazy do %>
170
+ <%= render '/lookout/layouts/shared/tile_loader' %>
171
+ <% end %>
172
+
173
+ </div>
174
+ <% end %>
175
+ <% component.with_details_cta do %>
176
+ <button data-action="click->details-modal#openModal" data-controller="details-modal" data-details-modal-target-value="#goals" class="no-underline text-xs text-gray-400 font-normal hover:text-gray-500">Details</button>
177
+ <% end %>
178
+ <% end %>
179
+ </div>
180
+ </main>
181
+
182
+ <%= render 'lookout/roots/filters' %>
183
+
184
+ <dialog id="detailsModal" class="modal">
185
+ <div class="modal-box w-11/12 max-w-5xl">
186
+ <%= turbo_frame_tag :details %>
187
+ </div>
188
+ <form method="dialog" class="modal-backdrop">
189
+ <button>close</button>
190
+ </form>
191
+ </dialog>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::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 Lookout::TableComponent.new(items: @sources, category_name: 'Sources', unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%= turbo_frame_tag :chart do %>
2
+ <div class="p-4">
3
+ <div class="flex justify-end gap-3 items-center ">
4
+ <a href="<%= export_path(request.query_parameters) %>" class="link text-sm" target="_blank" data-turbo-frame="false">Download</a>
5
+ <%= form_with url: url_for(params.permit!), method: :get, data: { controller: "interval" } do %>
6
+ <%= select_tag :interval, options_for_select(available_intervals.collect { |interval| [interval.titleize, interval] }, selected: selected_interval), class: "select text-accent select-sm w-full max-w-sm", 'data-action': "change->interval#handleChange" %>
7
+ <% end %>
8
+ </div>
9
+ <div>
10
+ <canvas id="overlay" width="600" height="400" style="position:absolute;pointer-events:none;"></canvas>
11
+
12
+ <% if compare_mode? %>
13
+ <canvas class="h-[300px] w-full"
14
+ data-controller="line-chart"
15
+ data-line-chart-label-value="<%= @label %>"
16
+ data-line-chart-interval-value="<%= selected_interval %>"
17
+ data-line-chart-current-value="<%= @stats.current.to_json %>"
18
+ data-line-chart-comparison-value="<%= params[:comparison] %>"
19
+ data-line-chart-compared-to-value="<%= @stats.compared_to.to_json %>"
20
+ data-line-chart-metric-value="<%= metric_type(@stats) %>"
21
+ data-action="resize@window->line-chart#resize"
22
+ data-action="mouseenter->line-chart#hover"
23
+ data-action="mouseleave->line-chart#hover"
24
+ ></canvas>
25
+ <% else %>
26
+ <canvas class="h-[300px] w-full"
27
+ data-controller="line-chart"
28
+ data-line-chart-label-value="<%= @label %>"
29
+ data-line-chart-interval-value="<%= selected_interval %>"
30
+ data-line-chart-current-value="<%= @stats.to_json %>"
31
+ data-line-chart-metric-value="<%= metric_type(@stats) %>"
32
+ data-action="resize@window->line-chart#resize"
33
+ data-action="mouseenter->line-chart#hover"
34
+ data-action="mouseleave->line-chart#hover"
35
+ ></canvas>
36
+ <% end %>
37
+ </div>
38
+ </div>
39
+
40
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%= turbo_frame_tag :stats, data: { controller: "active-frame-link" } do %>
2
+ <div 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" data-controller="active-links" data-active-links-classes-value='["text-primary"]'>
3
+ <% if @presenter.send(:range).realtime? %>
4
+ <%= render stats_container(@presenter.unique_visitors, stats_unique_visitors_url(search_params), "Unique Visits (30 min)", :number_with_delimiter, true) %>
5
+ <%= render stats_container(@presenter.total_pageviews, stats_total_pageviews_path(search_params), "Total Pageviews (30 min)", :number_with_delimiter) %>
6
+ <% else %>
7
+ <%= render stats_container(@presenter.unique_visitors, stats_unique_visitors_url(search_params), "Unique Visits", :number_with_delimiter, true) %>
8
+ <%= render stats_container(@presenter.total_visits, stats_total_visits_path(search_params), "Total Visits", :number_with_delimiter) %>
9
+ <%= render stats_container(@presenter.total_pageviews, stats_total_pageviews_path(search_params), "Total Pageviews", :number_with_delimiter) %>
10
+ <%= render stats_container(@presenter.views_per_visit, stats_views_per_visits_path(search_params), "Views per Visit", :number_with_delimiter) %>
11
+ <%= render stats_container(@presenter.bounce_rate, stats_bounce_rates_path(search_params), "Bounce Rate", :number_with_delimiter) %>
12
+ <%= render stats_container(@presenter.visit_duration, stats_visit_durations_url(search_params), "Visit Duration", :number_to_duration) %>
13
+ <% end %>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::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 Lookout::TableComponent.new(items: @pages, category_name: 'Page', unit_name: 'Visitors') %>
3
+ <% end %>
data/config/routes.rb ADDED
@@ -0,0 +1,69 @@
1
+ Lookout::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
+ namespace :locations do
16
+ resource :map, only: [:show]
17
+ resources :countries, only: [:index]
18
+ resources :regions, only: [:index]
19
+ resources :cities, only: [:index]
20
+ end
21
+
22
+ resources :properties, only: [:index, :show]
23
+ resource :export, only: [:show]
24
+ resource :realtime, only: [:show]
25
+ resources :funnels, only: [:show]
26
+ resources :goals, only: [:index]
27
+ resource :stats, only: [:show]
28
+ resources :campaigns, only: [:index]
29
+ resources :sources, only: [:index]
30
+ resources :exit_pages, only: [:index]
31
+ resources :top_pages, only: [:index]
32
+ resources :entry_pages, only: [:index]
33
+
34
+ namespace :stats do
35
+ resources :unique_visitors, only: [:index]
36
+ resources :total_visits, only: [:index]
37
+ resources :total_pageviews, only: [:index]
38
+ resources :views_per_visits, only: [:index]
39
+ resources :bounce_rates, only: [:index]
40
+ resources :visit_durations, only: [:index]
41
+ end
42
+ namespace :filters do
43
+ %w{source medium term content campaign}.each do |utm|
44
+ get "utm/#{utm}s" => "utms#index", defaults: { type: "utm_#{utm}" }
45
+ end
46
+
47
+ %w{country region city}.each do |type|
48
+ get "locations/#{type.pluralize}" => "locations#index", defaults: { type: type }
49
+ end
50
+
51
+ namespace :properties do
52
+ resources :names, only: [:index]
53
+ resources :values, only: [:index]
54
+ end
55
+
56
+ resources :goals, only: [:index]
57
+ resources :sources, only: [:index]
58
+ resources :screens, only: [:index]
59
+ scope :operating_systems, module: :operating_systems do
60
+ resources :names, only: [:index]
61
+ resources :versions, only: [:index]
62
+ end
63
+ scope :pages, module: :pages do
64
+ resources :actions, only: [:index]
65
+ resources :entry_pages, only: [:index]
66
+ resources :exit_pages, only: [:index]
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,31 @@
1
+ require "rails/generators"
2
+
3
+ module Lookout
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.join(__dir__, "templates")
7
+
8
+ def copy_templates
9
+ insert_into_file ::Rails.root.join("app/models/ahoy/event.rb").to_s, " include Lookout::Ahoy::EventMethods\n", after: "class Ahoy::Event < ApplicationRecord\n"
10
+ insert_into_file ::Rails.root.join("app/models/ahoy/visit.rb").to_s, " include Lookout::Ahoy::VisitMethods\n", after: "class Ahoy::Visit < ApplicationRecord\n"
11
+
12
+ template "config.rb", "config/initializers/lookout.rb"
13
+
14
+ route "mount Lookout::Engine => '/lookout'"
15
+
16
+ # Importmap (Rails 8+): add one-liner to host app if importmap.rb exists and hasn't been configured yet
17
+ importmap_path = ::Rails.root.join("config/importmap.rb")
18
+ if File.exist?(importmap_path)
19
+ content = File.read(importmap_path)
20
+ unless content.include?("Lookout.importmap(self)")
21
+ append_to_file importmap_path.to_s, <<~RUBY
22
+
23
+ # Lookout (analytics dashboard)
24
+ Lookout.importmap(self)
25
+ RUBY
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Lookout
5
+ module Generators
6
+ class MigrationGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+ source_root File.join(__dir__, "templates")
9
+
10
+ def copy_templates
11
+ migration_template "migration.rb", "db/migrate/create_lookout_indexes.rb", migration_version: migration_version
12
+ end
13
+
14
+ private
15
+
16
+ def migration_version
17
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,185 @@
1
+ Lookout.configure do |config|
2
+ # ==> Navigation
3
+ #
4
+ # Return path for the back link in the dashboard header
5
+ # Default: "/"
6
+ # config.return_path = "/admin"
7
+
8
+ # Return link text (can include HTML entities)
9
+ # Default: "← Back"
10
+ # config.return_copy = "← Dashboard"
11
+ # config.return_copy = "⟵ Admin Panel"
12
+
13
+ # ==> Event tracking
14
+ #
15
+ # View name
16
+ # The event you use to dictate if a page view occurred
17
+ # config.event.view_name = "$view"
18
+ #
19
+ # URL column
20
+ # The properties that indicate what URL was viewed. Ahoy suggested tracking the
21
+ # controller and action for each view by default, so we use that here.
22
+ # This is automatically adapted for your database (PostgreSQL or SQLite).
23
+ # Default configuration extracts: properties['controller'] + '#' + properties['action']
24
+ #
25
+ # If you have a `url` key in your `properties`, you can customize it:
26
+ # For PostgreSQL:
27
+ # config.event.url_column = "properties->>'url'"
28
+ # For SQLite:
29
+ # config.event.url_column = "JSON_EXTRACT(properties, '$.url')"
30
+ #
31
+ # URL exists
32
+ # A query that indicates if a view event has the correct properties for a page view.
33
+ # This is also automatically adapted for your database.
34
+ #
35
+ # ==> Models
36
+ #
37
+ # Ahoy::Event model
38
+ # config.models.event = '::Ahoy::Event'
39
+ #
40
+ # Ahoy::Visit model
41
+ # config.models.visit = '::Ahoy::Visit'
42
+ #
43
+ #
44
+ # ==> Theme
45
+ #
46
+ # https://daisyui.com/docs/themes/
47
+ # config.theme = "dark"
48
+
49
+ # ==> Disabled widgets
50
+ # Some widgets are more expensive than others. You can disable them here.
51
+ #
52
+ # Here's the list of widgets:
53
+ # * sources
54
+ # * campaigns.utm_medium
55
+ # * campaigns.utm_source
56
+ # * campaigns.utm_term
57
+ # * campaigns.utm_content
58
+ # * campaigns.utm_campaign
59
+ # * top_pages
60
+ # * entry_pages
61
+ # * landing_pages
62
+ # * locations.countries
63
+ # * locations.regions
64
+ # * locations.cities
65
+ # * devices.browsers
66
+ # * devices.operating_systems
67
+ # * devices.device_types
68
+ #
69
+ # config.disabled_widgets = []
70
+
71
+ # ==> Time periods
72
+ #
73
+ # Defaults come from lib/lookout/period_collection.rb
74
+ #
75
+ # If you want your own entirely, first call reset.
76
+ # config.ranges.reset
77
+ #
78
+ # Then you can add your own.
79
+ # config.ranges.add :param_name, "Label", -> { [3.days.ago, Date.today] }
80
+ #
81
+ # You can also remove an existing one:
82
+ # config.ranges.delete(:mtd)
83
+ #
84
+ # Or add to the defaults:
85
+ # config.ranges.add :custom, "Custom", -> { [6.hours.ago, 2.minutes.ago] }
86
+ #
87
+ # Or overwrite the defaults:
88
+ # config.ranges.add :mtd, "Custom MTD", -> { [2.weeks.ago, Time.current] }
89
+ #
90
+ # And handle the default range, which will be used if no range is given:
91
+ # config.ranges.default = '3d'
92
+ #
93
+ # The max range if a custom range is sent
94
+ # config.ranges.max = 180.days
95
+ #
96
+ # Set to false to disable custom ranges
97
+ # config.ranges.custom = true
98
+ #
99
+ # For an interval to be considered "realtime" it must not have a secondary item in the range
100
+
101
+ # ==> Filters
102
+ #
103
+ # Defaults come from lib/lookout/filters_configuration.rb
104
+ #
105
+ # If you want your own entirely, first call reset.
106
+ # config.filters.reset
107
+ #
108
+ # Then you can add your own.
109
+ #
110
+ # config.filters.register "Group label" do
111
+ # filter label: "Some label", column: :column_name, url: :url_for_options, predicates: [:in, :not_in], multiple: true
112
+ # end
113
+ #
114
+ # You can also remove an existing group:
115
+ #
116
+ # config.filters.delete("Group label")
117
+ #
118
+ # Remove a specific filter from a group:
119
+ #
120
+ # config.filters["Group label"].delete(:column_name)
121
+ #
122
+ # You can add to an existing group:
123
+ #
124
+ # config.filters["Group label"].filter label: "Some label", column: :column_name, url: :url_for_options, predicates: [:in, :not_in], multiple: true
125
+
126
+ # ==> Caching
127
+ # config.cache.enabled = false
128
+ #
129
+ # Cache store should be an ActiveSupport::Cache::Store instance
130
+ # config.cache.store = Rails.cache
131
+ #
132
+ # TTL
133
+ # config.cache.ttl = 1.minute
134
+
135
+ #==> Goal tracking
136
+ # Your mother told you to have goals. Track those goals.
137
+ #
138
+ # Basically:
139
+ #
140
+ # config.goal :unique_id do
141
+ # label "Some label here"
142
+ # name "The event name you're tracking in your Ahoy::Event table"
143
+ # end
144
+ #
145
+ # Real-world example:
146
+ #
147
+ # config.goal :appointment_paid do
148
+ # label "Appointment Paid"
149
+ # name "$appointment.paid"
150
+ # end
151
+ #
152
+ # You can also use queries:
153
+ #
154
+ # config.goal :appointment_paid do
155
+ # label "Appointment Paid"
156
+ # query do
157
+ # ::Ahoy::Event.where(...)
158
+ # end
159
+ # end
160
+
161
+ # ==> Funnels
162
+ # Your mother definitely didn't tell you about conversion rate.
163
+ # Except, you're here, so...
164
+ #
165
+ # Basically:
166
+ #
167
+ # config.funnel :id do
168
+ # label "Some label"
169
+ # goal :goal_id_1
170
+ # goal :goal_id_2
171
+ # end
172
+ #
173
+ # Real-world example:
174
+ #
175
+ # config.funnel :appointments do
176
+ # label "Appointment Workflow"
177
+ # goal :appointment_created
178
+ # goal :appointment_paid
179
+ # end
180
+ #
181
+ # => Realtime interval
182
+ # config.realtime_interval = 30.seconds
183
+ #
184
+ # How frequently the page should refresh if the interval is realtime
185
+ end
@@ -0,0 +1,7 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_index :<%= ::Lookout.visit.table_name %>, :started_at unless index_exists?(:<%= ::Lookout.visit.table_name %>, :started_at)
4
+ add_index :<%= ::Lookout.event.table_name %>, :visit_id unless index_exists?(:<%= ::Lookout.event.table_name %>, :visit_id)
5
+ add_index :<%= ::Lookout.event.table_name %>, :time unless index_exists?(:<%= ::Lookout.event.table_name %>, :time)
6
+ end
7
+ end