Add heartbeat import (#469)

This commit is contained in:
Jeffrey Wang 2025-08-18 22:51:18 -04:00 committed by GitHub
parent fac7758391
commit 9d8cc0d75d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 192 additions and 2 deletions

View file

@ -64,7 +64,7 @@ module My
end
}
filename = "heartbeats_#{current_user.slack_uid}_#{start_date.strftime('%Y%m%d')}_#{end_date.strftime('%Y%m%d')}.json"
filename = "heartbeats_#{current_user.slack_uid}_#{start_date.strftime("%Y%m%d")}_#{end_date.strftime("%Y%m%d")}.json"
respond_to do |format|
format.json {
@ -76,6 +76,56 @@ module My
end
end
def import
unless Rails.env.development?
redirect_to my_settings_path, alert: "Hey you! This is noit a dev env, STOP DOING THIS!!!!!) Also, idk why this is happning, you should not be able to see this button hmm...."
return
end
unless params[:heartbeat_file].present?
redirect_to my_settings_path, alert: "pls select a file to import"
return
end
file = params[:heartbeat_file]
unless file.content_type == "application/json" || file.original_filename.ends_with?(".json")
redirect_to my_settings_path, alert: "pls upload only json (download from the button above it)"
return
end
begin
file_content = file.read.force_encoding("UTF-8")
rescue => e
redirect_to my_settings_path, alert: "error reading file: #{e.message}"
return
end
result = HeartbeatImportService.import_from_file(file_content, current_user)
if result[:success]
message = "Imported #{result[:imported_count]} out of #{result[:total_count]} heartbeats"
if result[:skipped_count] > 0
message += " (#{result[:skipped_count]} skipped cause they were duplicates)"
end
if result[:errors].any?
error_count = result[:errors].length
if error_count <= 3
message += ". Errors occurred: #{result[:errors].join("; ")}"
else
message += ". #{error_count} errors occurred. First few: #{result[:errors].first(2).join("; ")}..."
end
end
redirect_to root_path, notice: message
else
error_message = "Import failed: #{result[:error]}"
if result[:errors].any? && result[:errors].length > 1
error_message += "Errors: #{result[:errors][1..2].join("; ")}"
end
redirect_to my_settings_path, alert: error_message
end
end
private
def ensure_current_user

View file

@ -0,0 +1,105 @@
class HeartbeatImportService
def self.import_from_file(file_content, user)
unless Rails.env.development?
raise StandardError, "Not dev env, not running"
end
begin
parsed_data = JSON.parse(file_content)
rescue JSON::ParserError => e
raise StandardError, "Not json: #{e.message}"
end
unless parsed_data.is_a?(Hash) && parsed_data["heartbeats"].is_a?(Array)
raise StandardError, "Not correct format, download from /my/settings on the offical hackatime then import here"
end
heartbeats_data = parsed_data["heartbeats"]
imported_count = 0
skipped_count = 0
errors = []
cc = 817263
heartbeats_data.each_slice(100) do |batch|
records_to_upsert = []
batch.each_with_index do |heartbeat_data, index|
begin
time_value = if heartbeat_data["time"].is_a?(String)
Time.parse(heartbeat_data["time"]).to_f
else
heartbeat_data["time"].to_f
end
attrs = {
user_id: user.id,
time: time_value,
entity: heartbeat_data["entity"],
type: heartbeat_data["type"],
category: heartbeat_data["category"] || "coding",
project: heartbeat_data["project"],
language: heartbeat_data["language"],
editor: heartbeat_data["editor"],
operating_system: heartbeat_data["operating_system"],
machine: heartbeat_data["machine"],
branch: heartbeat_data["branch"],
user_agent: heartbeat_data["user_agent"],
is_write: heartbeat_data["is_write"] || false,
line_additions: heartbeat_data["line_additions"],
line_deletions: heartbeat_data["line_deletions"],
lineno: heartbeat_data["lineno"],
lines: heartbeat_data["lines"],
cursorpos: heartbeat_data["cursorpos"],
dependencies: heartbeat_data["dependencies"] || [],
project_root_count: heartbeat_data["project_root_count"],
source_type: :wakapi_import,
raw_data: heartbeat_data.slice(*Heartbeat.indexed_attributes)
}
attrs[:fields_hash] = Heartbeat.generate_fields_hash(attrs)
print(attrs[:fields_hash])
print("\n")
records_to_upsert << attrs
rescue => e
errors << "Row #{index + 1}: #{e.message}"
next
end
end
if records_to_upsert.any?
print("importing!!!!!!!!!!!!!!!!!!!!!!")
print("\n")
begin
# Copied from migrate user from hackatime (app\jobs\migrate_user_from_hackatime_job.rb)
records_to_upsert = records_to_upsert.group_by { |r| r[:fields_hash] }.map do |_, records|
records.max_by { |r| r[:time] }
end
result = Heartbeat.upsert_all(records_to_upsert, unique_by: [ :fields_hash ])
imported_count += result.length
rescue => e
errors << "Import error: #{e.message}"
print(e.message)
print("\n")
end
end
end
{
success: true,
imported_count: imported_count,
total_count: heartbeats_data.length,
skipped_count: heartbeats_data.length - imported_count,
errors: errors
}
rescue => e
{
success: false,
error: e.message,
imported_count: 0,
total_count: 0,
skipped_count: 0,
errors: [ e.message ]
}
end
end

View file

@ -420,6 +420,38 @@
</div>
</div>
<% dev_tool do %>
<div class="p-6 bg-gray-800 border border-gray-600 rounded">
<div class="flex items-center gap-3 mb-3">
<div class="p-2 bg-green-600/10 rounded">
<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<h4 class="text-white font-medium">Import Heartbeat Data</h4>
</div>
<p class="text-gray-300 text-sm mb-4">Import ur data from real hackatime to test stuff with.</p>
<p class="text-gray-300 text-sm mb-4">PS: your console will be spammed and might crash ur dev env so be carefull if the file is very big</p>
<%= form_with url: import_my_heartbeats_path, method: :post, multipart: true, local: true, class: "space-y-4" do |form| %>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Select JSON File</label>
<%= form.file_field :heartbeat_file,
accept: ".json,application/json",
class: "w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-white hover:file:bg-red transition-colors",
required: true %>
</div>
<div class="flex gap-3">
<%= form.submit "Import Heartbeats",
class: "bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center gap-2",
data: { confirm: "Are you sure you want to import heartbeats? This will add new data to your account." } %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>

View file

@ -113,6 +113,7 @@ Rails.application.routes.draw do
resources :heartbeats, only: [] do
collection do
get :export
post :import
end
end
end

View file

@ -7,7 +7,9 @@ if Rails.env.development?
# Creating test user
test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user|
user.username = 'testuser'
user.is_admin = true
# Before you had user.is_admin = true, does not work, changed it to that, looks like it works but idk how to use the admin pages so pls check this, i just guess coded this, the cmd to seed the db works without errors
user.set_admin_level(:superadmin)
# Ensure timezone is set to avoid nil timezone issues
user.timezone = 'America/New_York'
end