Skip to content

Commit 5bd2c84

Browse files
committed
feat: use TestWakatimeService
1 parent c0eca77 commit 5bd2c84

File tree

2 files changed

+188
-16
lines changed

2 files changed

+188
-16
lines changed

app/controllers/api/v1/stats_controller.rb

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,33 @@ def user_stats
6161
service_params[:scope] = scope if scope.present?
6262

6363
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)
72-
end
64+
# dis just test and we don't want to affect other services!
65+
if params[:test_param] == "true"
66+
service_params[:boundary_aware] = params[:boundary_aware] == "true"
7367

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
68+
summary = TestWakatimeService.new(**service_params).generate_summary
69+
return render json: { total_seconds: summary[:total_seconds] }
7870
else
79-
query.duration_seconds || 0
71+
query = @user.heartbeats
72+
.coding_only
73+
.with_valid_timestamps
74+
.where(time: start_date..end_date)
75+
76+
if params[:filter_by_project].present?
77+
filter_by_project = params[:filter_by_project].split(",")
78+
query = query.where(project: filter_by_project)
79+
end
80+
81+
# do the boundary thingie if requested
82+
use_boundary_aware = params[:boundary_aware] == "true"
83+
total_seconds = if use_boundary_aware
84+
Heartbeat.duration_seconds_boundary_aware(query, start_date.to_f, end_date.to_f) || 0
85+
else
86+
query.duration_seconds || 0
87+
end
88+
89+
return render json: { total_seconds: total_seconds }
8090
end
81-
82-
return render json: { total_seconds: total_seconds }
8391
end
8492

8593
summary = WakatimeService.new(**service_params).generate_summary

lib/test_wakatime_service.rb

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

0 commit comments

Comments
 (0)