Skip to content

Commit 336836e

Browse files
[bug fix] N+1 query in Analytics V2 (#1451)
Addresses: #1399 Co-authored-by: Prashant <25191509+alis-khadka@users.noreply.github.com>
1 parent a4b22bc commit 336836e

File tree

9 files changed

+157
-72
lines changed

9 files changed

+157
-72
lines changed

.env.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ APP_HOST=lvh.me:5250
99
RECAPTCHA_SITE_KEY=6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy
1010
RECAPTCHA_SECRET_KEY=6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx
1111
SECRET_KEY_BASE='38c72586473e364229897f24f1892f1dc5565776878aa4d8c6bf051258622bd2e923b926ab59b40f912b661216f764d993e8d6b8bbfbc33026e5c954b6c51f9b'
12-
RACK_TIMEOUT_SERVICE_TIMEOUT=8
12+
RACK_TIMEOUT_SERVICE_TIMEOUT=80

app/controllers/comfy/admin/v2/dashboard_controller.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ def dashboard
88
@end_date = params[:end_date]&.to_date || Date.today.end_of_month
99
date_range = @start_date.beginning_of_day..@end_date.end_of_day
1010

11-
@visits = Ahoy::Visit.where(started_at: @start_date.beginning_of_day..@end_date.end_of_day)
11+
@visits = Ahoy::Visit.where(started_at: date_range)
12+
13+
filtered_events = Ahoy::Event.joins(:visit)
1214

1315
Ahoy::Event::EVENT_CATEGORIES.values.each do |event_category|
1416
if event_category == Ahoy::Event::EVENT_CATEGORIES[:page_visit]
15-
events = Ahoy::Event.where(name: 'comfy-cms-page-visit').joins(:visit)
17+
events = filtered_events.where(name: 'comfy-cms-page-visit')
1618
else
17-
events = Ahoy::Event.jsonb_search(:properties, { category: event_category }).joins(:visit)
19+
events = filtered_events.jsonb_search(:properties, { category: event_category })
1820
end
1921
events = events.jsonb_search(:properties, { page_id: params[:page] }) if params[:page].present?
2022
instance_variable_set("@previous_period_#{event_category}_events", events.where(time: previous_period(params[:interval], @start_date, @end_date)))
@@ -23,7 +25,7 @@ def dashboard
2325

2426
# legacy and system events does not have category
2527
# separating out 'comfy-cms-page-visit' event since we have a seprate section
26-
@legacy_and_system_events = Ahoy::Event.where.not('properties::jsonb ? :key', key: 'category').where.not(name: 'comfy-cms-page-visit').joins(:visit)
28+
@legacy_and_system_events = filtered_events.where.not('properties::jsonb ? :key', key: 'category').where.not(name: 'comfy-cms-page-visit')
2729
@previous_period_legacy_and_system_events = @legacy_and_system_events.where(time: previous_period(params[:interval], @start_date, @end_date))
2830
@legacy_and_system_events = @legacy_and_system_events.where(time: date_range)
2931
end

app/helpers/dashboard_helper.rb

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,29 @@ def session_detail_title
2424

2525
def page_visit_chart_data(page_visit_events, start_date, end_date)
2626
period, format = split_into(start_date, end_date)
27-
page_visit_events.where.not('ahoy_visits.device_type': nil).group_by { |u| u.visit.device_type }.map do |key, value|
28-
{ name: key, data: Ahoy::Event.where(id: value.pluck(:id)).group_by_period(period, :time, range: start_date..end_date, format: format).count }
29-
end
30-
end
3127

28+
page_visit_events
29+
.where.not(visit: {device_type: nil})
30+
.group("visit.device_type")
31+
.group_by_period(period, :time, range: start_date..end_date, format: format)
32+
.size
33+
.group_by {|k, v| k.first}
34+
.map do |k, v|
35+
{
36+
name: k,
37+
data: v.map {|item| [item.first.last, item.last]}.to_h
38+
}
39+
end
40+
end
41+
3242
def page_name(page_id)
3343
return 'Website' if page_id.blank?
3444

3545
Comfy::Cms::Page.find_by(id: page_id)&.label
3646
end
3747

3848
def visitors_chart_data(visits)
39-
visitors_by_token = visits.group(:visitor_token).count
49+
visitors_by_token = visits.group(:visitor_token).size
4050
recurring_visitors = visitors_by_token.values.count { |v| v > 1 }
4151
single_time_visitors = visitors_by_token.keys.count - recurring_visitors
4252
{"Single time visitor": single_time_visitors, "Recurring visitors" => recurring_visitors }
@@ -61,46 +71,55 @@ def tooltip_content(current_count, prev_count, interval, start_date, end_date)
6171
end
6272

6373
def total_watch_time(video_view_events)
64-
video_view_events.sum { |event| event.properties['watch_time'].to_i }
74+
video_view_events.pluck(Arel.sql("SUM((#{Ahoy::Event.table_name}.properties ->> 'watch_time')::bigint)")).sum
6575
end
6676

6777
def to_minutes(time_in_milisecond)
6878
"#{number_with_delimiter((time_in_milisecond.to_f / (1000 * 60)).round(2) , :delimiter => ',')} min"
6979
end
7080

7181
def total_views(video_view_events)
72-
video_view_events.select { |event| event.properties['video_start'] }.size
82+
video_view_events.pluck(Arel.sql("SUM(CASE WHEN (#{Ahoy::Event.table_name}.properties ->> 'video_start')::boolean THEN 1 ELSE 0 END)")).sum
7383
end
7484

7585
def avg_view_duration(video_view_events)
7686
total_watch_time(video_view_events).to_f / (total_views(video_view_events).nonzero? || 1)
7787
end
7888

7989
def avg_view_percentage(video_view_events)
80-
view_percentage_arr = video_view_events.group_by { |event| event.properties['resource_id'] }.map do |_resource_id, events|
81-
(events.sum { |event| event.properties['watch_time'].to_f / event.properties['total_duration'].to_f }) * 100
82-
end
83-
view_percentage_arr.sum / (total_views(video_view_events).nonzero? || 1)
84-
end
85-
86-
def top_three_videos(video_view_events, previous_video_view_events)
87-
video_view_events.group_by { |event| event.properties['resource_id'] }.map do |resource_id, events|
88-
previous_period_event = previous_video_view_events.jsonb_search(:properties, { resource_id: resource_id })
89-
api_resource = ApiResource.find_by(id: resource_id)
90-
{
91-
total_views: total_views(events),
92-
total_watch_time: total_watch_time(events),
93-
previous_period_total_views: total_views(previous_period_event),
94-
previous_period_total_watch_time: total_watch_time(previous_period_event),
95-
resource_title: api_resource&.properties.dig(api_resource&.api_namespace.analytics_metadata&.dig("title")) || "Resource Id: #{resource_id}",
96-
resource_author: api_resource&.properties.dig(api_resource&.api_namespace.analytics_metadata&.dig("author")),
97-
resource_image: api_resource&.non_primitive_properties.find_by(field_type: "file", label: api_resource&.api_namespace.analytics_metadata&.dig("thumbnail"))&.file_url,
98-
resource_id: api_resource&.id,
99-
namespace_id: api_resource&.api_namespace.id,
100-
duration: events.first.properties['total_duration'],
101-
name: events.first.name
102-
}
103-
end.sort_by {|event| event[:total_views]}.reverse.first(3)
90+
video_view_events.pluck(Arel.sql("((properties ->> 'watch_time')::float / (properties ->> 'total_duration')::float) * 100")).sum / (total_views(video_view_events).nonzero? || 1)
91+
end
92+
93+
def top_three_videos(video_view_events, previous_video_view_events)
94+
video_view_events
95+
.with_api_resource
96+
.group(:resource_id)
97+
.reorder("SUM(is_viewed) DESC", "total_watch_time DESC")
98+
.select(:resource_id,
99+
"SUM(watch_time)::INT AS total_watch_time",
100+
"SUM(is_viewed) AS total_views",
101+
"MAX(total_duration)::float AS duration",
102+
"json_agg(ahoy_events.name) AS names",
103+
"json_agg(namespace_id) AS namespace_ids")
104+
.limit(3)
105+
.as_json
106+
.map(&:with_indifferent_access)
107+
.each do |video_event|
108+
previous_period_event = previous_video_view_events.jsonb_search(:properties, { resource_id: video_event[:resource_id] })
109+
api_resource = ApiResource.find_by(id: video_event[:resource_id])
110+
111+
video_event[:name] = video_event[:names].uniq.first
112+
video_event[:namespace_id] = video_event[:namespace_ids].uniq.first
113+
video_event[:previous_period_total_views] = total_views(previous_period_event)
114+
video_event[:previous_period_total_watch_time] = total_watch_time(previous_period_event)
115+
video_event[:resource_title] = api_resource&.properties.dig(api_resource&.api_namespace.analytics_metadata&.dig("title")) || "Resource Id: #{video_event[:resource_id]}"
116+
video_event[:resource_author] = api_resource&.properties.dig(api_resource&.api_namespace.analytics_metadata&.dig("author"))
117+
video_event[:resource_image] = api_resource&.non_primitive_properties.find_by(field_type: "file", label: api_resource&.api_namespace.analytics_metadata&.dig("thumbnail"))&.file_url
118+
119+
video_event.delete(:names)
120+
video_event.delete(:namespace_ids)
121+
video_event.delete(:id)
122+
end
104123
end
105124

106125
private

app/models/ahoy/event.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,33 @@ class Ahoy::Event < ApplicationRecord
2727
belongs_to :visit
2828
belongs_to :user, optional: true
2929

30+
scope :with_label , -> {
31+
# Build a subquery SQL
32+
subquery = self.unscoped.select("(case when #{table_name}.properties->>'label' is not NULL then #{table_name}.properties->>'label' else #{table_name}.name end) as label, #{table_name}.id").to_sql
33+
34+
# join the subquery to base model
35+
joins("INNER JOIN (#{subquery}) as labelled_events ON labelled_events.id = #{table_name}.id")
36+
}
37+
38+
scope :with_api_resource , -> {
39+
# Build a subquery SQL
40+
subquery = self
41+
.unscoped
42+
.joins("INNER JOIN #{ApiResource.table_name} ON ahoy_events.properties->>'resource_id' IS NOT NULL AND (ahoy_events.properties ->> 'resource_id')::int = #{ApiResource.table_name}.id")
43+
.select(
44+
"(#{self.table_name}.properties ->> 'resource_id')::int AS resource_id",
45+
"#{self.table_name}.id",
46+
"#{ApiResource.table_name}.api_namespace_id AS namespace_id",
47+
"(#{self.table_name}.properties ->> 'watch_time')::bigint AS watch_time",
48+
"round((#{self.table_name}.properties->>'total_duration')::numeric, 3) AS total_duration",
49+
"CASE WHEN (#{self.table_name}.properties ->> 'video_start')::boolean THEN 1 ELSE 0 END AS is_viewed",
50+
)
51+
.to_sql
52+
53+
# join the subquery to base model
54+
joins("INNER JOIN (#{subquery}) as api_resourced_events ON api_resourced_events.id = #{table_name}.id")
55+
}
56+
3057
# For events_list page, sorting on the grouped query
3158
# https://stackoverflow.com/a/35987240
3259
ransacker :count do

app/views/comfy/admin/v2/dashboard/_events.html.haml

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,27 @@
33
.d-md-flex.align-items-center
44
.vr-analytics-sub-title
55
= title
6-
- if events.present?
6+
- if events_exists
77
.d-flex.align-items-center.mt-2.mt-md-0
88
.vr-analytics-count
9-
= events.count
9+
= events_count
1010
= "total #{type}"
1111
.vr-analytics-percent-change
12-
= display_percent_change(events.count, previous_period_events.count)
13-
.vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(events.count, previous_period_events.count, params[:interval], @start_date, @end_date) }
12+
= display_percent_change(events_count, previous_period_events_count)
13+
.vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(events_count, previous_period_events_count, params[:interval], @start_date, @end_date) }
1414
?
1515

16-
- if events.present?
16+
- if events_exists
1717
.vr-analytics-section-body.d-flex.align-items-center
1818
.vr-analytics-events-grid.row.w-100
19-
- previous_grouped_events = previous_period_events.group(:name).size
20-
- events.group_by(&:label).each do |key, value|
19+
- label_grouped_event.each do |key, value|
2120
.vr-analytics-events-grid-item.col.col-12.col-sm-6.col-md-4.col-lg-3.mb-4
2221
.d-flex.mr-4.align-items-center.mb-2
2322
.vr-analytics-count-lg
24-
= value.count
23+
= value.size
2524
.vr-analytics-percent-change
26-
= display_percent_change(value.count, previous_grouped_events[key].to_i)
25+
= display_percent_change(value.size, previous_grouped_events[key].to_i)
2726
- if previous_grouped_events[key].to_i == 0
28-
.vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(value.count, 0, params[:interval], @start_date, @end_date) }
27+
.vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(value.size, 0, params[:interval], @start_date, @end_date) }
2928
?
30-
= link_to key, dashboard_events_path(ahoy_event_type: value.first&.name), class: 'vr-analytics-event-label'
29+
= link_to key, dashboard_events_path(ahoy_event_type: value.first), class: 'vr-analytics-event-label'

app/views/comfy/admin/v2/dashboard/dashboard.html.haml

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@
2222

2323
%main{class: 'my-5'}
2424
%section.row.vr-analytics-section.vr-analytics-page-visit-events
25-
.col{ class: @page_visit_events.present? ? "col-lg-9 mb-4" : "col-12" }
25+
.col{ class: @page_visit_events.load.any? ? "col-lg-9 mb-4" : "col-12" }
2626
.d-flex.justify-content-between
2727
.vr-analytics-section-header.d-md-flex.justify-content-between.align-items-center
2828
.vr-analytics-sub-title
2929
= "#{page_name(params[:page])} visits"
30-
- if @page_visit_events.present?
30+
- if @page_visit_events.load.any?
3131
.d-flex.justify-content-between.align-items-center.mt-2.mt-md-0
3232
.vr-analytics-count
33-
= @page_visit_events.count
33+
= @page_visit_events.size
3434
total visits
3535
.vr-analytics-percent-change
36-
= display_percent_change(@page_visit_events.count, @previous_period_page_visit_events.count)
37-
.vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(@page_visit_events.count, @previous_period_page_visit_events.count, params[:interval], @start_date, @end_date) }
36+
= display_percent_change(@page_visit_events.size, @previous_period_page_visit_events.size)
37+
.vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(@page_visit_events.size, @previous_period_page_visit_events.size, params[:interval], @start_date, @end_date) }
3838
?
3939
.vr-analytics-section-body
40-
- if @page_visit_events.present?
40+
- if @page_visit_events.load.any?
4141
= column_chart page_visit_chart_data(@page_visit_events, @start_date, @end_date), colors: ["#88DAE3FF", "#D785E3FF", "#F7C85CFF"], library: { plugins: { legend: { position: "top", align: "end", labels: { padding: 24, boxWidth: 8, usePointStyle: true, font: { size: 16 } } } } }
4242
- else
4343
.vr-analytics-blank-states
@@ -58,16 +58,16 @@
5858
%br
5959
:escaped
6060
</main>
61-
- if @page_visit_events.present?
61+
- if @page_visit_events.load.any?
6262
.col.col-lg-3
6363
.vr-analytics-card.vr-analytics-page-visit-events-donut-chart
6464
%h5 Website visitors
65-
= pie_chart visitors_chart_data(@visits), colors: ['#F7A47B', '#B5E69A'], donut: true, library: { cutout: 85, plugins: { legend: { position: "bottom", align: 'start', labels: {boxWidth: 8, usePointStyle: true, font: { size: 16 } } } } }
65+
= pie_chart visitors_chart_data(@page_visit_events), colors: ['#F7A47B', '#B5E69A'], donut: true, library: { cutout: 85, plugins: { legend: { position: "bottom", align: 'start', labels: {boxWidth: 8, usePointStyle: true, font: { size: 16 } } } } }
6666

6767

6868
%hr.m-0
69-
= render partial: 'events', locals: { events: @click_events, previous_period_events: @previous_period_click_events, title: 'Clicks', type: 'clickables' }
70-
- unless @click_events.present?
69+
= render partial: 'events', locals: { events_exists: @click_events.load.any?, events_count: @click_events.size, label_grouped_event: @click_events.with_label.group(:label).pluck(:label, Arel.sql('json_agg(ahoy_events.name)')), previous_period_events_count: @previous_period_click_events.size, previous_grouped_events: @previous_period_click_events.with_label.group(:label).size, title: 'Clicks', type: 'clickables' }
70+
- unless @click_events.load.any?
7171
.vr-analytics-blank-states
7272
There are no click events within the selected date range.
7373
%br
@@ -86,7 +86,7 @@
8686
.vr-analytics-sub-title.col.col-12.col-md-2
8787
Watch time
8888

89-
- if @video_view_events.present?
89+
- if @video_view_events.load.any?
9090
.col.col-md-10
9191
.row
9292
- watch_time = total_watch_time(@video_view_events)
@@ -121,11 +121,11 @@
121121
= display_percent_change(view_percent, previous_view_percent)
122122
.vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(view_percent, previous_view_percent, params[:interval], @start_date, @end_date) }
123123
?
124-
- if @video_view_events.present?
124+
- if @video_view_events.load.any?
125125
.vr-analytics-section-body
126126
.vr-analytics-event-label
127127
- top_videos = top_three_videos(@video_view_events, @previous_period_video_view_events)
128-
= "Top #{top_videos.count} videos"
128+
= "Top #{top_videos.size} videos"
129129

130130
.row.mt-4
131131
- top_videos.each do |event|
@@ -184,8 +184,8 @@
184184
Please make sure 'data-violet-resource-id' is present and Analytics mapping is populated
185185

186186
%hr.m-0
187-
= render partial: 'events', locals: { events: @form_submit_events, previous_period_events: @previous_period_form_submit_events, title: 'Form Submissions', type: 'submitables' }
188-
- unless @form_submit_events.present?
187+
= render partial: 'events', locals: { events_exists: @form_submit_events.load.any?, events_count: @form_submit_events.size, label_grouped_event: @form_submit_events.with_label.group(:label).pluck(:label, Arel.sql('json_agg(ahoy_events.name)')), previous_period_events_count: @previous_period_form_submit_events.size, previous_grouped_events: @previous_period_form_submit_events.with_label.group(:label).size, title: 'Form Submissions', type: 'submitables' }
188+
- unless @form_submit_events.load.any?
189189
.vr-analytics-blank-states
190190
There are no form submission events within the selected date range.
191191
%br
@@ -217,8 +217,8 @@
217217
</form>
218218

219219
%hr.m-0
220-
= render partial: 'events', locals: { events: @section_view_events, previous_period_events: @previous_period_section_view_events, title: 'Section Views', type: 'viewables' }
221-
- unless @section_view_events.present?
220+
= render partial: 'events', locals: { events_exists: @section_view_events.load.any?, events_count: @section_view_events.size, label_grouped_event: @section_view_events.with_label.group(:label).pluck(:label, Arel.sql('json_agg(ahoy_events.name)')), previous_period_events_count: @previous_period_section_view_events.size, previous_grouped_events: @previous_period_section_view_events.with_label.group(:label).size, title: 'Section Views', type: 'viewables' }
221+
- unless @section_view_events.load.any?
222222
.vr-analytics-blank-states
223223
There are no section view events within the selected date range.
224224
%br
@@ -252,4 +252,4 @@
252252
By default, the threshold value is 0.75
253253

254254
%hr.m-0
255-
= render partial: 'events', locals: { events: @legacy_and_system_events, previous_period_events: @previous_period_legacy_and_system_events, title: 'Events', type: 'events' }
255+
= render partial: 'events', locals: { events_exists: @legacy_and_system_events.load.any?, events_count: @legacy_and_system_events.size, label_grouped_event: @legacy_and_system_events.with_label.group(:label).pluck(:label, Arel.sql('json_agg(ahoy_events.name)')), previous_period_events_count: @previous_period_legacy_and_system_events.size, previous_grouped_events: @previous_period_legacy_and_system_events.with_label.group(:label).size, title: 'Events', type: 'events' }

0 commit comments

Comments
 (0)