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