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,152 @@
1
+ module Lookout
2
+ module ComparableQuery
3
+ class Comparison
4
+ class ComparisonResult
5
+ attr_reader :current, :compared_to
6
+ def initialize(current, compared_to)
7
+ @current = current
8
+ @compared_to = compared_to
9
+ end
10
+ end
11
+
12
+ def initialize(query)
13
+ @query = query
14
+ @params = @query.params.deep_dup
15
+ @compare = @query.class.call(comparison_params)
16
+ @model = @query.all.klass
17
+ @query_class = @query.class
18
+ end
19
+
20
+ def count(column_name = :id)
21
+ @operation = :count
22
+ @column = column_name
23
+ perform_calculations(:count, column_name)
24
+
25
+ self
26
+ end
27
+
28
+ def average(column_name)
29
+ @operation = :average
30
+ @column = column_name
31
+
32
+ perform_calculations(:average, column_name)
33
+
34
+ self
35
+ end
36
+
37
+ def perform_calculations(operation, column_name)
38
+ @query = perform_calculation(@query, operation, column_name)
39
+ @compare = perform_calculation(@compare, operation, column_name)
40
+ end
41
+
42
+ def perform_calculation(q, operation, column_name)
43
+ operation = operation.to_s.downcase
44
+ # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
45
+ # considered distinct.
46
+ distinct = q.send(:distinct_value)
47
+
48
+ if operation == "count"
49
+ column_name ||= q.send(:select_for_count)
50
+ if column_name == :all
51
+ if !distinct
52
+ distinct = q.send(:distinct_select?, :select_for_count) if q.group_values.empty?
53
+ elsif q.send(:group_values).any? || q.send(:select_values).empty? && q.order_values.empty?
54
+ column_name = q.primary_key
55
+ end
56
+ elsif q.all.send(:distinct_select?, column_name)
57
+ distinct = nil
58
+ end
59
+ end
60
+
61
+ if q.group_values.any?
62
+ raise "use a subquery"
63
+ else
64
+ execute_simple_calculation(q, operation, column_name, distinct)
65
+ end
66
+ end
67
+
68
+ def result
69
+ type = @query_class.cast_type(@column)
70
+
71
+ if Lookout::DatabaseAdapter.sqlite?
72
+ # SQLite: Use scalar subselects to avoid selecting bare CTE names
73
+ sql = "SELECT (#{@query.to_sql}) AS current, (#{@compare.to_sql}) AS compare"
74
+ row = @model.find_by_sql(sql).first
75
+ if row
76
+ current = @query_class.cast_value(type, row["current"].to_s)
77
+ compare = @query_class.cast_value(type, row["compare"].to_s)
78
+ else
79
+ current = @query_class.cast_value(type, '0')
80
+ compare = @query_class.cast_value(type, '0')
81
+ end
82
+ else
83
+ result = @model.with(
84
+ current: @query.to_sql,
85
+ compare: @compare.to_sql
86
+ ).select("current, compare").from("current, compare")[0]
87
+
88
+ if result
89
+ current = @query_class.cast_value(type, result.current[1...-1])
90
+ compare = @query_class.cast_value(type, result.compare[1...-1])
91
+ else
92
+ current = @query_class.cast_value(type, '0')
93
+ compare = @query_class.cast_value(type, '0')
94
+ end
95
+ end
96
+
97
+ @result = ComparisonResult.new((current), (compare))
98
+ end
99
+
100
+ def compare_range
101
+ @compare_range ||= begin
102
+ og_range = range
103
+ [og_range[0] - (og_range[1] - og_range[0]), og_range[0]]
104
+ end
105
+
106
+ end
107
+
108
+ def range
109
+ @range ||= @query.send(:range)
110
+ end
111
+
112
+ private
113
+
114
+ def execute_simple_calculation(q, operation, column_name, distinct) # :nodoc:
115
+ if operation == "count" && (column_name == :all && distinct || q.has_limit_or_offset?)
116
+ # Shortcut when limit is zero.
117
+ return 0 if q.limit_value == 0
118
+
119
+ q.send(:build_count_subquery, q.spawn, column_name, distinct)
120
+ else
121
+ # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
122
+ relation = q.all.unscope(:order).distinct!(false)
123
+
124
+ column = q.all.send(:aggregate_column, column_name)
125
+ select_value = q.all.send(:operation_over_aggregate_column, column, operation, distinct)
126
+ select_value.distinct = true if operation == "sum" && distinct
127
+
128
+ relation.select_values = [select_value]
129
+
130
+ relation.arel
131
+ end
132
+ end
133
+
134
+ def comparison_params
135
+ params = @params.deep_dup
136
+ params.delete("period")
137
+
138
+ params[:start_date] = compare_range[0]
139
+ params[:end_date] = compare_range[1]
140
+ params
141
+ end
142
+ end
143
+
144
+ def with_comparison(enabled = false)
145
+ if enabled
146
+ Comparison.new(self)
147
+ else
148
+ self
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,42 @@
1
+ module Lookout
2
+ module LazyComparableQuery
3
+ class LazyComparison
4
+ class LazyComparisonResult
5
+ attr_accessor :current, :compared_to
6
+ end
7
+
8
+ include ::Lookout::ComparableQueries
9
+
10
+ def initialize(query)
11
+ @query = query
12
+ @params = @query.params.deep_dup
13
+ @compare = @query.class.call(comparison_params)
14
+ end
15
+
16
+ def method_missing(name, *args)
17
+ perform_operations(name, *args)
18
+ end
19
+
20
+ def perform_operations(name, *args)
21
+ @query = @query.public_send(name, *args)
22
+ @compare = @compare.public_send(name, *args)
23
+
24
+ self
25
+ end
26
+
27
+ def result
28
+ return @result if @result
29
+
30
+ @result = LazyComparisonResult.new.tap { |comparison| comparison.current = @query; comparison.compared_to = @compare }
31
+ end
32
+ end
33
+
34
+ def with_lazy_comparison(enabled = false)
35
+ if enabled
36
+ LazyComparison.new(self)
37
+ else
38
+ self
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,186 @@
1
+ require_relative '../concerns/lookout/comparable_queries'
2
+
3
+ module Lookout
4
+ class ApplicationQuery
5
+ # if you want to enforce returning a relation
6
+ class_attribute :strict, default: true
7
+
8
+ delegate_missing_to :@query
9
+
10
+ def self.inherited(klass)
11
+ klass.protected_methods :build
12
+ klass.private_methods :call
13
+ end
14
+
15
+ def self.call(params, query = nil)
16
+ new(params, query).send(:call)
17
+ end
18
+
19
+ attr_reader :params
20
+ def initialize(params, query)
21
+ @params = params
22
+ @query = query
23
+ end
24
+
25
+ def inspect
26
+ "<#{self.class.name}>"
27
+ end
28
+ protected
29
+
30
+ def build
31
+ raise NotImplementedError
32
+ end
33
+
34
+ private
35
+
36
+ def call
37
+ @query = build
38
+
39
+ if self.class.strict?
40
+ if @query.is_a?(ActiveRecord::Relation) || @query.class != self.class
41
+ else
42
+ raise ArgumentError, "#{self.class.name} has strict enabled, and should return a relation. Returned a #{@query.class}."
43
+ end
44
+ end
45
+
46
+ self
47
+ end
48
+
49
+ def visit_query
50
+ VisitQuery.call(params)
51
+ end
52
+
53
+ def event_query
54
+ EventQuery.call(params)
55
+ end
56
+
57
+ # this could be better
58
+ def ransack_params_for(type)
59
+ ransackable_params = {}
60
+
61
+ if params[:q]
62
+ ransackable_attributes = {
63
+ visit: (Lookout.visit.ransackable_attributes + Lookout.visit.ransackable_scopes).map(&:to_s),
64
+ event: (Lookout.event.ransackable_attributes + Lookout.event.ransackable_scopes).map(&:to_s),
65
+ }
66
+
67
+ pattern = /(?:#{Ransack.predicates.sorted_names_with_underscores.to_h.values.join("|")})$/
68
+ params[:q].each do |key, value|
69
+ attribute_name = key.gsub(pattern, '')
70
+ if type == :event && (ransackable_attributes[:visit].include?(attribute_name) || ransackable_attributes[:visit].include?(key))
71
+ ransackable_params["visit_#{key}"] = value
72
+ elsif type == :visit && (ransackable_attributes[:event].include?(attribute_name) || ransackable_attributes[:event].include?(key))
73
+ ransackable_params["events_#{key}"] = value
74
+ else
75
+ ransackable_params[key] = value
76
+ end
77
+ end
78
+ end
79
+
80
+ merge_params = {}
81
+ ransackable_params.each do |k,v|
82
+ transform = false
83
+ if v == Lookout.none.value
84
+ transform = true
85
+ elsif v.is_a?(Array) && v[0] == Lookout.none.value
86
+ transform = true
87
+ end
88
+
89
+ if transform
90
+ key = k.dup
91
+ ransackable_params.delete(key)
92
+ predicate = Ransack::Predicate.detect_and_strip_from_string!(key)
93
+ if predicate.include?("not")
94
+ merge_params["#{key}_not_null"] = '1'
95
+ else
96
+ merge_params["#{key}_null"] = '1'
97
+ end
98
+ end
99
+ end
100
+
101
+ ransackable_params.merge!(merge_params)
102
+ # send the right format
103
+ # ::Ahoy::Visit.ransack(events_time_lt: Time.now).result.to_sql
104
+ # is not
105
+ # ::Ahoy::Visit.ransack(events_time_lt: Time.now.to_i).result.to_sql
106
+ if range
107
+ if type == :event
108
+ if range.realtime?
109
+ ransackable_params['time_gt'] = range[0]
110
+ ransackable_params["visit_started_at_gt"] = range[0]
111
+ else
112
+ ransackable_params['time_gt'] = range[0]
113
+ ransackable_params['time_lt'] = range[1]
114
+ ransackable_params["visit_started_at_gt"] = range[0]
115
+ ransackable_params["visit_started_at_lt"] = range[1]
116
+ end
117
+ elsif type == :visit
118
+ if range.realtime?
119
+ ransackable_params["started_at_gt"] = range[0]
120
+ ransackable_params["events_time_gt"] = range[0]
121
+ else
122
+ ransackable_params["started_at_gt"] = range[0]
123
+ ransackable_params["started_at_lt"] = range[1]
124
+ ransackable_params["events_time_gteq"] = range[0]
125
+ ransackable_params["events_time_lteq"] = range[1]
126
+ end
127
+ end
128
+ end
129
+
130
+ ransackify(ransackable_params, type)
131
+ end
132
+
133
+ # merge both sets of ransackable params and ensure that they're being set on the correct association
134
+ # if we have params that reach across associations
135
+ def ransack_params
136
+ if self.class.name == "Lookout::EventQuery"
137
+ ransack_params_for(:event)
138
+ elsif self.class.name == "Lookout::VisitQuery"
139
+ ransack_params_for(:visit)
140
+ else
141
+ raise ArgumentError, "use ransack_params_for(type)"
142
+ end
143
+ end
144
+
145
+ def ransackify(query, type)
146
+ return unless query
147
+
148
+ query = query.try(:permit!).try(:to_h) unless query.is_a?(Hash)
149
+ obj = query.each_with_object({}) do |(k, v), obj|
150
+ if k.starts_with?('properties.')
151
+ field = k.split('properties.').last
152
+ operation = Ransack::Predicate.detect_and_strip_from_string!(field)
153
+
154
+ raise ArgumentError, "No valid predicate for #{field}" unless operation
155
+
156
+ prefix = type == :event ? "" : "events_"
157
+ obj[:c] ||= []
158
+
159
+ obj[:c] << {
160
+ a: {
161
+ '0' => {
162
+ name: "#{prefix}properties",
163
+ ransacker_args: field
164
+ }
165
+ },
166
+ p: operation,
167
+ v: [v]
168
+ }
169
+
170
+ else
171
+ obj[k] = v
172
+ end
173
+ end
174
+
175
+ if type == :event
176
+ return obj
177
+ else
178
+ return obj
179
+ end
180
+ end
181
+
182
+ def range
183
+ RangeFromParams.from_params(params)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,14 @@
1
+ module Lookout
2
+ class CampaignQuery < ApplicationQuery
3
+ def build
4
+ visit_query
5
+ .select(
6
+ "COALESCE(#{params[:campaigns_type]}, 'Direct/None') as label",
7
+ "count(COALESCE(#{params[:campaigns_type]}, 'Direct/None')) as count",
8
+ "sum(count(COALESCE(#{params[:campaigns_type]}, 'Direct/None'))) OVER() as total_count"
9
+ )
10
+ .group("COALESCE(#{params[:campaigns_type]}, 'Direct/None')")
11
+ .order(Arel.sql("count(COALESCE(#{params[:campaigns_type]}, 'Direct/None')) desc"))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Lookout
2
+ class CityQuery < ApplicationQuery
3
+ def build
4
+ # Use database-agnostic concat
5
+ concat_expr = Lookout::DatabaseAdapter.concat('city', 'region', 'country')
6
+
7
+ visit_query
8
+ .select("city, country, count(#{concat_expr}) as count, sum(count(#{concat_expr})) over() as total_count")
9
+ .where.not(city: nil)
10
+ .group("city, region, country")
11
+ .order(Arel.sql "count(#{concat_expr}) desc")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module Lookout
2
+ class CountryQuery < ApplicationQuery
3
+ def build
4
+ visit_query
5
+ .reselect("country as label, count(country) as count, sum(count(country)) OVER() as total_count")
6
+ .group("country")
7
+ .order("count(country) desc")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Lookout
2
+ class DeviceQuery < ApplicationQuery
3
+ def build
4
+ visit_query
5
+ .select("#{params[:devices_type]} as label", "count(#{params[:devices_type]}) as count", "sum(count(#{params[:devices_type]})) over() as total_count")
6
+ .group(params[:devices_type])
7
+ .order("count(#{params[:devices_type]}) desc")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ module Lookout
2
+ class EntryPagesQuery < ApplicationQuery
3
+
4
+ def build
5
+ max_id_query = event_query.with_routes.select("min(#{Lookout.event.table_name}.id) as id").group("visit_id")
6
+ event_query.with_routes.select(
7
+ "#{Lookout.config.event[:url_column]} as url",
8
+ "count(#{Lookout.config.event[:url_column]}) as count",
9
+ "sum(count(#{Lookout.config.event[:url_column]})) over() as total_count"
10
+ )
11
+ .where(id: max_id_query)
12
+ .group(Lookout.config.event[:url_column])
13
+ .order(Arel.sql "count(#{Lookout.config.event[:url_column]}) desc")
14
+ end
15
+
16
+
17
+ end
18
+ end
@@ -0,0 +1,42 @@
1
+ module Lookout
2
+ class EventQuery < ApplicationQuery
3
+
4
+ def build
5
+ entry_pages = ransack_params_for(:event).select { |k,v| k.start_with?("entry_page") }
6
+ exit_pages = ransack_params_for(:event).select { |k,v| k.start_with?("exit_page") }
7
+
8
+ event = Lookout.event
9
+ shared_context = Ransack::Context.for(event)
10
+
11
+ search_parents = event.ransack(
12
+ ransack_params_for(:event).reject { |k,v| k.start_with?("visit_") }, context: shared_context
13
+ )
14
+
15
+ visit_params = ransack_params_for(:visit).reject { |k,v| k.start_with?("event_") || k.start_with?("events_") || k == :c }.transform_keys { |key| "visit_#{key}" }
16
+ search_children = Lookout.visit.ransack(
17
+ visit_params, context: shared_context
18
+ )
19
+ shared_conditions = [search_parents, search_children].map { |search|
20
+ Ransack::Visitor.new.accept(search.base)
21
+ }
22
+
23
+ joined = Lookout.event.joins(shared_context.join_sources)
24
+
25
+ if entry_pages.values.any?(&:present?) || params[:controller].include?("entry_pages")
26
+ joined = joined.with_entry_pages
27
+ end
28
+
29
+ if exit_pages.values.any?(&:present?) || params[:controller].include?("exit_pages")
30
+ joined = joined.with_exit_pages
31
+ end
32
+
33
+ joined.where(shared_conditions.reduce(&:and))
34
+ end
35
+
36
+ def page_view
37
+ @query = @query.page_view
38
+
39
+ self
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ module Lookout
2
+ class ExitPagesQuery < ApplicationQuery
3
+
4
+ def build
5
+ max_id_query = event_query.with_routes.select("max(#{Lookout.event.table_name}.id) as id").group("visit_id")
6
+ event_query.with_routes.select(
7
+ "#{Lookout.config.event[:url_column]} as url",
8
+ "count(#{Lookout.config.event[:url_column]}) as count",
9
+ "sum(count(#{Lookout.config.event[:url_column]})) over() as total_count"
10
+ )
11
+ .where(id: max_id_query)
12
+ .group(Lookout.config.event[:url_column])
13
+ .order(Arel.sql "count(#{Lookout.config.event[:url_column]}) desc")
14
+
15
+ end
16
+
17
+
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Lookout
2
+ class RegionQuery < ApplicationQuery
3
+ def build
4
+ # Use database-agnostic concat
5
+ concat_expr = Lookout::DatabaseAdapter.concat('region', 'country')
6
+
7
+ visit_query
8
+ .reselect("region, country, count(#{concat_expr}) as count, sum(count(region)) over() as total_count")
9
+ .where.not(region: nil)
10
+ .group("region, country")
11
+ .order(Arel.sql "count(#{concat_expr}) desc")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module Lookout
2
+ class SourceQuery < ApplicationQuery
3
+ def build
4
+ expr = Lookout::DatabaseAdapter.domain_from("referring_domain")
5
+ visit_query.select("#{expr} as referring_domain, count(#{expr}) as count, sum(count(#{expr})) OVER() as total_count")
6
+ .where.not(referring_domain: nil)
7
+ .group(expr)
8
+ .order(Arel.sql "count(#{expr}) desc")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Lookout
2
+ module Stats
3
+ # pls fix
4
+ class AverageViewsPerVisitQuery < BaseQuery
5
+ def build
6
+ subquery = event_query.select("count(ahoy_events.visit_id) as count").where(name: "$view").group(:visit_id)
7
+
8
+ Lookout.event.select("count").from("(#{subquery.to_sql}) as events")
9
+ end
10
+
11
+ def self.cast_type(_column)
12
+ nil
13
+ end
14
+
15
+ def self.cast_value(_, value)
16
+ value.to_i
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ module Lookout
2
+ module Stats
3
+ class AverageVisitDurationQuery < BaseQuery
4
+ def build
5
+ max_events = event_query.select("#{Lookout.event.table_name}.visit_id, max(#{Lookout.event.table_name}.time) as created_at").group("visit_id")
6
+
7
+ # PostgreSQL has interval type, SQLite stores timestamp diff as numeric (seconds)
8
+ duration_cast = Lookout::DatabaseAdapter.postgresql? ?
9
+ "avg((max_events.created_at - #{Lookout.visit.table_name}.started_at))::interval" :
10
+ "avg(julianday(max_events.created_at) - julianday(#{Lookout.visit.table_name}.started_at)) * 86400"
11
+
12
+ visit_query.select("#{duration_cast} as average_visit_duration")
13
+ .joins("LEFT JOIN (#{max_events.to_sql}) as max_events ON #{Lookout.visit.table_name}.id = max_events.visit_id")
14
+ end
15
+
16
+ def self.cast_type(value)
17
+ ActiveRecord::Type.lookup(:string)
18
+ end
19
+
20
+ def self.cast_value(_type, value)
21
+ if value.present?
22
+ # SQLite returns numeric seconds, PostgreSQL returns interval string
23
+ if value.is_a?(Numeric) || value.to_s.match?(/^\d+(\.\d+)?$/)
24
+ value.to_f.seconds
25
+ else
26
+ ActiveSupport::Duration.parse(value)
27
+ end
28
+ else
29
+ 0.seconds
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ module Lookout
2
+ module Stats
3
+ class BaseQuery < ApplicationQuery
4
+ include ComparableQuery
5
+ include LazyComparableQuery
6
+
7
+ protected
8
+
9
+ def self.cast_type(column)
10
+ ActiveRecord::Type.lookup(::Ahoy::Visit.columns_hash[column.to_s].type)
11
+ end
12
+
13
+ def self.cast_value(type, value)
14
+ type.cast(value)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ module Lookout
2
+ module Stats
3
+ class BounceRatesQuery < BaseQuery
4
+ def build
5
+ total_visits = visit_query.select("date(#{Lookout.visit.table_name}.started_at) as date, count(*) as count").group("date(#{Lookout.visit.table_name}.started_at)")
6
+ subquery = visit_query.select(:id, :started_at).joins(:events).group("#{Lookout.visit.table_name}.id, #{Lookout.visit.table_name}.started_at").having("count(#{Lookout.event.table_name}.id) = 1")
7
+ single_page_visits = ::Ahoy::Visit.select("date(subquery.started_at) as date, count(*) as count").from("(#{subquery.to_sql}) as subquery").group("date(started_at)")
8
+
9
+ percent_expr = Lookout::DatabaseAdapter.percentage_calculation("single_page_visits.count", "total_visits.count")
10
+ daily_bounce_rate = ::Ahoy::Visit.select("total_visits.date, #{percent_expr} as bounce_rate")
11
+ .from("total_visits")
12
+ .joins("join single_page_visits ON total_visits.date = single_page_visits.date")
13
+
14
+ ::Ahoy::Visit.with(total_visits: total_visits, single_page_visits: single_page_visits, daily_bounce_rate: daily_bounce_rate).select("bounce_rate, date").from("daily_bounce_rate")
15
+ end
16
+
17
+ protected
18
+
19
+ def self.cast_type(column)
20
+ ActiveRecord::Type.lookup(:decimal)
21
+ end
22
+
23
+ def self.cast_value(type, value)
24
+ if value.blank?
25
+ return 0.to_d
26
+ end
27
+
28
+ super.round(2)
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Lookout
2
+ module Stats
3
+ class TotalPageviewsQuery < BaseQuery
4
+ def build
5
+ event_query.page_view
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Lookout
2
+ module Stats
3
+ class TotalVisitorsQuery < BaseQuery
4
+ def build
5
+ visit_query.distinct.select(:id)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Lookout
2
+ module Stats
3
+ class UniqueVisitorsQuery < BaseQuery
4
+ def build
5
+ visit_query.distinct.select(:visitor_token)
6
+ end
7
+ end
8
+ end
9
+ end