Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 116 additions & 46 deletions app/controllers/concerns/dashboard_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ module DashboardData

FILTER_OPTIONS_CACHE_VERSION = "v1".freeze
WEEKLY_PROJECT_DIMENSION = "weekly_project".freeze
DAILY_DURATION_DIMENSION = "daily_duration".freeze
TODAY_CONTEXT_DIMENSION = "today_context".freeze
TODAY_TOTAL_DURATION_DIMENSION = "today_total_duration".freeze
TODAY_LANGUAGE_COUNT_DIMENSION = "today_language_count".freeze
TODAY_EDITOR_COUNT_DIMENSION = "today_editor_count".freeze

private

Expand All @@ -22,9 +27,12 @@ def filterable_dashboard_data

def activity_graph_data
tz = current_user.timezone
key = "user_#{current_user.id}_daily_durations_#{tz}"
durations = Rails.cache.fetch(key, expires_in: 1.minute) do
Time.use_zone(tz) { current_user.heartbeats.daily_durations(user_timezone: tz).to_h }
durations = dashboard_rollup_daily_durations
unless durations
key = "user_#{current_user.id}_daily_durations_#{tz}"
durations = Rails.cache.fetch(key, expires_in: 1.minute) do
Time.use_zone(tz) { current_user.heartbeats.daily_durations(user_timezone: tz).to_h }
end
end

{
Expand All @@ -38,39 +46,44 @@ def activity_graph_data
end

def today_stats_data
h = ApplicationController.helpers
Time.use_zone(current_user.timezone) do
rows = current_user.heartbeats.today
.select(:language, :editor,
"COUNT(*) OVER (PARTITION BY language) as language_count",
"COUNT(*) OVER (PARTITION BY editor) as editor_count")
.distinct.to_a

lang_counts = rows
.map { |r| [ r.language&.categorize_language, r.language_count ] }
.reject { |l, _| l.blank? }
.group_by(&:first).transform_values { |p| p.sum(&:last) }
.sort_by { |_, c| -c }

ed_counts = rows
.map { |r| [ r.editor, r.editor_count ] }
.reject { |e, _| e.blank? }.uniq
.sort_by { |_, c| -c }

todays_languages = lang_counts.map { |l, _| h.display_language_name(l) }
todays_editors = ed_counts.map { |e, _| h.display_editor_name(e) }
todays_duration = current_user.heartbeats.today.duration_seconds
show_logged_time_sentence = todays_duration > 1.minute && (todays_languages.any? || todays_editors.any?)

{
show_logged_time_sentence: show_logged_time_sentence,
todays_duration_display: h.short_time_detailed(todays_duration.to_i),
todays_languages: todays_languages,
todays_editors: todays_editors
}
cache_key, timezone = today_stats_cache_key
Rails.cache.fetch(cache_key, expires_in: 1.minute) do
rollup_stats = dashboard_rollup_today_stats(timezone)
if rollup_stats
rollup_stats
else
today_data = DashboardRollupRefreshService.today_rollup_data_for(current_user)
DashboardRollupRefreshService.upsert_today_rollup!(user: current_user, data: today_data)
dashboard_clear_rollup_rows_memo

build_today_stats_from_rollup_data(today_data)
end
end
end
Comment thread
skyfallwastaken marked this conversation as resolved.

def build_today_stats_from_rollup_data(data)
h = ApplicationController.helpers
todays_languages = data.fetch(:language_counts).sort_by { |_, count| -count }
.map { |language, _| h.display_language_name(language) }
todays_editors = data.fetch(:editor_counts).sort_by { |_, count| -count }
.map { |editor, _| h.display_editor_name(editor) }
todays_duration = data.fetch(:total_duration).to_i

{
show_logged_time_sentence: todays_duration > 1.minute && (todays_languages.any? || todays_editors.any?),
todays_duration_display: h.short_time_detailed(todays_duration),
todays_languages: todays_languages,
todays_editors: todays_editors
}
end

def today_stats_cache_key
timezone = current_user.timezone
local_date = Time.use_zone(timezone) { Time.zone.today.iso8601 }
key = [ "user", current_user.id, "today_stats", timezone, local_date ]
[ key, timezone ]
end

def dashboard_filters
%i[project language operating_system editor category]
end
Expand Down Expand Up @@ -229,23 +242,13 @@ def dashboard_fill_aggregate_result(result:, grouped_durations:, total_time:, to
end

def dashboard_rollup_snapshot
return unless dashboard_rollups_available?
return unless dashboard_rollup_eligible?

rows = DashboardRollup.where(user_id: current_user.id).to_a
total_row = rows.find(&:total_dimension?)
unless total_row
DashboardRollupRefreshJob.schedule_for(current_user.id, wait: 0.seconds)
return
end

if DashboardRollup.dirty?(current_user.id)
DashboardRollupRefreshJob.schedule_for(current_user.id, wait: 0.seconds)
elsif dashboard_rollup_time_fingerprint(total_row.source_max_heartbeat_time) != dashboard_rollup_source_max_heartbeat_time
DashboardRollupRefreshJob.schedule_for(current_user.id, wait: 0.seconds)
end
rows = dashboard_rollup_rows
return unless rows

grouped_rows = rows.reject(&:total_dimension?).group_by(&:dimension)
total_row = rows.find(&:total_dimension?)

{
total_time: total_row.total_seconds,
Expand All @@ -270,6 +273,73 @@ def dashboard_rollups_available?
false
end

def dashboard_rollup_rows
return @dashboard_rollup_rows if defined?(@dashboard_rollup_rows)

unless dashboard_rollups_available?
@dashboard_rollup_rows = nil
return
end

rows = DashboardRollup.where(user_id: current_user.id).to_a
total_row = rows.find(&:total_dimension?)
unless total_row
DashboardRollupRefreshJob.schedule_for(current_user.id, wait: 0.seconds)
@dashboard_rollup_rows = nil
return
end

if DashboardRollup.dirty?(current_user.id)
DashboardRollupRefreshJob.schedule_for(current_user.id, wait: 0.seconds)
elsif dashboard_rollup_time_fingerprint(total_row.source_max_heartbeat_time) != dashboard_rollup_source_max_heartbeat_time
DashboardRollupRefreshJob.schedule_for(current_user.id, wait: 0.seconds)
end

@dashboard_rollup_rows = rows
end

def dashboard_rollup_grouped_rows
return @dashboard_rollup_grouped_rows if defined?(@dashboard_rollup_grouped_rows)

rows = dashboard_rollup_rows
@dashboard_rollup_grouped_rows = rows ? rows.reject(&:total_dimension?).group_by(&:dimension) : {}
end

def dashboard_rollup_daily_durations
rows = dashboard_rollup_grouped_rows.fetch(DAILY_DURATION_DIMENSION, [])
return if rows.empty?

rows.to_h { |row| [ row.bucket, row.total_seconds.to_i ] }
end

def dashboard_rollup_today_stats(timezone)
context_row = dashboard_rollup_grouped_rows.fetch(TODAY_CONTEXT_DIMENSION, []).first
return unless context_row

context_timezone, context_date = JSON.parse(context_row.bucket_value)
current_date = Time.use_zone(timezone) { Time.zone.today.iso8601 }
return unless context_timezone == timezone && context_date == current_date

total_row = dashboard_rollup_grouped_rows.fetch(TODAY_TOTAL_DURATION_DIMENSION, []).first
return unless total_row

data = {
timezone: timezone,
local_date: current_date,
total_duration: total_row.total_seconds.to_i,
language_counts: dashboard_rollup_grouped_rows.fetch(TODAY_LANGUAGE_COUNT_DIMENSION, []).to_h { |row| [ row.bucket.to_s, row.total_seconds.to_i ] },
editor_counts: dashboard_rollup_grouped_rows.fetch(TODAY_EDITOR_COUNT_DIMENSION, []).to_h { |row| [ row.bucket.to_s, row.total_seconds.to_i ] }
}
build_today_stats_from_rollup_data(data)
rescue JSON::ParserError
nil
end

def dashboard_clear_rollup_rows_memo
remove_instance_variable(:@dashboard_rollup_rows) if instance_variable_defined?(:@dashboard_rollup_rows)
remove_instance_variable(:@dashboard_rollup_grouped_rows) if instance_variable_defined?(:@dashboard_rollup_grouped_rows)
end

def dashboard_rollup_source_max_heartbeat_time
dashboard_rollup_time_fingerprint(current_user.heartbeats.maximum(:time))
end
Expand Down
13 changes: 5 additions & 8 deletions app/controllers/static_pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,10 @@ def programming_goals_progress_data
goals = current_user.goals.order(:id)
return [] if goals.blank?

goals_hash = ActiveSupport::Digest.hexdigest(
goals.pluck(:id, :period, :target_seconds, :languages, :projects).to_json
)
cache_key = "user_#{current_user.id}_programming_goals_progress_#{current_user.timezone}_#{goals_hash}"

Rails.cache.fetch(cache_key, expires_in: 1.minute) do
ProgrammingGoalsProgressService.new(user: current_user, goals: goals).call
end
ProgrammingGoalsProgressService.new(
user: current_user,
goals: goals,
rollup_rows: dashboard_rollup_rows
).call
Comment thread
skyfallwastaken marked this conversation as resolved.
end
end
18 changes: 17 additions & 1 deletion app/models/dashboard_rollup.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
class DashboardRollup < ApplicationRecord
DIMENSIONS = %w[total project language editor operating_system category weekly_project].freeze
DIMENSIONS = %w[
total
project
language
editor
operating_system
category
weekly_project
daily_duration
today_context
today_total_duration
today_language_count
today_editor_count
goals_period_total
goals_period_project
goals_period_language
].freeze
TOTAL_DIMENSION = "total".freeze
DIRTY_CACHE_KEY_PREFIX = "dashboard_rollup_dirty".freeze

Expand Down
14 changes: 11 additions & 3 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def streak_days_formatted
compliment_text: 2
}

after_save :invalidate_activity_graph_cache, if: :saved_change_to_timezone?
after_save :handle_timezone_change, if: :saved_change_to_timezone?

def flipper_id
"User;#{id}"
Expand Down Expand Up @@ -339,8 +339,16 @@ def self.not_suspect

private

def invalidate_activity_graph_cache
Rails.cache.delete("user_#{id}_daily_durations")
def handle_timezone_change
previous_timezone, current_timezone = saved_change_to_timezone
[ previous_timezone, current_timezone ].compact_blank.uniq.each do |timezone|
Rails.cache.delete("user_#{id}_daily_durations_#{timezone}")

local_date = Time.use_zone(timezone) { Time.zone.today.iso8601 }
Rails.cache.delete([ "user", id, "today_stats", timezone, local_date ])
end

DashboardRollupRefreshJob.schedule_for(id, wait: 0.seconds)
end

def track_signup
Expand Down
Loading
Loading