ahoy_captain 0.8 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -13
  3. data/app/assets/javascript/ahoy_captain/application.js +4 -4
  4. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +14 -0
  5. data/app/assets/javascript/ahoy_captain/controllers/application.js +5 -5
  6. data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +9 -10
  7. data/app/assets/javascript/ahoy_captain/controllers/details_modal_controller.js +5 -5
  8. data/app/assets/javascript/ahoy_captain/controllers/dropdown_label_controller.js +2 -2
  9. data/app/assets/javascript/ahoy_captain/controllers/filter/item_controller.js +12 -0
  10. data/app/assets/javascript/ahoy_captain/controllers/filter_form_controller.js +13 -0
  11. data/app/assets/javascript/ahoy_captain/controllers/filter_tag_controller.js +11 -8
  12. data/app/assets/javascript/ahoy_captain/controllers/funnel_chart_controller.js +77 -107
  13. data/app/assets/javascript/ahoy_captain/controllers/index.js +4 -3
  14. data/app/assets/javascript/ahoy_captain/controllers/interval_controller.js +3 -3
  15. data/app/assets/javascript/ahoy_captain/controllers/line_chart_controller.js +37 -0
  16. data/app/assets/javascript/ahoy_captain/controllers/predicate_select_controller.js +10 -0
  17. data/app/assets/javascript/ahoy_captain/controllers/realtime_controller.js +9 -8
  18. data/app/assets/javascript/ahoy_captain/controllers/search_select_controller.js +65 -0
  19. data/app/assets/javascript/ahoy_captain/controllers/toggle_controller.js +17 -0
  20. data/app/components/ahoy_captain/dropdown_button_component.html.erb +5 -5
  21. data/app/components/ahoy_captain/dropdown_link_component.html.erb +5 -5
  22. data/app/components/ahoy_captain/filter/dropdown_component.html.erb +48 -0
  23. data/app/components/ahoy_captain/filter/dropdown_component.rb +51 -0
  24. data/app/components/ahoy_captain/filter/modal_component.html.erb +8 -7
  25. data/app/components/ahoy_captain/filter/select_component.html.erb +14 -12
  26. data/app/components/ahoy_captain/filter/select_component.rb +40 -9
  27. data/app/components/ahoy_captain/filter/tag_component.html.erb +8 -4
  28. data/app/components/ahoy_captain/filter/tag_component.rb +6 -30
  29. data/app/components/ahoy_captain/filter/tag_container_component.html.erb +2 -3
  30. data/app/components/ahoy_captain/filter/tag_container_component.rb +1 -8
  31. data/app/components/ahoy_captain/stats/container_component.html.erb +8 -0
  32. data/app/components/ahoy_captain/stats/container_component.rb +11 -0
  33. data/app/components/ahoy_captain/sticky_nav_component.html.erb +7 -19
  34. data/app/components/ahoy_captain/sticky_nav_component.rb +8 -0
  35. data/app/components/ahoy_captain/table_component.html.erb +4 -37
  36. data/app/components/ahoy_captain/table_component.rb +15 -4
  37. data/app/components/ahoy_captain/tables/headers/devices_header_component.html.erb +3 -0
  38. data/app/components/ahoy_captain/tables/headers/devices_header_component.rb +9 -0
  39. data/app/components/ahoy_captain/tables/headers/goals_header_component.html.erb +6 -0
  40. data/app/components/ahoy_captain/tables/headers/goals_header_component.rb +9 -0
  41. data/app/components/ahoy_captain/tables/headers/header_component.html.erb +5 -0
  42. data/app/components/ahoy_captain/tables/headers/header_component.rb +12 -0
  43. data/app/components/ahoy_captain/tables/rows/devices_row_component.html.erb +5 -0
  44. data/app/components/ahoy_captain/tables/rows/devices_row_component.rb +12 -0
  45. data/app/components/ahoy_captain/tables/rows/goals_row_component.html.erb +11 -0
  46. data/app/components/ahoy_captain/tables/rows/goals_row_component.rb +20 -0
  47. data/app/components/ahoy_captain/tables/rows/row_component.html.erb +6 -0
  48. data/app/components/ahoy_captain/tables/rows/row_component.rb +41 -0
  49. data/app/components/ahoy_captain/tile_component.html.erb +4 -3
  50. data/app/components/ahoy_captain/tile_component.rb +1 -1
  51. data/app/components/ahoy_captain/tooltip_component.html.erb +2 -2
  52. data/app/controllers/ahoy_captain/application_controller.rb +13 -14
  53. data/app/controllers/ahoy_captain/campaigns_controller.rb +2 -10
  54. data/app/controllers/ahoy_captain/cities_controller.rb +2 -6
  55. data/app/controllers/ahoy_captain/countries_controller.rb +2 -6
  56. data/app/controllers/ahoy_captain/devices_controller.rb +3 -6
  57. data/app/controllers/ahoy_captain/entry_pages_controller.rb +2 -4
  58. data/app/controllers/ahoy_captain/exit_pages_controller.rb +3 -4
  59. data/app/controllers/ahoy_captain/exports_controller.rb +15 -0
  60. data/app/controllers/ahoy_captain/filters/pages/entry_pages_controller.rb +2 -2
  61. data/app/controllers/ahoy_captain/filters/pages/exit_pages_controller.rb +1 -2
  62. data/app/controllers/ahoy_captain/filters/properties/names_controller.rb +11 -0
  63. data/app/controllers/ahoy_captain/filters/properties/values_controller.rb +15 -0
  64. data/app/controllers/ahoy_captain/filters/sources_controller.rb +1 -1
  65. data/app/controllers/ahoy_captain/regions_controller.rb +3 -7
  66. data/app/controllers/ahoy_captain/sources_controller.rb +2 -5
  67. data/app/controllers/ahoy_captain/stats/base_controller.rb +3 -3
  68. data/app/controllers/ahoy_captain/stats/bounce_rates_controller.rb +1 -0
  69. data/app/controllers/ahoy_captain/stats/total_pageviews_controller.rb +1 -0
  70. data/app/controllers/ahoy_captain/stats/total_visits_controller.rb +1 -0
  71. data/app/controllers/ahoy_captain/stats/unique_visitors_controller.rb +1 -0
  72. data/app/controllers/ahoy_captain/stats/views_per_visits_controller.rb +3 -2
  73. data/app/controllers/ahoy_captain/stats/visit_durations_controller.rb +1 -0
  74. data/app/controllers/ahoy_captain/top_pages_controller.rb +2 -8
  75. data/app/decorators/ahoy_captain/application_decorator.rb +27 -3
  76. data/app/decorators/ahoy_captain/campaign_decorator.rb +8 -0
  77. data/app/decorators/ahoy_captain/city_decorator.rb +12 -0
  78. data/app/decorators/ahoy_captain/country_decorator.rb +10 -0
  79. data/app/decorators/ahoy_captain/device_decorator.rb +13 -2
  80. data/app/decorators/ahoy_captain/page_decorator.rb +11 -0
  81. data/app/decorators/ahoy_captain/region_decorator.rb +16 -0
  82. data/app/decorators/ahoy_captain/source_decorator.rb +7 -0
  83. data/app/helpers/ahoy_captain/application_helper.rb +33 -0
  84. data/app/models/ahoy_captain/export.rb +48 -0
  85. data/app/models/ahoy_captain/filter_parser.rb +67 -0
  86. data/app/presenters/ahoy_captain/dashboard_presenter.rb +6 -1
  87. data/app/presenters/ahoy_captain/goals_presenter.rb +3 -2
  88. data/app/queries/ahoy_captain/application_query.rb +4 -3
  89. data/app/queries/ahoy_captain/campaign_query.rb +14 -0
  90. data/app/queries/ahoy_captain/city_query.rb +11 -0
  91. data/app/queries/ahoy_captain/country_query.rb +10 -0
  92. data/app/queries/ahoy_captain/device_query.rb +10 -0
  93. data/app/queries/ahoy_captain/entry_pages_query.rb +3 -2
  94. data/app/queries/ahoy_captain/event_query.rb +17 -15
  95. data/app/queries/ahoy_captain/exit_pages_query.rb +6 -4
  96. data/app/queries/ahoy_captain/region_query.rb +11 -0
  97. data/app/queries/ahoy_captain/source_query.rb +10 -0
  98. data/app/queries/ahoy_captain/top_page_query.rb +13 -0
  99. data/app/queries/ahoy_captain/visit_query.rb +1 -1
  100. data/app/views/ahoy_captain/devices/_table.html.erb +5 -0
  101. data/app/views/ahoy_captain/devices/index.html+details.erb +1 -1
  102. data/app/views/ahoy_captain/devices/index.html.erb +2 -2
  103. data/app/views/ahoy_captain/funnels/show.html.erb +5 -2
  104. data/app/views/ahoy_captain/goals/index.html.erb +3 -35
  105. data/app/views/ahoy_captain/layouts/application.html.erb +3 -3
  106. data/app/views/ahoy_captain/realtimes/show.html.erb +1 -1
  107. data/app/views/ahoy_captain/roots/_filters.html.erb +34 -0
  108. data/app/views/ahoy_captain/roots/show.html.erb +33 -95
  109. data/app/views/ahoy_captain/stats/base/index.html.erb +4 -7
  110. data/app/views/ahoy_captain/stats/show.html.erb +6 -51
  111. data/config/routes.rb +7 -0
  112. data/lib/ahoy_captain/ahoy/event_methods.rb +36 -73
  113. data/lib/ahoy_captain/ahoy/visit_methods.rb +1 -1
  114. data/lib/ahoy_captain/configuration.rb +13 -3
  115. data/lib/ahoy_captain/engine.rb +18 -0
  116. data/lib/ahoy_captain/filter_configuration/filter.rb +16 -0
  117. data/lib/ahoy_captain/filter_configuration/filter_collection.rb +48 -0
  118. data/lib/ahoy_captain/filters_configuration.rb +73 -0
  119. data/lib/ahoy_captain/goals.rb +10 -2
  120. data/lib/ahoy_captain/period_collection.rb +1 -1
  121. data/lib/ahoy_captain/predicate_label.rb +7 -0
  122. data/lib/ahoy_captain/version.rb +1 -1
  123. data/lib/ahoy_captain.rb +1 -0
  124. data/lib/generators/ahoy_captain/templates/config.rb.tt +25 -0
  125. metadata +56 -20
  126. data/app/assets/javascript/ahoy_captain/controllers/filter_controller.js +0 -145
  127. data/app/assets/javascript/ahoy_captain/controllers/link_controller.js +0 -43
  128. data/app/assets/javascript/ahoy_captain/controllers/navigation_controller.js +0 -25
  129. data/app/models/ahoy_captain/current.rb +0 -9
  130. data/app/models/ahoy_captain/url_helpers.rb +0 -6
@@ -1,10 +1,26 @@
1
1
  module AhoyCaptain
2
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
+
3
11
  def display_name
4
12
  search = search_query(region_eq: object.region, country_eq: object.country)
5
13
  frame_link("#{country_emoji(object.country)} #{object.region}", search)
6
14
  end
7
15
 
16
+ def country
17
+ "#{country_emoji(object.country)} #{object.country}"
18
+ end
19
+
20
+ def region
21
+ object.region
22
+ end
23
+
8
24
  def unit_amount
9
25
  object.count
10
26
  end
@@ -1,5 +1,12 @@
1
1
  module AhoyCaptain
2
2
  class SourceDecorator < ApplicationDecorator
3
+ def self.csv_map(params = {})
4
+ {
5
+ "Domain" => :referring_domain,
6
+ "Total" => :unit_amount
7
+ }
8
+ end
9
+
3
10
  def display_name
4
11
  display = %Q(
5
12
  <div class='flex justify-start space-x-8 col-span-1 items-center'>
@@ -2,7 +2,9 @@ module AhoyCaptain
2
2
  module ApplicationHelper
3
3
  include Pagy::Frontend
4
4
 
5
+ # params that are coerced from the ApplicationController#act_like_an_spa
5
6
  SPECIAL_PARAMS = [:campaigns_type, :devices_type]
7
+
6
8
  def ahoy_captain_importmap_tags(entry_point = "application", shim: true)
7
9
  safe_join [
8
10
  (javascript_importmap_shim_tag if shim),
@@ -21,6 +23,37 @@ module AhoyCaptain
21
23
  params.to_unsafe_h.slice(*SPECIAL_PARAMS)
22
24
  end
23
25
 
26
+ # gets put into the form as a hidden field
27
+ #
28
+ def non_filter_ransack_params
29
+ other_params = {}
30
+ map = [
31
+ :start_date,
32
+ :end_date,
33
+ :period,
34
+ :interval
35
+ ]
36
+
37
+ ransack = [:goal]
38
+
39
+ map.each do |key|
40
+ if params[key]
41
+ other_params[key] = params[key]
42
+ end
43
+ end
44
+
45
+ ransack.each do |key|
46
+ Ransack.predicates.keys.each do |predicate|
47
+ if value = params.dig(:q, "#{key}_#{predicate}")
48
+ other_params[:q] ||= {}
49
+ other_params[:q]["#{key}_#{predicate}"] = value
50
+ end
51
+ end
52
+ end
53
+
54
+ other_params
55
+ end
56
+
24
57
  def render_pagination
25
58
  if @pagination
26
59
  pagy_nav(@pagination).html_safe
@@ -0,0 +1,48 @@
1
+ module AhoyCaptain
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,67 @@
1
+ module AhoyCaptain
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
+ item = Item.new
30
+
31
+ item.values = Array(values)
32
+ item.predicate = Ransack::Predicate.detect_and_strip_from_string!(key.dup)
33
+ item.column = key.delete_suffix("_#{item.predicate}")
34
+ modal_name = AhoyCaptain.config.filters.detect { |_, filters| filters.include?(item.column) }[1].modal_name
35
+ if modal_name
36
+ item.modal = modal_name
37
+ end
38
+
39
+ label = if item.column == "goal"
40
+ AhoyCaptain.config.goals[values].title
41
+ else
42
+ item.values.to_sentence(last_word_connector: " or ")
43
+ end
44
+ item.label = label
45
+ item.description = "#{item.column.titleize} #{::AhoyCaptain::PredicateLabel[item.predicate]} #{label}"
46
+ item.url = build_url(key, values)
47
+ @items[key] = item
48
+ end
49
+
50
+ @items
51
+ end
52
+
53
+ private
54
+
55
+ def build_url(name, values)
56
+ search_params = @request.query_parameters.deep_dup
57
+ if search_params["q"][name].is_a?(Array)
58
+ search_params["q"][name] = search_params["q"][name] - Array(values)
59
+ else
60
+ search_params["q"].delete(name)
61
+ end
62
+
63
+ @request.path + "?" + search_params.to_query
64
+ end
65
+
66
+ end
67
+ end
@@ -30,7 +30,12 @@ module AhoyCaptain
30
30
  cached(:views_per_visit) do
31
31
  begin
32
32
  result = Stats::AverageViewsPerVisitQuery.call(params).count(:id)
33
- (result.values.sum.to_f / result.size).round(2)
33
+ count = (result.values.sum.to_f / result.size).round(2)
34
+ if count.nan?
35
+ return "0"
36
+ else
37
+ return count
38
+ end
34
39
  rescue ::ActiveRecord::StatementInvalid => e
35
40
  if e.message.include?("PG::DivisionByZero")
36
41
  return "0"
@@ -16,7 +16,7 @@ module AhoyCaptain
16
16
  queries = {
17
17
  totals: @event_query.select("count(distinct(#{AhoyCaptain.event.table_name}.visit_id)) as unique_visits, '_internal_total_visits_' as name, count(distinct #{AhoyCaptain.event.table_name}.id) as total_events, 0 as sort_order")
18
18
  }
19
- selects = ["SELECT unique_visits, name, total_events, sort_order, 0 as cr from totals"]
19
+ selects = ["SELECT unique_visits, name, total_events, sort_order, 0 as cr, '' as goal_id from totals"]
20
20
  last_goal = nil
21
21
  map = {}.with_indifferent_access
22
22
 
@@ -27,9 +27,10 @@ module AhoyCaptain
27
27
  "'#{goal.id}' as name",
28
28
  "count(distinct #{AhoyCaptain.event.table_name}.id) as total_events",
29
29
  "#{index + 1} as sort_order",
30
+ "'#{goal.id}' as goal_id"
30
31
  ]
31
32
  ).merge(goal.event_query.call).group("#{AhoyCaptain.event.table_name}.name")
32
- selects << ["SELECT unique_visits, name, total_events, sort_order, 0::decimal as cr from #{goal.id}"]
33
+ selects << ["SELECT unique_visits, name, total_events, sort_order, 0::decimal as cr, '#{goal.id}' as goal_id from #{goal.id}"]
33
34
  map[goal.id] = goal
34
35
  last_goal = goal
35
36
  end
@@ -53,6 +53,7 @@ module AhoyCaptain
53
53
  EventQuery.call(params)
54
54
  end
55
55
 
56
+ # this could be better
56
57
  def ransack_params_for(type)
57
58
  ransackable_params = {}
58
59
 
@@ -61,12 +62,13 @@ module AhoyCaptain
61
62
  visit: (AhoyCaptain.visit.ransackable_attributes + AhoyCaptain.visit.ransackable_scopes).map(&:to_s),
62
63
  event: (AhoyCaptain.event.ransackable_attributes + AhoyCaptain.event.ransackable_scopes).map(&:to_s),
63
64
  }
65
+
64
66
  pattern = /(?:_not_eq|_eq|_in|_not_in|_cont|_not_cont|_i_cont)$/
65
67
  params[:q].each do |key, value|
66
68
  attribute_name = key.gsub(pattern, '')
67
- if type == :event && ransackable_attributes[:visit].include?(attribute_name) || ransackable_attributes[:visit].include?(key)
69
+ if type == :event && (ransackable_attributes[:visit].include?(attribute_name) || ransackable_attributes[:visit].include?(key))
68
70
  ransackable_params["visit_#{key}"] = value
69
- elsif type == :visit && ransackable_attributes[:event].include?(attribute_name) || ransackable_attributes[:event].include?(key)
71
+ elsif type == :visit && (ransackable_attributes[:event].include?(attribute_name) || ransackable_attributes[:event].include?(key))
70
72
  ransackable_params["events_#{key}"] = value
71
73
  else
72
74
  ransackable_params[key] = value
@@ -102,7 +104,6 @@ module AhoyCaptain
102
104
  end
103
105
  end
104
106
 
105
-
106
107
  ransackable_params
107
108
  end
108
109
 
@@ -0,0 +1,14 @@
1
+ module AhoyCaptain
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,11 @@
1
+ module AhoyCaptain
2
+ class CityQuery < ApplicationQuery
3
+ def build
4
+ visit_query
5
+ .select("city, country, count(concat(city, region, country)) as count, sum(count(concat(city, region, country))) over() as total_count")
6
+ .where.not(city: nil)
7
+ .group("city, region, country")
8
+ .order(Arel.sql "count(concat(city, region, country)) desc")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module AhoyCaptain
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 AhoyCaptain
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
@@ -2,14 +2,15 @@ module AhoyCaptain
2
2
  class EntryPagesQuery < ApplicationQuery
3
3
 
4
4
  def build
5
- max_id_query = @query.with_routes.select("min(#{AhoyCaptain.event.table_name}.id) as id").group("visit_id")
6
- @query = @query.with_routes.select(
5
+ max_id_query = event_query.with_routes.select("min(#{AhoyCaptain.event.table_name}.id) as id").group("visit_id")
6
+ event_query.with_routes.select(
7
7
  "#{AhoyCaptain.config.event[:url_column]} as url",
8
8
  "count(#{AhoyCaptain.config.event[:url_column]}) as count",
9
9
  "sum(count(#{AhoyCaptain.config.event[:url_column]})) over() as total_count"
10
10
  )
11
11
  .where(id: max_id_query)
12
12
  .group(AhoyCaptain.config.event[:url_column])
13
+ .order(Arel.sql "count(#{AhoyCaptain.config.event[:url_column]}) desc")
13
14
  end
14
15
 
15
16
 
@@ -3,33 +3,35 @@ module AhoyCaptain
3
3
  include Rangeable
4
4
 
5
5
  def build
6
- shared_context = Ransack::Context.for(AhoyCaptain.event)
6
+ entry_pages = ransack_params_for(:event).select { |k,v| k.start_with?("entry_page") }
7
+ exit_pages = ransack_params_for(:event).select { |k,v| k.start_with?("exit_page") }
7
8
 
8
- search_parents = AhoyCaptain.event.ransack(
9
+ event = AhoyCaptain.event
10
+ shared_context = Ransack::Context.for(event)
11
+
12
+ search_parents = event.ransack(
9
13
  ransack_params_for(:event).reject { |k,v| k.start_with?("visit_") }, context: shared_context
10
14
  )
15
+
16
+ visit_params = ransack_params_for(:visit).reject { |k,v| k.start_with?("event_") || k.start_with?("events_") }.transform_keys { |key| "visit_#{key}" }
11
17
  search_children = AhoyCaptain.visit.ransack(
12
- ransack_params_for(:visit).reject { |k,v| k.start_with?("event_") }.transform_keys { |key| "visit_#{key}" }, context: shared_context
18
+ visit_params, context: shared_context
13
19
  )
14
-
15
20
  shared_conditions = [search_parents, search_children].map { |search|
16
21
  Ransack::Visitor.new.accept(search.base)
17
22
  }
18
23
 
19
- AhoyCaptain.event.joins(shared_context.join_sources)
20
- .where(shared_conditions.reduce(&:or))
24
+ joined = AhoyCaptain.event.joins(shared_context.join_sources)
21
25
 
22
- end
26
+ if entry_pages.values.any?(&:present?) || params[:controller].include?("entry_pages")
27
+ joined = joined.with_entry_pages
28
+ end
23
29
 
24
- def within_range
25
- self
26
- end
27
-
28
- def with_visit
29
- @query = @query.joins(:visit)
30
+ if exit_pages.values.any?(&:present?) || params[:controller].include?("exit_pages")
31
+ joined = joined.with_exit_pages
32
+ end
30
33
 
31
- self
34
+ joined.where(shared_conditions.reduce(&:and))
32
35
  end
33
-
34
36
  end
35
37
  end
@@ -2,14 +2,16 @@ module AhoyCaptain
2
2
  class ExitPagesQuery < ApplicationQuery
3
3
 
4
4
  def build
5
- max_id_query = @query.with_routes.select("max(#{AhoyCaptain.event.table_name}.id) as id").group("visit_id")
6
- @query = @query.with_routes.select(
5
+ max_id_query = event_query.with_routes.select("max(#{AhoyCaptain.event.table_name}.id) as id").group("visit_id")
6
+ event_query.with_routes.select(
7
7
  "#{AhoyCaptain.config.event[:url_column]} as url",
8
8
  "count(#{AhoyCaptain.config.event[:url_column]}) as count",
9
9
  "sum(count(#{AhoyCaptain.config.event[:url_column]})) over() as total_count"
10
10
  )
11
- .where(id: max_id_query)
12
- .group(AhoyCaptain.config.event[:url_column])
11
+ .where(id: max_id_query)
12
+ .group(AhoyCaptain.config.event[:url_column])
13
+ .order(Arel.sql "count(#{AhoyCaptain.config.event[:url_column]}) desc")
14
+
13
15
  end
14
16
 
15
17
 
@@ -0,0 +1,11 @@
1
+ module AhoyCaptain
2
+ class RegionQuery < ApplicationQuery
3
+ def build
4
+ visit_query
5
+ .reselect("region, country, count(concat(region, country)) as count, sum(count(region)) over() as total_count")
6
+ .where.not(region: nil)
7
+ .group("region, country")
8
+ .order(Arel.sql "count(concat(region, country)) desc")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module AhoyCaptain
2
+ class SourceQuery < ApplicationQuery
3
+ def build
4
+ visit_query.select("substring(referring_domain from '(?:.*://)?(?:www\.)?([^/?]*)') as referring_domain, count(substring(referring_domain from '(?:.*://)?(?:www\.)?([^/?]*)')) as count, sum(count(substring(referring_domain from '(?:.*://)?(?:www\.)?([^/?]*)'))) OVER() as total_count")
5
+ .where.not(referring_domain: nil)
6
+ .group("substring(referring_domain from '(?:.*://)?(?:www\.)?([^/?]*)')")
7
+ .order(Arel.sql "count(substring(referring_domain from '(?:.*://)?(?:www\.)?([^/?]*)')) desc")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ module AhoyCaptain
2
+ class TopPageQuery < ApplicationQuery
3
+ def build
4
+ event_query.with_routes
5
+ .select(
6
+ "#{AhoyCaptain.config.event[:url_column]} as url",
7
+ "count(*) as count",
8
+ "sum(count(*)) over() as total_count"
9
+ ).group(Arel.sql ("(#{AhoyCaptain.config.event[:url_column]})"))
10
+ .order(Arel.sql("count(#{AhoyCaptain.config.event[:url_column]}) desc"))
11
+ end
12
+ end
13
+ end
@@ -17,7 +17,7 @@ module AhoyCaptain
17
17
  }
18
18
 
19
19
  AhoyCaptain.visit.joins(shared_context.join_sources)
20
- .where(shared_conditions.reduce(&:or))
20
+ .where(shared_conditions.reduce(&:and))
21
21
 
22
22
  end
23
23
 
@@ -0,0 +1,5 @@
1
+ <%= render AhoyCaptain::TableComponent.new(items: @devices,
2
+ category_name: 'Devices',
3
+ unit_name: 'Visitors',
4
+ header: ::AhoyCaptain::Tables::Headers::DevicesHeaderComponent,
5
+ row: ::AhoyCaptain::Tables::Rows::DevicesRowComponent) %>
@@ -1,4 +1,4 @@
1
1
  <%= turbo_frame_tag :details do %>
2
- <%= render AhoyCaptain::TableComponent.new(items: @devices, category_name: 'Devices', unit_name: 'Visitors', additional_cols: [:percent_total]) %>
2
+ <%= render 'table' %>
3
3
  <span class="flex justify-center"><%= render_pagination %></span>
4
4
  <% end %>
@@ -1,3 +1,3 @@
1
1
  <%= turbo_frame_tag :devices do %>
2
- <%= render AhoyCaptain::TableComponent.new(items: @devices, category_name: 'Devices', unit_name: 'Visitors', additional_cols: [:percent_total]) %>
3
- <% end %>
2
+ <%= render 'table' %>
3
+ <% end %>
@@ -1,6 +1,9 @@
1
1
  <%= turbo_frame_tag :goals do %>
2
2
  <div >
3
- <canvas data-controller="funnel-chart" data-data="<%= @funnel.to_json %>" ></canvas>
4
-
3
+ <% if params.dig(:q, :goal_id) %>
4
+ <p>Funnels are unavailable if filtering by a goal.</p>
5
+ <% else %>
6
+ <canvas data-controller="funnel-chart" data-data="<%= @funnel.to_json %>" ></canvas>
7
+ <% end %>
5
8
  </div>
6
9
  <% end %>
@@ -1,39 +1,7 @@
1
1
  <%= turbo_frame_tag :goals do %>
2
- <div class="flex flex-col min-h-[380px] w-full pt-4">
3
- <div class="flex text-sm font-bold text-base-content mb-4">
4
- <span class="grow">Goal</span>
5
- <span >Uniques</span>
6
- <span class="w-8 ml-8 text-right">Total</span>
7
- <span class="w-8 ml-8 text-right">CR</span>
8
- </div>
9
- <div class='min-h-[420px]'>
10
- <div class="grow">
11
- <% if @presenter.goals.respond_to?(:each) && @presenter.goals.any? %>
12
- <% @presenter.goals.each do |item| %>
13
-
14
- <div class='leading-10 flex relative'>
15
- <progress class='progress-primary bg-base-100 h-8 grow' value="<%= item.cr %>" max="100">
16
- </progress>
17
- <span class="grow text-elipsis overflow-hidden absolute left-4 bottom-3 h-8 text-base-content">
18
- <%= item.name %>
19
- </span>
20
- <span class="w-8 ml-8 text-right">
21
- <%= render AhoyCaptain::TooltipComponent.new(amount: item.unique_visits) %>
22
- </span>
23
- <span class="w-8 ml-8 text-right">
24
- <%= render AhoyCaptain::TooltipComponent.new(amount: item.total_events) %>
25
- </span>
26
- <span class="w-8 ml-8 text-right">
27
- <%= item.cr %>%
28
- </span>
29
- </div>
30
- <% end %>
31
- <% else %>
32
- <p>No data found</p>
33
- <% end %>
34
- </div>
35
- </div>
36
- </div>
2
+ <%= render AhoyCaptain::TableComponent.new(items: @presenter.goals,
3
+ header: ::AhoyCaptain::Tables::Headers::GoalsHeaderComponent,
4
+ row: ::AhoyCaptain::Tables::Rows::GoalsRowComponent) %>
37
5
 
38
6
 
39
7
  <% end %>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html data-theme='<%= AhoyCaptain.config.theme %>'>
3
3
  <head>
4
4
  <title>Ahoy Captain</title>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -116,9 +116,9 @@
116
116
  </style>
117
117
  </head>
118
118
 
119
- <body data-theme='<%= AhoyCaptain.config.theme %>' class='bg-base-300' data-controller='navigation' data-action="filter-tag:remove->navigation#removeQueryParam">
119
+ <body data-controller='application'>
120
120
  <%= yield %>
121
- <div class="flex justify-center bg-base-300 border-t-4 border-base-100 py-4">
121
+ <div class="flex justify-center border-t-4 border-base-100 py-4">
122
122
  <div class="flex justify-around space-x-4 my-4">
123
123
  <h5>Powered by <a href='https://github.com/joshmn/ahoy_captain' target=”_blank”>Ahoy Captain v<%= AhoyCaptain::VERSION %></a></h5>
124
124
  </div>
@@ -1,6 +1,6 @@
1
1
  <%= turbo_frame_tag :realtime do %>
2
2
  <div>
3
- <a class="block ml-1 md:ml-2 mr-auto text-xs md:text-sm font-bold text-base-content dark:text-base-content" title="" data-realtime-target="label">
3
+ <a class="block ml-1 md:ml-2 mr-auto text-xs md:text-sm font-bold " title="" data-realtime-target="label">
4
4
  <svg class="inline w-2 mr-1 md:mr-2 text-green-500 fill-current animate-pulse" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
5
5
  <circle cx="8" cy="8" r="8"></circle>
6
6
  </svg><%= @total %> <span class="hidden sm:inline-block">current visitors</span>
@@ -0,0 +1,34 @@
1
+ <%= form_with url: url_for(params.permit!.except(:q)), method: :get, data: { turbo_frame: "_top", controller: "filter-form", action: "reset->filter-form#handleReset" } do |form| %>
2
+ <% non_filter_ransack_params.each do |k,v| %>
3
+ <%= form.hidden_field k, value: v %>
4
+ <% end %>
5
+ <% AhoyCaptain.config.filters.each do |name, filters| %>
6
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Filter by #{name}", id: "#{filters.modal_name}") do |modal| %>
7
+ <% modal.with_modal_display do %>
8
+ <% filters.each do |filter| %>
9
+ <%= render AhoyCaptain::Filter::SelectComponent.new(label: filter.label, column: filter.column, url: public_send(filter.url), predicates: filter.predicates, multiple: filter.multiple, form: form) %>
10
+ <% end %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
14
+
15
+
16
+ <%= render AhoyCaptain::Filter::ModalComponent.new(title: "Custom Range", id: "customRangeModal") do |modal| %>
17
+ <% modal.with_modal_display do %>
18
+ <div class="flex gap-2 w-full">
19
+ <div class="form-control w-full max-w-xs">
20
+ <label class="label" for="start_date">
21
+ <span class="label-text">Start Date</span>
22
+ </label>
23
+ <input type="datetime-local" id="start_date" name="start_date" class="input input-bordered w-full" value="<%= params[:start_date] %>" />
24
+ </div>
25
+ <div class="form-control w-full max-w-xs">
26
+ <label class="label" for="end_date">
27
+ <span class="label-text">End Date</span>
28
+ </label>
29
+ <input type="datetime-local" id="end_date" name="end_date" class="input input-bordered w-full" value="<%= params[:end_date] %>" />
30
+ </div>
31
+ </div>
32
+ <% end %>
33
+ <% end %>
34
+ <% end %>