Skip to content

Commit d9cd8f0

Browse files
authored
Merge pull request #433 from hackclub/som-fix
Summer of Making fix
2 parents c0eca77 + 03347ec commit d9cd8f0

File tree

2 files changed

+191
-18
lines changed

2 files changed

+191
-18
lines changed

app/controllers/api/v1/stats_controller.rb

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,30 +60,42 @@ def user_stats
6060
service_params[:end_date] = end_date
6161
service_params[:scope] = scope if scope.present?
6262

63-
if params[:total_seconds] == "true"
64-
query = @user.heartbeats
65-
.coding_only
66-
.with_valid_timestamps
67-
.where(time: start_date..end_date)
68-
69-
if params[:filter_by_project].present?
70-
filter_by_project = params[:filter_by_project].split(",")
71-
query = query.where(project: filter_by_project)
63+
# use TestWakatimeService when test_param=true for all requests
64+
if params[:test_param] == "true"
65+
service_params[:boundary_aware] = true # always and i mean always use boundary aware in testwakatime service
66+
67+
if params[:total_seconds] == "true"
68+
summary = TestWakatimeService.new(**service_params).generate_summary
69+
return render json: { total_seconds: summary[:total_seconds] }
7270
end
7371

74-
# do the boundary thingie if requested
75-
use_boundary_aware = params[:boundary_aware] == "true"
76-
total_seconds = if use_boundary_aware
77-
Heartbeat.duration_seconds_boundary_aware(query, start_date.to_f, end_date.to_f) || 0
78-
else
79-
query.duration_seconds || 0
72+
summary = TestWakatimeService.new(**service_params).generate_summary
73+
else
74+
if params[:total_seconds] == "true"
75+
query = @user.heartbeats
76+
.coding_only
77+
.with_valid_timestamps
78+
.where(time: start_date..end_date)
79+
80+
if params[:filter_by_project].present?
81+
filter_by_project = params[:filter_by_project].split(",")
82+
query = query.where(project: filter_by_project)
83+
end
84+
85+
# do the boundary thingie if requested
86+
use_boundary_aware = params[:boundary_aware] == "true"
87+
total_seconds = if use_boundary_aware
88+
Heartbeat.duration_seconds_boundary_aware(query, start_date.to_f, end_date.to_f) || 0
89+
else
90+
query.duration_seconds || 0
91+
end
92+
93+
return render json: { total_seconds: total_seconds }
8094
end
8195

82-
return render json: { total_seconds: total_seconds }
96+
summary = WakatimeService.new(**service_params).generate_summary
8397
end
8498

85-
summary = WakatimeService.new(**service_params).generate_summary
86-
8799
if params[:features]&.include?("projects") && params[:filter_by_project].present?
88100
filter_by_project = params[:filter_by_project].split(",")
89101
heartbeats = @user.heartbeats

lib/test_wakatime_service.rb

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
include ApplicationHelper
2+
3+
class TestWakatimeService
4+
def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, scope: nil, boundary_aware: false)
5+
@scope = scope || Heartbeat.all
6+
@scope = @scope.coding_only
7+
@scope = @scope.with_valid_timestamps
8+
@user = user
9+
@boundary_aware = boundary_aware
10+
11+
@start_date = convert_to_unix_timestamp(start_date)
12+
@end_date = convert_to_unix_timestamp(end_date)
13+
14+
# Default to 1 year ago if no start_date provided or if no data exists
15+
@start_date = @start_date || @scope.minimum(:time) || 1.year.ago.to_i
16+
@end_date = @end_date || @scope.maximum(:time) || Time.current.to_i
17+
18+
@scope = @scope.where("time >= ? AND time < ?", @start_date, @end_date)
19+
20+
@limit = limit
21+
@limit = nil if @limit&.zero?
22+
23+
@scope = @scope.where(user_id: @user.id) if @user.present?
24+
25+
@specific_filters = specific_filters
26+
@allow_cache = allow_cache
27+
end
28+
29+
def generate_summary
30+
summary = {}
31+
32+
summary[:username] = @user.username if @user.present?
33+
summary[:user_id] = @user.id.to_s if @user.present?
34+
summary[:is_coding_activity_visible] = true if @user.present?
35+
summary[:is_other_usage_visible] = true if @user.present?
36+
summary[:status] = "ok"
37+
38+
@start_time = @start_date
39+
@end_time = @end_date
40+
41+
summary[:start] = Time.at(@start_time).strftime("%Y-%m-%dT%H:%M:%SZ")
42+
summary[:end] = Time.at(@end_time).strftime("%Y-%m-%dT%H:%M:%SZ")
43+
44+
summary[:range] = "all_time"
45+
summary[:human_readable_range] = "All Time"
46+
47+
@total_seconds = if @boundary_aware
48+
result = Heartbeat.duration_seconds_boundary_aware(@scope, @start_date, @end_date) || 0
49+
result
50+
else
51+
result = @scope.duration_seconds || 0
52+
result
53+
end
54+
55+
summary[:total_seconds] = @total_seconds
56+
57+
@total_days = (@end_time - @start_time) / 86400
58+
summary[:daily_average] = @total_days.zero? ? 0 : @total_seconds / @total_days
59+
60+
summary[:human_readable_total] = ApplicationController.helpers.short_time_detailed(@total_seconds)
61+
summary[:human_readable_daily_average] = ApplicationController.helpers.short_time_detailed(summary[:daily_average])
62+
63+
summary[:languages] = generate_summary_chunk(:language) if @specific_filters.include?(:languages)
64+
summary[:projects] = generate_summary_chunk(:project) if @specific_filters.include?(:projects)
65+
66+
summary
67+
end
68+
69+
def generate_summary_chunk(group_by)
70+
result = []
71+
@scope.group(group_by).duration_seconds.each do |key, value|
72+
result << {
73+
name: key.presence || "Other",
74+
total_seconds: value,
75+
text: ApplicationController.helpers.short_time_simple(value),
76+
hours: value / 3600,
77+
minutes: (value % 3600) / 60,
78+
percent: (100.0 * value / @total_seconds).round(2),
79+
digital: ApplicationController.helpers.digital_time(value)
80+
}
81+
end
82+
result = result.sort_by { |item| -item[:total_seconds] }
83+
result = result.first(@limit) if @limit.present?
84+
result
85+
end
86+
87+
def self.parse_user_agent(user_agent)
88+
# Based on https://github.com/muety/wakapi/blob/b3668085c01dc0724d8330f4d51efd5b5aecaeb2/utils/http.go#L89
89+
90+
# Regex pattern to match wakatime client user agents
91+
user_agent_pattern = /wakatime\/[^ ]+ \(([^)]+)\)(?: [^ ]+ ([^\/]+)(?:\/([^\/]+))?)?/
92+
93+
if matches = user_agent.match(user_agent_pattern)
94+
os = matches[1].split("-").first
95+
96+
editor = matches[2]
97+
editor ||= ""
98+
99+
{ os: os, editor: editor, err: nil }
100+
else
101+
# Try parsing as browser user agent as fallback
102+
if browser_ua = user_agent.match(/^([^\/]+)\/([^\/\s]+)/)
103+
# If "wakatime" is present, assume it's the browser extension
104+
if user_agent.include?("wakatime") then
105+
full_os = user_agent.split(" ")[1]
106+
if full_os.present?
107+
os = full_os.include?("_") ? full_os.split("_")[0] : full_os
108+
{ os: os, editor: browser_ua[1].downcase, err: nil }
109+
else
110+
{ os: "", editor: "", err: "failed to parse user agent string" }
111+
end
112+
else
113+
{ os: browser_ua[1], editor: browser_ua[2], err: nil }
114+
end
115+
else
116+
{ os: "", editor: "", err: "failed to parse user agent string" }
117+
end
118+
end
119+
rescue => e
120+
Rails.logger.error("Error parsing user agent string: #{e.message}")
121+
{ os: "", editor: "", err: "failed to parse user agent string" }
122+
end
123+
124+
def categorize_os(os)
125+
case os.downcase
126+
when "win" then "Windows"
127+
when "darwin" then "MacOS"
128+
when os.include?("windows") then "Windows"
129+
else os.capitalize
130+
end
131+
end
132+
133+
def categorize_editor(editor)
134+
case editor.downcase
135+
when "vscode" then "VSCode"
136+
when "KTextEditor" then "Kate"
137+
else editor.capitalize
138+
end
139+
end
140+
141+
private
142+
143+
def convert_to_unix_timestamp(timestamp)
144+
# our lord and savior stack overflow for this bit of code
145+
return nil if timestamp.nil?
146+
147+
case timestamp
148+
when String
149+
Time.parse(timestamp).to_i
150+
when Time, DateTime, Date
151+
timestamp.to_i
152+
when Numeric
153+
timestamp.to_i
154+
else
155+
nil
156+
end
157+
rescue ArgumentError => e
158+
Rails.logger.error("Error converting timestamp: #{e.message}")
159+
nil
160+
end
161+
end

0 commit comments

Comments
 (0)