|
| 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