Skip to content

Use wakatime service in stats endpoint #270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 11 additions & 86 deletions app/controllers/api/hackatime/v1/hackatime_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,94 +50,17 @@ def status_bar_today
end
end

def stats_last_7_days
Time.use_zone(@user.timezone) do
# Calculate time range within the user's timezone
start_time = (Time.current - 7.days).beginning_of_day
end_time = Time.current.end_of_day

# Convert to Unix timestamps
start_timestamp = start_time.to_i
end_timestamp = end_time.to_i

# Get heartbeats in the time range
heartbeats = @user.heartbeats.where(time: start_timestamp..end_timestamp)

# Calculate total seconds
total_seconds = heartbeats.duration_seconds.to_i

# Get unique days
days = []
heartbeats.pluck(:time).each do |timestamp|
day = Time.at(timestamp).in_time_zone(@user.timezone).to_date
days << day unless days.include?(day)
end
days_covered = days.length

# Calculate daily average
daily_average = days_covered > 0 ? (total_seconds.to_f / days_covered).round(1) : 0

# Format human readable strings
hours = total_seconds / 3600
minutes = (total_seconds % 3600) / 60
human_readable_total = "#{hours} hrs #{minutes} mins"

avg_hours = daily_average.to_i / 3600
avg_minutes = (daily_average.to_i % 3600) / 60
human_readable_daily_average = "#{avg_hours} hrs #{avg_minutes} mins"

# Calculate statistics for different categories
editors_data = calculate_category_stats(heartbeats, "editor")
languages_data = calculate_category_stats(heartbeats, "language")
projects_data = calculate_category_stats(heartbeats, "project")
machines_data = calculate_category_stats(heartbeats, "machine")
os_data = calculate_category_stats(heartbeats, "operating_system")

# Categories data
hours = total_seconds / 3600
minutes = (total_seconds % 3600) / 60
seconds = total_seconds % 60

categories = [
{
name: "coding",
total_seconds: total_seconds,
percent: 100.0,
digital: format("%d:%02d:%02d", hours, minutes, seconds),
text: human_readable_total,
hours: hours,
minutes: minutes,
seconds: seconds
}
]
# GET /api/hackatime/v1/users/:id/stats/:range
def stats
range = params[:range] || "last_7_days"
range_config = TimeRangeFilterable::RANGES[range.to_sym]
unless range_config.present?
return render json: { error: "Invalid range", message: "Invalid range, valid ranges are: #{TimeRangeFilterable::RANGES.keys.join(", ")}" }, status: :bad_request
end

result = {
data: {
username: @user.slack_uid,
user_id: @user.slack_uid,
start: start_time.iso8601,
end: end_time.iso8601,
status: "ok",
total_seconds: total_seconds,
daily_average: daily_average,
days_including_holidays: days_covered,
range: "last_7_days",
human_readable_range: "Last 7 Days",
human_readable_total: human_readable_total,
human_readable_daily_average: human_readable_daily_average,
is_coding_activity_visible: true,
is_other_usage_visible: true,
editors: editors_data,
languages: languages_data,
machines: machines_data,
projects: projects_data,
operating_systems: os_data,
categories: categories
}
}
summary = WakatimeService.new(user: @user, range: range, specific_filters: [ :editors, :languages, :projects, :machines, :operating_systems ]).generate_summary

render json: result
end
render json: { data: summary }, status: :ok and return
end

private
Expand Down Expand Up @@ -267,6 +190,8 @@ def queue_project_mapping(project_name)
end

def set_user
@user = User.find_by(id: params[:id]) and return if Rails.env.development?

api_header = request.headers["Authorization"]
raw_token = api_header&.split(" ")&.last
header_type = api_header&.split(" ")&.first
Expand Down
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def self.matches?(request)
get "/", to: "hackatime#index" # many clients seem to link this as the user's dashboard
get "/users/:id/statusbar/today", to: "hackatime#status_bar_today"
post "/users/:id/heartbeats", to: "hackatime#push_heartbeats"
get "/users/current/stats/last_7_days", to: "hackatime#stats_last_7_days"
get "/users/:id/stats/:range", to: "hackatime#stats"
end
end
end
Expand Down
39 changes: 31 additions & 8 deletions lib/wakatime_service.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
include ApplicationHelper

class WakatimeService
def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil)
def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, range: nil)
@scope = Heartbeat.all
@user = user

# Default to 1 year ago if no start_date provided or if no data exists
@start_date = start_date || @scope.minimum(:time) || 1.year.ago.to_i
@end_date = end_date || @scope.maximum(:time) || Time.current.to_i
@tz = @user&.timezone || "UTC"
@range_name = range.to_s

Time.use_zone(@tz) do
if start_date.present? || end_date.present?
@start_date = start_date || @scope.minimum(:time) || 1.year.ago.to_i
@end_date = end_date || @scope.maximum(:time) || Time.current.to_i
@range_name = "custom"
else
if range.present?
@range = TimeRangeFilterable::RANGES&.[](range.to_sym) || TimeRangeFilterable::RANGES[:all_time]
@range_name = range.to_s
else
@range = TimeRangeFilterable::RANGES&.[](:all_time)
@range_name = "all_time"
end

@start_date = @range[:calculate].call.first
@end_date = @range[:calculate].call.last
end
end

@scope = @scope.where(time: @start_date..@end_date)

Expand All @@ -32,23 +50,28 @@ def generate_summary
@start_time = @scope.minimum(:time) || @start_date
@end_time = @scope.maximum(:time) || @end_date

summary[:start] = Time.at(@start_time).strftime("%Y-%m-%dT%H:%M:%SZ")
summary[:end] = Time.at(@end_time).strftime("%Y-%m-%dT%H:%M:%SZ")
summary[:start] = Time.at(@start_time).iso8601
summary[:end] = Time.at(@end_time).iso8601

summary[:range] = "all_time"
summary[:human_readable_range] = "All Time"
summary[:range] = @range_name || "custom"
summary[:human_readable_range] = @range&.[](:human_name) || "custom"

@total_seconds = @scope.duration_seconds || 0
summary[:total_seconds] = @total_seconds

@total_days = (@end_time - @start_time) / 86400
summary[:daily_average] = @total_days.zero? ? 0 : @total_seconds / @total_days
@days_including_holidays = @scope.distinct.count(Arel.sql("DATE(timezone(?, to_timestamp(time)))", @tz))
summary[:days_including_holidays] = @days_including_holidays

summary[:human_readable_total] = ApplicationController.helpers.short_time_detailed(@total_seconds)
summary[:human_readable_daily_average] = ApplicationController.helpers.short_time_detailed(summary[:daily_average])

summary[:languages] = generate_summary_chunk(:language) if @specific_filters.include?(:languages)
summary[:projects] = generate_summary_chunk(:project) if @specific_filters.include?(:projects)
summary[:editors] = generate_summary_chunk(:editor) if @specific_filters.include?(:editors)
summary[:machines] = generate_summary_chunk(:machine) if @specific_filters.include?(:machines)
summary[:operating_systems] = generate_summary_chunk(:operating_system) if @specific_filters.include?(:operating_systems)

summary
end
Expand Down