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,148 @@
1
+ module Lookout
2
+ module Stats
3
+ class BaseController < ApplicationController
4
+
5
+ INTERVAL_PERIOD = {
6
+ "realtime" => ["minute"],
7
+ "day" => ["minute", "hour"],
8
+ "7d" => ["hour", "day"],
9
+ "month" => ["day", "week"],
10
+ "all" => ["day", "week", "month"]
11
+ }
12
+
13
+ INTERVALS = ["minute", "hour", "day", "week", "month"]
14
+
15
+ private
16
+
17
+ helper_method :metric_type
18
+ def metric_type(stats)
19
+ if compare_mode?
20
+ stats.current.values.first.try(:class) || stats.compared_to.values.first.try(:class)
21
+ else
22
+ stats.values.first.class
23
+ end
24
+ end
25
+
26
+ helper_method :selected_interval
27
+ def selected_interval
28
+ if params[:interval].in?(INTERVALS)
29
+ params[:interval]
30
+ else
31
+ default_interval_for_period
32
+ end
33
+ end
34
+
35
+ def default_interval_for_period
36
+ default_interval_for_date_range(range)
37
+ end
38
+
39
+ def default_interval_for_date_range(range)
40
+ if range[1].nil?
41
+ # assume we're in a realtime
42
+ return INTERVAL_PERIOD["realtime"][0]
43
+ end
44
+ diff = (range[1] - range[0]).seconds.in_days
45
+ if diff >= 31
46
+ "month"
47
+ elsif diff > 1
48
+ "day"
49
+ elsif diff == 1
50
+ "hour"
51
+ else
52
+ "hour"
53
+ end
54
+ end
55
+
56
+ helper_method :available_intervals
57
+ def available_intervals
58
+ if range
59
+ return INTERVAL_PERIOD["realtime"] if range[1].nil?
60
+
61
+ diff = (range[1] - range[0]).seconds.in_days
62
+
63
+ if diff < 1
64
+ INTERVAL_PERIOD["day"]
65
+ elsif diff <= 7
66
+ INTERVAL_PERIOD["7d"]
67
+ elsif diff <= 31
68
+ INTERVAL_PERIOD["month"]
69
+ else
70
+ INTERVAL_PERIOD["all"]
71
+ end
72
+ else
73
+ INTERVAL_PERIOD["month"]
74
+ end
75
+ end
76
+
77
+ def lazy_window(result, value = 0, base = nil)
78
+ if result.is_a?(Lookout::LazyComparableQuery::LazyComparison)
79
+ result.result.current = lazy_window(result.result.current, value, range)
80
+ result.result.compared_to = lazy_window(result.result.compared_to, value, result.compare_range)
81
+ return result.result
82
+ end
83
+
84
+ base ||= range
85
+ window = window_for(selected_interval, result.keys[0].class, base.numeric)
86
+
87
+ window.each do |item|
88
+ if result.key?(item)
89
+ next
90
+ end
91
+
92
+ result[item] ||= value
93
+ end
94
+
95
+ transform = interval_label_transformation(selected_interval)
96
+
97
+ if transform
98
+ result.transform_keys! { |key| key.strftime(transform) }
99
+ end
100
+
101
+ result
102
+ end
103
+
104
+ def interval_label_transformation(interval)
105
+ return nil
106
+ if interval == 'hour'
107
+ return '%H:%M %p'
108
+ end
109
+
110
+ nil
111
+ end
112
+
113
+ # base should be a range
114
+ def window_for(interval, type, base = nil)
115
+ function = case type.to_s
116
+ when 'Date', 'NilClass'
117
+ ->(value) {
118
+ date = Time.at(value).utc
119
+ if interval == 'month'
120
+ date.change(day: 1)
121
+ elsif interval == 'week'
122
+ date.beginning_of_week
123
+ elsif interval == 'day'
124
+ date.beginning_of_day
125
+ elsif interval == 'hour'
126
+ date.beginning_of_hour
127
+ elsif interval == 'minute'
128
+ date.beginning_of_minute
129
+ else
130
+ abort
131
+ end.to_date
132
+ }
133
+ when 'DateTime'
134
+ ->(value) { Time.at(value).utc.change(sec: 0) }
135
+ when 'ActiveSupport::TimeWithZone'
136
+ ->(value) { Time.at(value).utc }
137
+ else
138
+ raise ArgumentError
139
+ end
140
+
141
+ base
142
+ .step(1.send(interval))
143
+ .to_a
144
+ .map { |value| function.call(value) }
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,12 @@
1
+ module Lookout
2
+ module Stats
3
+ class BounceRatesController < BaseController
4
+ # @todo: this is lazy
5
+ def index
6
+ @stats = Lookout::Stats::BounceRatesQuery.call(params)
7
+ @stats = lazy_window(@stats.with_lazy_comparison(compare_mode?).group_by_period(selected_interval, "daily_bounce_rate.date").average("bounce_rate"))
8
+ @label = "Bounce Rate"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ module Lookout
2
+ module Stats
3
+ class TotalPageviewsController < BaseController
4
+ def index
5
+ @stats = lazy_window(Lookout::Stats::TotalPageviewsQuery.call(params).with_lazy_comparison(compare_mode?).group_by_period(selected_interval, :time).count, 0)
6
+ @label = "Visitors"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Lookout
2
+ module Stats
3
+ class TotalVisitsController < BaseController
4
+ def index
5
+ @stats = lazy_window(Lookout::Stats::TotalVisitorsQuery.call(params).with_lazy_comparison(compare_mode?).group_by_period(selected_interval, :started_at).count)
6
+ @label = "Visitors"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module Lookout
2
+ module Stats
3
+ class UniqueVisitorsController < BaseController
4
+ def index
5
+ @stats = Lookout::Stats::UniqueVisitorsQuery.call(params).with_lazy_comparison(compare_mode?).group_by_period(selected_interval, :started_at).count
6
+ @stats = lazy_window(@stats)
7
+ @label = "Visitors"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Lookout
2
+ module Stats
3
+ class ViewsPerVisitsController < BaseController
4
+ def index
5
+ @stats = lazy_window(Lookout::Stats::ViewsPerVisitQuery.call(params).with_lazy_comparison(compare_mode?).group_by_period(selected_interval, 'views_per_visit_table.started_at').average(:views_per_visit))
6
+
7
+ @label = "Views"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Lookout
2
+ module Stats
3
+ class VisitDurationsController < BaseController
4
+ def index
5
+ @stats = lazy_window(Lookout::Stats::VisitDurationQuery.call(params).with_lazy_comparison(compare_mode?).group_by_period(selected_interval, 'started_at').average(:duration))
6
+ @label = "Duration"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Lookout
2
+ class StatsController < ApplicationController
3
+ def show
4
+ @presenter = DashboardPresenter.new(params)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ module Lookout
2
+ class TopPagesController < ApplicationController
3
+ include Limitable
4
+
5
+ before_action do
6
+ if Widget.disabled?(:top_pages)
7
+ raise Widget::WidgetDisabled.new("Widget disabled", :pages)
8
+ end
9
+ end
10
+
11
+ def index
12
+ results = cached(:top_pages) do
13
+ TopPageQuery.call(params)
14
+ .limit(limit)
15
+ end
16
+
17
+ @pages = paginate(results).map { |page| TopPageDecorator.new(page, self) }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,58 @@
1
+ require 'csv'
2
+
3
+ module Lookout
4
+ class ApplicationDecorator
5
+ attr_reader :object
6
+
7
+ def self.to_csv(collection, context)
8
+ rows = collection.map { |row| new(row, context) }
9
+ CSV.generate do |csv|
10
+ csv << csv_map(context.params).keys
11
+
12
+ rows.each do |row|
13
+ items = []
14
+ csv_map.values.each do |attr|
15
+ items << row.send(attr)
16
+ end
17
+
18
+ csv << items
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.csv_map(params = {})
24
+ raise NotImplementedError
25
+ end
26
+
27
+ delegate_missing_to :object
28
+ def initialize(object, context)
29
+ @object = object
30
+ @context = context
31
+ end
32
+
33
+ private
34
+
35
+ def h
36
+ @h ||= @context.view_context
37
+ end
38
+
39
+ def params
40
+ h.params
41
+ end
42
+
43
+ def search_query(args = {})
44
+ query = h.search_params.dup
45
+ query[:q] ||= {}
46
+ args.each { |k,v| query[:q]["#{k}"] = v }
47
+ query.to_query
48
+ end
49
+
50
+ def frame_link(label, search)
51
+ h.link_to(label, "#{Lookout::Engine.routes.url_helpers.root_path}?#{search}", data: { turbo_frame: "_top" }).html_safe
52
+ end
53
+
54
+ def request
55
+ @context.request
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ module Lookout
2
+ class CampaignDecorator < ApplicationDecorator
3
+
4
+ def self.csv_map(params = {})
5
+ {
6
+ params[:campaigns_type] => :label,
7
+ "Total" => :unit_amount
8
+ }
9
+ end
10
+
11
+ def display_name
12
+ if object.label == "Direct/None"
13
+ value = ""
14
+ else
15
+ value = object.label
16
+ end
17
+
18
+ search = search_query("#{params[:campaigns_type]}_eq" => value)
19
+
20
+ frame_link(object.label, search)
21
+ end
22
+
23
+ def unit_amount
24
+ object.count
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ module Lookout
2
+ class CityDecorator < CountryDecorator
3
+ def self.csv_map(params = {})
4
+ {
5
+ "Country" => :country,
6
+ "City" => :city,
7
+ "Total" => :unit_amount
8
+ }
9
+ end
10
+
11
+ def display_name
12
+ search = search_query(country_eq: object.country, city_eq: object.city)
13
+ frame_link("#{country_emoji(object.country)} #{object.city}", search)
14
+ end
15
+
16
+ def country
17
+ "#{country_emoji(object.country)} #{object.country}"
18
+ end
19
+
20
+ def unit_amount
21
+ object.count
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module Lookout
2
+ class CountryDecorator < ApplicationDecorator
3
+ EMOJI_MAP = { "AD": "🇦🇩", "AE": "🇦🇪", "AF": "🇦🇫", "AG": "🇦🇬", "AI": "🇦🇮", "AL": "🇦🇱", "AM": "🇦🇲", "AO": "🇦🇴", "AQ": "🇦🇶", "AR": "🇦🇷", "AS": "🇦🇸", "AT": "🇦🇹", "AU": "🇦🇺", "AW": "🇦🇼", "AX": "🇦🇽", "AZ": "🇦🇿", "BA": "🇧🇦", "BB": "🇧🇧", "BD": "🇧🇩", "BE": "🇧🇪", "BF": "🇧🇫", "BG": "🇧🇬", "BH": "🇧🇭", "BI": "🇧🇮", "BJ": "🇧🇯", "BL": "🇧🇱", "BM": "🇧🇲", "BN": "🇧🇳", "BO": "🇧🇴", "BQ": "🇧🇶", "BR": "🇧🇷", "BS": "🇧🇸", "BT": "🇧🇹", "BV": "🇧🇻", "BW": "🇧🇼", "BY": "🇧🇾", "BZ": "🇧🇿", "CA": "🇨🇦", "CC": "🇨🇨", "CD": "🇨🇩", "CF": "🇨🇫", "CG": "🇨🇬", "CH": "🇨🇭", "CI": "🇨🇮", "CK": "🇨🇰", "CL": "🇨🇱", "CM": "🇨🇲", "CN": "🇨🇳", "CO": "🇨🇴", "CR": "🇨🇷", "CU": "🇨🇺", "CV": "🇨🇻", "CW": "🇨🇼", "CX": "🇨🇽", "CY": "🇨🇾", "CZ": "🇨🇿", "DE": "🇩🇪", "DJ": "🇩🇯", "DK": "🇩🇰", "DM": "🇩🇲", "DO": "🇩🇴", "DZ": "🇩🇿", "EC": "🇪🇨", "EE": "🇪🇪", "EG": "🇪🇬", "EH": "🇪🇭", "ER": "🇪🇷", "ES": "🇪🇸", "ET": "🇪🇹", "FI": "🇫🇮", "FJ": "🇫🇯", "FK": "🇫🇰", "FM": "🇫🇲", "FO": "🇫🇴", "FR": "🇫🇷", "GA": "🇬🇦", "GB": "🇬🇧", "GD": "🇬🇩", "GE": "🇬🇪", "GF": "🇬🇫", "GG": "🇬🇬", "GH": "🇬🇭", "GI": "🇬🇮", "GL": "🇬🇱", "GM": "🇬🇲", "GN": "🇬🇳", "GP": "🇬🇵", "GQ": "🇬🇶", "GR": "🇬🇷", "GS": "🇬🇸", "GT": "🇬🇹", "GU": "🇬🇺", "GW": "🇬🇼", "GY": "🇬🇾", "HK": "🇭🇰", "HM": "🇭🇲", "HN": "🇭🇳", "HR": "🇭🇷", "HT": "🇭🇹", "HU": "🇭🇺", "ID": "🇮🇩", "IE": "🇮🇪", "IL": "🇮🇱", "IM": "🇮🇲", "IN": "🇮🇳", "IO": "🇮🇴", "IQ": "🇮🇶", "IR": "🇮🇷", "IS": "🇮🇸", "IT": "🇮🇹", "JE": "🇯🇪", "JM": "🇯🇲", "JO": "🇯🇴", "JP": "🇯🇵", "KE": "🇰🇪", "KG": "🇰🇬", "KH": "🇰🇭", "KI": "🇰🇮", "KM": "🇰🇲", "KN": "🇰🇳", "KP": "🇰🇵", "KR": "🇰🇷", "KW": "🇰🇼", "KY": "🇰🇾", "KZ": "🇰🇿", "LA": "🇱🇦", "LB": "🇱🇧", "LC": "🇱🇨", "LI": "🇱🇮", "LK": "🇱🇰", "LR": "🇱🇷", "LS": "🇱🇸", "LT": "🇱🇹", "LU": "🇱🇺", "LV": "🇱🇻", "LY": "🇱🇾", "MA": "🇲🇦", "MC": "🇲🇨", "MD": "🇲🇩", "ME": "🇲🇪", "MF": "🇲🇫", "MG": "🇲🇬", "MH": "🇲🇭", "MK": "🇲🇰", "ML": "🇲🇱", "MM": "🇲🇲", "MN": "🇲🇳", "MO": "🇲🇴", "MP": "🇲🇵", "MQ": "🇲🇶", "MR": "🇲🇷", "MS": "🇲🇸", "MT": "🇲🇹", "MU": "🇲🇺", "MV": "🇲🇻", "MW": "🇲🇼", "MX": "🇲🇽", "MY": "🇲🇾", "MZ": "🇲🇿", "NA": "🇳🇦", "NC": "🇳🇨", "NE": "🇳🇪", "NF": "🇳🇫", "NG": "🇳🇬", "NI": "🇳🇮", "NL": "🇳🇱", "NO": "🇳🇴", "NP": "🇳🇵", "NR": "🇳🇷", "NU": "🇳🇺", "NZ": "🇳🇿", "OM": "🇴🇲", "PA": "🇵🇦", "PE": "🇵🇪", "PF": "🇵🇫", "PG": "🇵🇬", "PH": "🇵🇭", "PK": "🇵🇰", "PL": "🇵🇱", "PM": "🇵🇲", "PN": "🇵🇳", "PR": "🇵🇷", "PS": "🇵🇸", "PT": "🇵🇹", "PW": "🇵🇼", "PY": "🇵🇾", "QA": "🇶🇦", "RE": "🇷🇪", "RO": "🇷🇴", "RS": "🇷🇸", "RU": "🇷🇺", "RW": "🇷🇼", "SA": "🇸🇦", "SB": "🇸🇧", "SC": "🇸🇨", "SD": "🇸🇩", "SE": "🇸🇪", "SG": "🇸🇬", "SH": "🇸🇭", "SI": "🇸🇮", "SJ": "🇸🇯", "SK": "🇸🇰", "SL": "🇸🇱", "SM": "🇸🇲", "SN": "🇸🇳", "SO": "🇸🇴", "SR": "🇸🇷", "SS": "🇸🇸", "ST": "🇸🇹", "SV": "🇸🇻", "SX": "🇸🇽", "SY": "🇸🇾", "SZ": "🇸🇿", "TC": "🇹🇨", "TD": "🇹🇩", "TF": "🇹🇫", "TG": "🇹🇬", "TH": "🇹🇭", "TJ": "🇹🇯", "TK": "🇹🇰", "TL": "🇹🇱", "TM": "🇹🇲", "TN": "🇹🇳", "TO": "🇹🇴", "TR": "🇹🇷", "TT": "🇹🇹", "TV": "🇹🇻", "TW": "🇹🇼", "TZ": "🇹🇿", "UA": "🇺🇦", "UG": "🇺🇬", "UM": "🇺🇲", "US": "🇺🇸", "UY": "🇺🇾", "UZ": "🇺🇿", "VA": "🇻🇦", "VC": "🇻🇨", "VE": "🇻🇪", "VG": "🇻🇬", "VI": "🇻🇮", "VN": "🇻🇳", "VU": "🇻🇺", "WF": "🇼🇫", "WS": "🇼🇸", "YE": "🇾🇪", "YT": "🇾🇹", "ZA": "🇿🇦", "ZM": "🇿🇲", "ZW": "🇿🇼" }
4
+ def self.csv_map(params = {})
5
+ {
6
+ "Country" => :label,
7
+ "Total" => :unit_amount
8
+ }
9
+ end
10
+
11
+ def label
12
+ "#{country_emoji(country)} #{country}"
13
+ end
14
+
15
+ def display_name
16
+ search = search_query(country_eq: object.label)
17
+ frame_link("#{country_emoji(country)} #{country}", search)
18
+ end
19
+
20
+ def unit_amount
21
+ object.count
22
+ end
23
+
24
+ private
25
+
26
+ def country_emoji(iso)
27
+ if iso
28
+ EMOJI_MAP[iso.to_sym]
29
+ else
30
+ "🏴‍️"
31
+ end
32
+ end
33
+
34
+ def country
35
+ object.label
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ module Lookout
2
+ class DeviceDecorator < ApplicationDecorator
3
+ def self.csv_map(params = {})
4
+ {
5
+ "#{params[:devices_type]}" => :label,
6
+ "Total" => :unit_amount
7
+ }
8
+ end
9
+
10
+ def display_name
11
+ search = search_query("#{params[:devices_type]}_eq" => label)
12
+ frame_link(label, search)
13
+ end
14
+
15
+ def label
16
+ object.label
17
+ end
18
+
19
+ def unit_amount
20
+ object.count
21
+ end
22
+
23
+ def total_count
24
+ object.total_count
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module Lookout
2
+ class EntryPageDecorator < PageDecorator
3
+ def type
4
+ :entry_page_in
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Lookout
2
+ class ExitPageDecorator < PageDecorator
3
+ def type
4
+ :exit_page_in
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ module Lookout
2
+ class PageDecorator < ApplicationDecorator
3
+ def self.csv_map(params = {})
4
+ {
5
+ "URL" => :label,
6
+ "Total" => :unit_amount
7
+ }
8
+ end
9
+
10
+ def label
11
+ object.url
12
+ end
13
+
14
+ def display_name
15
+ search = search_query(type => object.url)
16
+ frame_link(object.url, search)
17
+ end
18
+
19
+ def unit_amount
20
+ object.count
21
+ end
22
+
23
+ def type
24
+ raise NotImplementedError
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ module Lookout
2
+ class RegionDecorator < CountryDecorator
3
+ def self.csv_map(params = {})
4
+ {
5
+ "Country" => :country,
6
+ "Region" => :region,
7
+ "Total" => :unit_amount
8
+ }
9
+ end
10
+
11
+ def display_name
12
+ search = search_query(region_eq: object.region, country_eq: object.country)
13
+ frame_link("#{country_emoji(object.country)} #{object.region}", search)
14
+ end
15
+
16
+ def country
17
+ "#{country_emoji(object.country)} #{object.country}"
18
+ end
19
+
20
+ def region
21
+ object.region
22
+ end
23
+
24
+ def unit_amount
25
+ object.count
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module Lookout
2
+ class SourceDecorator < ApplicationDecorator
3
+ def self.csv_map(params = {})
4
+ {
5
+ "Domain" => :referring_domain,
6
+ "Total" => :unit_amount
7
+ }
8
+ end
9
+
10
+ def display_name
11
+ display = %Q(
12
+ <div class='flex justify-start space-x-8 col-span-1 items-center'>
13
+ <img class='transparent w-5 h-5'
14
+ src='https://www.google.com/s2/favicons?domain=#{object.referring_domain}&sz=32'
15
+ />
16
+ <span>#{object.referring_domain}</span>
17
+ </div>
18
+ ).html_safe
19
+ path = search_query(ref_domain_eq: object.referring_domain)
20
+ frame_link(display, path)
21
+ end
22
+
23
+ def unit_amount
24
+ object.count
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module Lookout
2
+ class TopPageDecorator < PageDecorator
3
+ def type
4
+ :route_eq
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,124 @@
1
+ module Lookout
2
+ module ApplicationHelper
3
+ include Pagy::Frontend
4
+
5
+ def current_property_filter
6
+ return nil unless params[:q]
7
+
8
+ prop = params[:q].to_unsafe_h.detect { |key, _| key.starts_with?("properties.") }
9
+ if prop
10
+ key = prop[0].dup
11
+ Ransack::Predicate.detect_and_strip_from_string!(key)
12
+ { key: key, value: prop[1] }
13
+
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ def stats_container(value, url, label, formatter, selected = false)
20
+ if value.is_a?(Lookout::ComparableQuery::Comparison)
21
+ ::Lookout::Stats::ComparableContainerComponent.new(url, label, value, formatter, selected, compare_mode?)
22
+ else
23
+ ::Lookout::Stats::ContainerComponent.new(url, label, value, formatter, selected)
24
+ end
25
+ end
26
+
27
+ def number_to_duration(duration)
28
+ seconds =
29
+ case duration
30
+ when nil
31
+ nil
32
+ when ActiveSupport::Duration
33
+ duration.in_seconds
34
+ else
35
+ duration.to_f
36
+ end
37
+
38
+ if seconds && seconds > 0
39
+ minutes = (seconds / 60).to_i
40
+ seconds = (seconds % 60).to_i
41
+ "#{minutes}m #{seconds}s"
42
+ else
43
+ "0m 0s"
44
+ end
45
+ end
46
+
47
+ def number_to_percentage(number, options = {})
48
+ precision = options.fetch(:precision, 2)
49
+ "#{number.round(precision)}%"
50
+ end
51
+
52
+ def lookout_importmap_tags(entry_point = "application", shim: true)
53
+ # Prefer modern helper if available (importmap-rails >= 1.1)
54
+ if respond_to?(:javascript_importmap_tags)
55
+ return safe_join [
56
+ javascript_importmap_tags,
57
+ javascript_import_module_tag(entry_point)
58
+ ], "\n"
59
+ end
60
+
61
+ # Legacy helpers (importmap-rails 1.0 style) - guard per method
62
+ parts = []
63
+ if shim && respond_to?(:javascript_importmap_shim_tag)
64
+ parts << javascript_importmap_shim_tag
65
+ end
66
+ if shim && respond_to?(:javascript_importmap_shim_nonce_configuration_tag)
67
+ parts << javascript_importmap_shim_nonce_configuration_tag
68
+ end
69
+ parts << javascript_import_module_tag(entry_point)
70
+ if respond_to?(:javascript_importmap_module_preload_tags)
71
+ parts << javascript_importmap_module_preload_tags(Lookout.importmap)
72
+ end
73
+ if respond_to?(:javascript_inline_importmap_tag)
74
+ parts << javascript_inline_importmap_tag(Lookout.importmap.to_json(resolver: self))
75
+ end
76
+
77
+ safe_join parts.compact, "\n"
78
+ end
79
+
80
+ def search_params
81
+ request.query_parameters
82
+ end
83
+
84
+ # gets put into the form as a hidden field
85
+ #
86
+ def non_filter_ransack_params
87
+ other_params = {}
88
+ map = [
89
+ :start_date,
90
+ :end_date,
91
+ :period,
92
+ :interval,
93
+ :comparison,
94
+ ]
95
+
96
+ # properties stuff falls into current_property_filter
97
+ ransack = [:goal]
98
+
99
+ map.each do |key|
100
+ if params[key]
101
+ other_params[key] = params[key]
102
+ end
103
+ end
104
+
105
+ ransack.each do |key|
106
+ Ransack.predicates.keys.each do |predicate|
107
+ if value = params.dig(:q, "#{key}_#{predicate}")
108
+ other_params["q[#{key}_#{predicate}]"] = value
109
+ end
110
+ end
111
+ end
112
+
113
+ other_params
114
+ end
115
+
116
+ def render_pagination
117
+ if @pagination
118
+ pagy_nav(@pagination).html_safe
119
+ else
120
+ ""
121
+ end
122
+ end
123
+ end
124
+ end