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,19 @@
1
+ module Lookout
2
+ module CompareMode
3
+ def self.included(klass)
4
+ if klass < ActionController::Base
5
+ klass.helper_method :compare_mode?
6
+ klass.helper_method :comparison_label
7
+ end
8
+ end
9
+
10
+ # doesn't work for realtime and realtime doesn't need a secondary range
11
+ def compare_mode?
12
+ comparison_mode.enabled?
13
+ end
14
+
15
+ def comparison_mode
16
+ @comparison_mode ||= ComparisonMode.new(params)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Lookout
2
+ module Limitable
3
+ private
4
+
5
+ def limit
6
+ if request.variant.include?(:details)
7
+ nil
8
+ else
9
+ if params[:limit]
10
+ params[:limit].to_i
11
+ else
12
+ 10
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module Lookout
2
+ module RangeOptions
3
+
4
+ private def range
5
+ RangeFromParams.from_params(params)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,72 @@
1
+ module Lookout
2
+ class ComparisonMode
3
+ VALID_COMPARISONS = %w{previous year true}
4
+
5
+ include RangeOptions
6
+ include Rangeable
7
+
8
+ attr_reader :params
9
+ def initialize(params)
10
+ @params = params
11
+ end
12
+
13
+ # can't compare realtime
14
+ def enabled?(strict = true)
15
+ comparing = (params[:comparison].in?(VALID_COMPARISONS) || custom_compare?)
16
+ if strict
17
+ comparing && !range.realtime?
18
+ else
19
+ comparing
20
+ end
21
+ end
22
+
23
+ def label
24
+ if custom_compare?
25
+ return "Custom period"
26
+ end
27
+
28
+ if type == :true || type == :previous
29
+ "Previous period"
30
+ elsif type == :year
31
+ "Year-over-year"
32
+ else
33
+ raise ArgumentError
34
+ end
35
+ end
36
+
37
+ def compared_to_range
38
+ return custom_compare if custom_compare?
39
+
40
+ if type == :true || type == :previous
41
+ RangeFromParams.new(period: nil, start_date: (range[0] - (range[1] - range[0])).utc.to_s, end_date: range[0].utc.to_s).build
42
+ elsif type == :year
43
+ RangeFromParams.new(period: nil, start_date: range[0].change(year: range[0].year - 1).utc.to_s, end_date: range[1].change(year: range[1].year - 1).utc.to_s).build
44
+ else
45
+ raise ArgumentError
46
+ end
47
+ end
48
+
49
+ def type
50
+ return params[:comparison].to_sym if params[:comparison].in?(VALID_COMPARISONS)
51
+ return :custom if custom_compare?
52
+
53
+ nil
54
+ end
55
+
56
+ def match_to
57
+ return params[:compare_to].to_sym if params[:compare_to].in?(%w{dow date})
58
+
59
+ nil
60
+ end
61
+
62
+ def custom_compare
63
+ return nil unless (params[:compare_to_start_date].present? && params[:compare_to_end_date].present?)
64
+
65
+ RangeFromParams.new(period: nil, start_date: params[:compare_to_start_date], end_date: params[:compare_to_end_date]).build
66
+ end
67
+
68
+ def custom_compare?
69
+ custom_compare
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,48 @@
1
+ module Lookout
2
+ class Export
3
+ def initialize(params, context)
4
+ @params = params
5
+ @context = context
6
+ @files = {}
7
+ end
8
+
9
+ def build
10
+ @files["browsers.csv"] = to_csv(DeviceQuery.call(merged_params(devices_type: "browser")), DeviceDecorator)
11
+ @files["cities.csv"] = to_csv(CityQuery.call(merged_params), CityDecorator)
12
+ @files["countries.csv"] = to_csv(CountryQuery.call(merged_params), CountryDecorator)
13
+ @files["devices.csv"] = to_csv(DeviceQuery.call(merged_params(devices_type: :device_type)), DeviceDecorator)
14
+ @files["entry_pages.csv"] = to_csv(EntryPagesQuery.call(merged_params), EntryPageDecorator)
15
+ @files["exit_pages.csv"] = to_csv(ExitPagesQuery.call(merged_params), ExitPageDecorator)
16
+ @files["operating_systems.csv"] = to_csv(DeviceQuery.call(merged_params(devices_type: "os")), DeviceDecorator)
17
+ @files["top_pages.csv"] = to_csv(TopPageQuery.call(merged_params), TopPageDecorator)
18
+ @files["regions.csv"] = to_csv(RegionQuery.call(merged_params), RegionDecorator)
19
+ @files["sources.csv"] = to_csv(SourceQuery.call(merged_params), SourceDecorator)
20
+ ["campaign", "content", "medium", "source", "term"].each do |utm|
21
+ @files["utm_#{utm.pluralize}.csv"] = to_csv(CampaignQuery.call(merged_params(campaigns_type: "utm_#{utm}")), CampaignDecorator)
22
+ end
23
+ self
24
+ end
25
+
26
+ def to_zip
27
+ zip_stream = Zip::OutputStream.write_buffer do |zip|
28
+ @files.each do |filename, csv|
29
+ zip.put_next_entry(filename)
30
+ zip.write(csv)
31
+ end
32
+ end
33
+
34
+ zip_stream.rewind
35
+ zip_stream
36
+ end
37
+
38
+ private
39
+
40
+ def to_csv(query, decorator)
41
+ decorator.to_csv(query, @context)
42
+ end
43
+
44
+ def merged_params(params_to_merge = {})
45
+ @params.dup.merge(params_to_merge)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,82 @@
1
+ module Lookout
2
+ class FilterParser
3
+ FILTER_MENU_MAX_SIZE = 2
4
+ class Item
5
+ attr_accessor :name, :column, :description, :values, :predicate, :url, :modal, :label
6
+
7
+ def title
8
+ column.titleize
9
+ end
10
+ end
11
+
12
+ def self.parse(request)
13
+ new(request).tap do |instance|
14
+ instance.parse
15
+ end
16
+ end
17
+
18
+ delegate_missing_to :@items
19
+
20
+ def initialize(request)
21
+ @request = request
22
+ @params = @request.params
23
+ @filter_params = @request.params[:q] || {}
24
+ @items = {}
25
+ end
26
+
27
+ def parse
28
+ @filter_params.each do |key, values|
29
+ next if ::Lookout.event.ransackable_scopes.include?(key.to_sym)
30
+
31
+ item = build_item(key, values)
32
+ @items[key] = item
33
+ end
34
+
35
+ @items
36
+ end
37
+
38
+ private
39
+
40
+ def build_item(key, values)
41
+ item = Item.new
42
+ item.values = Array(values)
43
+
44
+ item.predicate = Ransack::Predicate.detect_and_strip_from_string!(key.dup)
45
+ item.column = key.delete_suffix("_#{item.predicate}")
46
+ modal_name = Lookout.config.filters.detect { |_, filters| filters.include?(item.column) }
47
+ if modal_name
48
+ item.modal = modal_name[1].modal_name
49
+ end
50
+
51
+ label = if item.column == "goal"
52
+ item.values.map { |value| Lookout.config.goals[value].title }
53
+ else
54
+ item.values
55
+ end.to_sentence(last_word_connector: " or ")
56
+
57
+ item.label = if key.start_with?("properties.")
58
+ item.modal = "customPropertyFilterModal"
59
+ item.column = item.column.dup.delete_prefix("properties.")
60
+ label
61
+ else
62
+ label
63
+ end
64
+
65
+ item.description = "#{item.label} #{::Lookout::PredicateLabel[item.predicate]} #{label}"
66
+ item.url = build_url(key, values)
67
+ item
68
+ end
69
+
70
+ def build_url(name, values)
71
+ search_params = @request.query_parameters.deep_dup
72
+ if search_params["q"][name].is_a?(Array)
73
+ search_params["q"][name] = search_params["q"][name] - Array(values)
74
+ else
75
+ search_params["q"].delete(name)
76
+ end
77
+
78
+ @request.path + "?" + search_params.to_query
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,78 @@
1
+ module Lookout
2
+ class RangeFromParams
3
+ def self.from_params(params)
4
+ compare = ComparisonMode.new(params)
5
+ new(period: params[:period], start_date: params[:start_date], end_date: params[:end_date], date: params[:date], comparison: compare.enabled?(false), raw: params).tap { |instance| instance.build }
6
+ end
7
+
8
+ attr_reader :params, :range
9
+ def initialize(period: Lookout.config.ranges.default, start_date: nil, end_date: nil, date: nil, comparison: false, raw: {})
10
+ @period = period || Lookout.config.ranges.default
11
+ @start_date = start_date
12
+ @end_date = end_date
13
+ @date = date
14
+ @range = nil
15
+ @comparison = comparison
16
+ @raw = raw
17
+ end
18
+
19
+ def build
20
+ if (@start_date.present? && @end_date.present?) || @date.present?
21
+ if @date
22
+ @start_date ||= @date.to_datetime.beginning_of_day
23
+ @end_date ||= @date.to_datetime.end_of_day
24
+ end
25
+
26
+ custom = [@start_date.to_datetime, @end_date.to_datetime].sort
27
+ duration = (custom[1].to_date - custom[0].to_date)
28
+
29
+ # if Lookout.config.ranges.max && (duration.days <= Lookout.config.ranges.max)
30
+ @range = Range.new(custom[0].utc, custom[1].utc)
31
+ return self
32
+ # end
33
+ end
34
+
35
+ @range = Range.new(selected_period[0], selected_period[1])
36
+ self
37
+ end
38
+
39
+ def starts_at
40
+ Time.at(@range.min)
41
+ end
42
+
43
+ def ends_at
44
+ if realtime?
45
+ Time.current
46
+ else
47
+ Time.at(@range.max)
48
+ end
49
+ end
50
+
51
+ def realtime?
52
+ @range.end.nil?
53
+ end
54
+
55
+ def custom?
56
+ @raw[:start_date].present? && @raw[:end_date].present?
57
+ end
58
+
59
+ def [](value)
60
+ if value == 0
61
+ starts_at
62
+ elsif value == 1
63
+ ends_at
64
+ else
65
+ raise NoMethodError
66
+ end
67
+ end
68
+
69
+ # return an integer-based range which works with step
70
+ def numeric
71
+ @numeric ||= Range.new(starts_at.to_i, ends_at.to_i)
72
+ end
73
+
74
+ def selected_period
75
+ Lookout.config.ranges.for(@period) || Lookout.config.ranges.default
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,7 @@
1
+ module Lookout
2
+ module Rangeable
3
+ def period
4
+ params[:period] || Lookout.config.ranges.default
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module Lookout
2
+ class Widget
3
+ class WidgetDisabled < StandardError
4
+ attr_reader :frame
5
+ def initialize(msg, frame = nil)
6
+ @frame = frame
7
+ super(msg)
8
+ end
9
+ end
10
+
11
+ def self.disabled?(*names)
12
+ Lookout.config.disabled_widgets.include?(names.join("."))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Lookout
2
+ class DashboardPresenter
3
+ include Rangeable
4
+ include RangeOptions
5
+ include CompareMode
6
+
7
+ attr_reader :params
8
+
9
+ def initialize(params)
10
+ @params = params
11
+ end
12
+
13
+ def unique_visitors
14
+ Stats::UniqueVisitorsQuery.call(params).with_comparison(compare_mode?).count
15
+ end
16
+
17
+ def total_visits
18
+ Stats::TotalVisitorsQuery.call(params).with_comparison(compare_mode?).count
19
+ end
20
+
21
+ def total_pageviews
22
+ Stats::TotalPageviewsQuery.call(params).with_comparison(compare_mode?).count
23
+ end
24
+
25
+ def views_per_visit
26
+ Stats::AverageViewsPerVisitQuery.call(params).with_comparison(compare_mode?).average("count")
27
+ end
28
+
29
+ def bounce_rate
30
+ query = Stats::BounceRatesQuery.call(params)
31
+ if compare_mode?
32
+ query.with_comparison(true).average("bounce_rate")
33
+ else
34
+ query.average("bounce_rate").try(:round, 2) || 0
35
+ end
36
+ end
37
+
38
+ def visit_duration
39
+ query = Stats::AverageVisitDurationQuery.call(params)
40
+ if compare_mode?
41
+ query.with_comparison(true)
42
+ else
43
+ query[0].average_visit_duration
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def compare_mode?
50
+ params[:comparison] != 'false'
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,75 @@
1
+ module Lookout
2
+ # this is incredibly naive and needs some tlc
3
+ class FunnelPresenter
4
+
5
+ attr_reader :steps
6
+ def initialize(funnel, event_query)
7
+ @funnel = funnel
8
+ @event_query = event_query.joins(:visit)
9
+ end
10
+
11
+ def build
12
+ if Lookout.config.goals.none? || @funnel.goals.none?
13
+ @steps = []
14
+ return self
15
+ end
16
+
17
+ queries = {
18
+ totals: @event_query.select("count(distinct(#{Lookout.event.table_name}.visit_id)) as unique_visits, '_internal_total_visits_' as name, count(distinct #{Lookout.event.table_name}.id) as total_events, 0 as sort_order")
19
+ }
20
+ selects = ["SELECT unique_visits, name, total_events, sort_order from totals"]
21
+ last_goal = nil
22
+ map = {}.with_indifferent_access
23
+
24
+ # Use funnel's goals in order, not all configured goals
25
+ @funnel.goals.each_with_index do |goal, index|
26
+ queries[goal.id] = @event_query.select("count(distinct(#{Lookout.event.table_name}.visit_id)) as unique_visits, '#{goal.id}' as name, count(distinct #{Lookout.event.table_name}.id) as total_events, #{index + 1} as sort_order").merge(goal.event_query.call).group("#{Lookout.event.table_name}.name")
27
+ selects << ["SELECT unique_visits, name, total_events, sort_order from #{goal.id}"]
28
+ map[goal.id] = goal
29
+ last_goal = goal
30
+ end
31
+
32
+ # activerecord quirk / with bug
33
+ select = selects.join(" UNION ").delete_suffix(" from #{last_goal.id}")
34
+ select = select.delete_prefix("SELECT ")
35
+ steps = ::Ahoy::Event.with(
36
+ queries,
37
+ ).select(select).from("#{last_goal.id}").order("sort_order asc")
38
+
39
+ # Cast to numeric/real for division depending on database
40
+ cast_type = Lookout::DatabaseAdapter.postgresql? ? "::numeric" : ""
41
+ cast_division = Lookout::DatabaseAdapter.postgresql? ?
42
+ "total_events::numeric/lag(total_events, 1) over ()" :
43
+ "CAST(total_events AS REAL)/lag(total_events, 1) over ()"
44
+
45
+ items = ::Ahoy::Event.with(steps: steps).select("total_events, unique_visits, name, round((#{cast_division}),2) as drop_off").from("steps").order("sort_order asc").index_by(&:name)
46
+ items.delete("_internal_total_visits_")
47
+ @steps = []
48
+
49
+ items.values.each do |item|
50
+ if map[item.name]
51
+ item.name = map[item.name].title
52
+ end
53
+ end
54
+
55
+ @steps = items.values
56
+ self
57
+ end
58
+
59
+
60
+ def total
61
+ @event_query.distinct(:visitor_token).count
62
+ end
63
+
64
+ def as_json
65
+ {
66
+ steps: @steps.as_json,
67
+ total: total
68
+ }
69
+ end
70
+
71
+ def to_json
72
+ as_json.to_json
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,72 @@
1
+ module Lookout
2
+ class GoalsPresenter
3
+ attr_reader :goals
4
+ def initialize(event_query)
5
+ @event_query = event_query
6
+ @goals = nil
7
+ end
8
+
9
+ # this is a dumpster fire
10
+ def build
11
+ if Lookout.config.goals.none?
12
+ @goals = []
13
+ return self
14
+ end
15
+
16
+ queries = {
17
+ totals: @event_query.select("count(distinct(#{Lookout.event.table_name}.visit_id)) as unique_visits, '_internal_total_visits_' as name, count(distinct #{Lookout.event.table_name}.id) as total_events, 0 as sort_order")
18
+ }
19
+ selects = ["SELECT unique_visits, name, total_events, sort_order, 0 as cr, '' as goal_id from totals"]
20
+ last_goal = nil
21
+ map = {}.with_indifferent_access
22
+
23
+ Lookout.config.goals.each_with_index do |goal, index|
24
+ queries[goal.id] = @event_query.select(
25
+ [
26
+ "count(distinct(#{Lookout.event.table_name}.visit_id)) as unique_visits" ,
27
+ "'#{goal.id}' as name",
28
+ "count(distinct #{Lookout.event.table_name}.id) as total_events",
29
+ "#{index + 1} as sort_order",
30
+ "'#{goal.id}' as goal_id"
31
+ ]
32
+ ).merge(goal.event_query.call).group("#{Lookout.event.table_name}.name")
33
+ # Cast 0 to decimal/real depending on database
34
+ zero_decimal = Lookout::DatabaseAdapter.postgresql? ? "0::decimal" : "0.0"
35
+ selects << ["SELECT unique_visits, name, total_events, sort_order, #{zero_decimal} as cr, '#{goal.id}' as goal_id from #{goal.id}"]
36
+ map[goal.id] = goal
37
+ last_goal = goal
38
+ end
39
+
40
+ # activerecord quirk / with bug
41
+ select = selects.join(" UNION ").delete_suffix(" from #{last_goal.id}")
42
+ select = select.delete_prefix("SELECT ")
43
+ steps = ::Ahoy::Event.with(
44
+ queries,
45
+ ).select(select).from("#{last_goal.id}").order("sort_order asc").index_by(&:name)
46
+ totals = steps.delete("_internal_total_visits_")
47
+
48
+ @goals = steps.keys.collect do |name|
49
+ step = steps[name]
50
+ step.name = map[name].title
51
+ step.cr = ((step.total_events.to_d / totals.total_events.to_d) * 100).round(2)
52
+ step
53
+ end
54
+ self
55
+ end
56
+
57
+ def total_visitors
58
+ @total_visitors ||= @event_query.select(:visit_id).distinct.count
59
+ end
60
+
61
+ def as_json
62
+ {
63
+ steps: @steps.as_json,
64
+ total: total
65
+ }
66
+ end
67
+
68
+ def to_json
69
+ as_json.to_json
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,25 @@
1
+ module Lookout
2
+ module ComparableQueries
3
+ def compare_range
4
+ @compare_range ||= begin
5
+ ComparisonMode.new(@params).compared_to_range
6
+ end
7
+ end
8
+
9
+ def range
10
+ @range ||= @query.send(:range)
11
+ end
12
+
13
+ def comparison_params
14
+ params = @params.deep_dup
15
+ params.delete("period")
16
+
17
+ params[:start_date] = compare_range[0]
18
+ params[:end_date] = compare_range[1]
19
+ params
20
+ end
21
+ end
22
+ end
23
+
24
+ require_relative './lazy_comparable_query'
25
+ require_relative './comparable_query'