Skip to content

Commit 63cbc70

Browse files
committed
fix: solve analytics dashboard issues
1 parent 523815b commit 63cbc70

2 files changed

Lines changed: 43 additions & 13 deletions

File tree

app/modules/analytics/controllers/competitive_controller.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,18 +153,32 @@ def build_ban_performance(rows, total_games)
153153
end.sort_by { |s| -s[:ban_count] }
154154
end
155155

156+
# Builds blue/red side win-rate stats from in-memory rows.
157+
#
158+
# Side values in the DB are validated as lowercase ('blue', 'red'), but records
159+
# ingested from external sources may have nil or differently-cased values (e.g.
160+
# 'Blue', 'RED'). Those records are normalised via downcase before matching.
161+
# Records with nil or unrecognised side values are excluded from both side
162+
# buckets and reported in the `unaccounted` key. The sum
163+
# blue.games + red.games may therefore be less than total_matches — this is
164+
# intentional and expected when incomplete data exists.
156165
def build_side_performance(rows)
157-
%w[blue red].each_with_object({}) do |side, result|
158-
side_rows = rows.select { |m| m.side == side }
166+
valid_sides = %w[blue red]
167+
result = valid_sides.each_with_object({}) do |side, hash|
168+
side_rows = rows.select { |m| m.side&.downcase == side }
159169
games = side_rows.size
160170
wins = side_rows.count(&:victory)
161-
result[side] = {
171+
hash[side] = {
162172
games: games,
163173
wins: wins,
164174
losses: games - wins,
165175
win_rate: games.positive? ? (wins.to_f / games * 100).round(1) : 0
166176
}
167177
end
178+
179+
accounted = result['blue'][:games] + result['red'][:games]
180+
result['unaccounted'] = rows.size - accounted
181+
result
168182
end
169183

170184
def build_role_performance(rows)
@@ -303,7 +317,7 @@ def empty_draft_performance
303317
{
304318
pick_performance: [],
305319
ban_performance: [],
306-
side_performance: { blue: side_zeros, red: side_zeros },
320+
side_performance: { 'blue' => side_zeros, 'red' => side_zeros, 'unaccounted' => 0 },
307321
role_performance: [],
308322
meta_champions: [],
309323
total_matches: 0

app/modules/analytics/controllers/performance_controller.rb

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,12 @@ class PerformanceController < Api::V1::BaseController
4444
# @param player_id [Integer] Player ID for individual stats (optional)
4545
# @return [JSON] Performance analytics data
4646
def index
47-
matches = apply_date_filters(organization_scoped(Match))
48-
4947
# Use active players for team-wide stats (best performers, role breakdown, etc.)
5048
# but validate player_id against ALL org players so that bench/trial/inactive
5149
# players can still have their individual stats viewed.
52-
active_players = organization_scoped(Player).includes(:organization).active
5350
all_org_players = organization_scoped(Player).includes(:organization)
54-
5551
player_id = params[:player_id].presence
52+
5653
if player_id.present? && !all_org_players.exists?(id: player_id)
5754
return render_error(
5855
message: 'Player not found',
@@ -61,11 +58,12 @@ def index
6158
)
6259
end
6360

64-
service = PerformanceAnalyticsService.new(matches, active_players)
65-
performance_data = service.calculate_performance_data(player_id: player_id, all_players: all_org_players)
66-
67-
data = cache_response('analytics/performance', expires_in: 15.minutes) do
68-
performance_data
61+
cache_key = performance_cache_key(player_id)
62+
data = cache_response(cache_key, expires_in: 15.minutes) do
63+
matches = apply_date_filters(organization_scoped(Match))
64+
active_players = organization_scoped(Player).includes(:organization).active
65+
service = PerformanceAnalyticsService.new(matches, active_players)
66+
service.calculate_performance_data(player_id: player_id, all_players: all_org_players)
6967
end
7068

7169
render_success(data)
@@ -96,6 +94,24 @@ def apply_date_filters(matches)
9694
end
9795
end
9896

97+
# Builds a cache key segment that distinguishes team vs player requests
98+
# and incorporates active date-filter params so that different filter
99+
# combinations never share a cached result.
100+
#
101+
# The key is intentionally short and URL-safe; the org-scoping prefix
102+
# is added by the Cacheable concern's +build_cache_key+ method.
103+
#
104+
# @param player_id [String, nil] player_id param value (nil for team view)
105+
# @return [String] cache key segment, e.g.
106+
# "analytics/performance/team",
107+
# "analytics/performance/team/month",
108+
# "analytics/performance/player/42/2025-01-01-2025-01-31"
109+
def performance_cache_key(player_id)
110+
base = player_id ? "analytics/performance/player/#{player_id}" : 'analytics/performance/team'
111+
suffix = [params[:time_period], params[:start_date], params[:end_date]].compact.join('-')
112+
suffix.present? ? "#{base}/#{suffix}" : base
113+
end
114+
99115
# Converts time period string to number of days
100116
#
101117
# @param period [String] Time period (week, month, season)

0 commit comments

Comments
 (0)