🚚 Migration Assistant
-This will migrate your heartbeats from waka.hackclub.com to this platform.
+This will migrate your heartbeats from other services to this platform.
From b6f3aa94ab2772fb28fe6da457b370252db5b178 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:27:14 +0200 Subject: [PATCH 1/7] Initial heartbeat download and frontend impl --- app/controllers/users_controller.rb | 8 +- .../migrate_wakatimecom_heartbeats_job.rb | 79 +++++ app/views/users/edit.html.erb | 18 ++ config/routes.rb | 1 + .../20250429114602_wakatime_api_key_user.rb | 5 + db/primary_direct_schema.rb | 277 ++++++++++++++++++ db/schema.rb | 3 +- prnotes.md | 1 + 8 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb create mode 100644 db/migrate/20250429114602_wakatime_api_key_user.rb create mode 100644 db/primary_direct_schema.rb create mode 100644 prnotes.md diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 280fcb6d..6752ec6b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -36,6 +36,12 @@ def migrate_heartbeats notice: "Heartbeats & api keys migration started" end + def migrate_wakatimecom_heartbeats + OneTime::MigrateWakatimecomHeartbeatsJob.perform_later(@user.id) + redirect_to is_own_settings? ? my_settings_path : user_settings_path(@user), + notice: "Wakatime.com heartbeats migration started" + end + def wakatime_setup api_key = current_user&.api_keys&.last api_key ||= current_user.api_keys.create!(name: "Wakatime API Key") @@ -86,6 +92,6 @@ def is_own_settings? end def user_params - params.require(:user).permit(:uses_slack_status, :hackatime_extension_text_type, :timezone) + params.require(:user).permit(:uses_slack_status, :hackatime_extension_text_type, :timezone, :wakatime_api_key) end end diff --git a/app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb b/app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb new file mode 100644 index 00000000..6c1dc208 --- /dev/null +++ b/app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb @@ -0,0 +1,79 @@ +require "fileutils" +require "open-uri" + +class OneTime::MigrateWakatimecomHeartbeatsJob < ApplicationJob + queue_as :default + + include GoodJob::ActiveJobExtensions::Concurrency + + # only allow one instance of this job to run at a time + good_job_control_concurrency_with( + key: -> { "migrate_wakatimecom_heartbeats_job_#{arguments.first}" }, + total_limit: 1, + ) + + def perform(user_id) + @user = User.find(user_id) + import_heartbeats + end + + private + + def import_heartbeats + puts "starting wakatime.com heartbeats import for user #{@user.id}" + + # get dump once to check if there's already one. + # in development i've already created one and don't want to keep spamming dumps + # (it's also really slow for me, my entire coding career is in there) + dump = get_dumps + + if dump.empty? + create_dump + while true + sleep 5 + dump = get_dumps + puts "wakatime.com import for #{@user.id} is at #{dump['percent_complete']}%" + break unless dump.empty? + end + end + + output_dir = Rails.root.join('storage', 'wakatime_dumps') + FileUtils.mkdir_p(output_dir) + output_path = output_dir.join("wakatime_heartbeats_#{@user.id}.json") + + puts "downloading wakatime.com heartbeats dump for user #{@user.id}" + auth_token = Base64.strict_encode64("#{@user.wakatime_api_key}:") + + File.open(output_path, 'wb') do |file| + # i don't get why with HTTP it doesn't work... + file.write(URI.open(dump['download_url']).read) + end + + puts "wakatime.com heartbeats saved to #{output_path} for user #{@user.id}" + end + + def get_dumps + auth_token = Base64.strict_encode64("#{@user.wakatime_api_key}:") + response = HTTP.auth("Basic #{auth_token}") + .get('https://api.wakatime.com/api/v1/users/current/data_dumps') + + if response.status.success? + dumps = JSON.parse(response.body)['data'].find { |dump| dump['type'] == 'heartbeats' && dump['status'] == 'Completed' } + return dumps || {} + else + puts "Failed to fetch Wakatime.com data dumps: #{response.status} - #{response.body}" + return {} + end + end + + def create_dump + auth_token = Base64.strict_encode64("#{@user.wakatime_api_key}:") + response = HTTP.auth("Basic #{auth_token}") + .post('https://api.wakatime.com/api/v1/users/current/data_dumps', + json: { + type: 'heartbeats', + email_when_finished: false, + } + ) + end +end \ No newline at end of file diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 140f0f3f..0a827149 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -147,10 +147,28 @@
+This will migrate your heartbeats from waka.hackclub.com to this platform.
<%= button_to "Migrate heartbeats", my_settings_migrate_heartbeats_path, method: :post %> + + <% if @user.wakatime_api_key.present? %> + <%= button_to "Migrate wakatime.com", my_settings_migrate_wakatimecom_heartbeats_path, method: :post %> + <% end %> <% if @heartbeats_migration_jobs.any? %>This will migrate your heartbeats from waka.hackclub.com to this platform.
- <%= button_to "Migrate heartbeats", my_settings_migrate_heartbeats_path, method: :post %> - +This will migrate your heartbeats from other services to this platform.
+ <%= button_to "Migrate hackatime v1", my_settings_migrate_heartbeats_path, method: :post %> <% if @user.wakatime_api_key.present? %> <%= button_to "Migrate wakatime.com", my_settings_migrate_wakatimecom_heartbeats_path, method: :post %> <% end %> diff --git a/prnotes.md b/prnotes.md deleted file mode 100644 index 0d29508f..00000000 --- a/prnotes.md +++ /dev/null @@ -1,62 +0,0 @@ -i've put the wakatime api key in the user table, but i'm pretty sure it should be on the api_keys table. - -generated types for wakatime json: -```ts -export interface WakatimeHeartbeats2 { - user: User; - range: Range; - days: Day[]; -} - -export interface Day { - date: Date; - heartbeats: Heartbeat[]; -} - -export interface Heartbeat { - branch: Branch | null; - category: Category; - created_at: Date; - cursorpos: number | null; - dependencies: string[]; - entity: string; - id: string; - is_write: boolean; - language: Language | null; - line_additions: null; - line_deletions: null; - lineno: number | null; - lines: number | null; - machine_name_id: string; - project: null | string; - project_root_count: number | null; - time: number; - type: Type; - user_agent_id: string; - user_id: string; -} - -export enum Branch { - Main = "main", -} - -export enum Type { - Domain = "domain", - File = "file", -} - -export interface Range { - end: number; - start: number; -} - -export enum Language { - C = "C", - CPlusPlus = "C++", - CSharp = "C#", - Java = "Java", - JavaScript = "JavaScript", - Python = "Python", - Ruby = "Ruby", -} -``` \ No newline at end of file From 8bbcbedfd4cc8ccb0772df588faccdebeb54b71d Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:16:37 +0200 Subject: [PATCH 5/7] use deduplication solution by wakatime ceo --- .../migrate_wakatimecom_heartbeats_job.rb | 16 +++++++++++----- app/views/users/edit.html.erb | 5 ++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb b/app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb index e3c3238c..79ca554b 100644 --- a/app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb +++ b/app/jobs/one_time/migrate_wakatimecom_heartbeats_job.rb @@ -14,6 +14,7 @@ class OneTime::MigrateWakatimecomHeartbeatsJob < ApplicationJob def perform(user_id) @user = User.find(user_id) + @api_key = WakatimeMirror.find_by(endpoint_url: "https://wakatime.com/api/v1", user_id: @user.id)&.encrypted_api_key import_heartbeats end @@ -41,7 +42,11 @@ def import_heartbeats machines = get_machines agents = get_agents - existing_hashes = Set.new(Heartbeat.where(user_id: @user.id).pluck(:fields_hash)) + existing_heartbeats = Heartbeat.where(user_id: @user.id) + .select(:entity, :type, :project, :branch, :language, :time) + .map { |h| generate_dedup_key(h.entity, h.type, h.project, h.branch, h.language, h.time) } + .to_set + # this could explode, let's see how it ends up. parsed_json = JSON.parse(File.read(output_path)) parsed_json = parsed_json["days"].select { |day| day["heartbeats"].any? } @@ -74,9 +79,10 @@ def import_heartbeats source_type: 3 # wakatimecom_import } - attrs[:fields_hash] = Heartbeat.generate_fields_hash(attrs) - if existing_hashes.include?(attrs[:fields_hash]) + + dedup_key = generate_dedup_key(attrs[:entity], attrs[:type], attrs[:project], attrs[:branch], attrs[:language], attrs[:time]) + if existing_heartbeats.include?(dedup_key) next end @@ -221,7 +227,7 @@ def wakatime_json_exists? File.exist?(output_path) end - def heartbeat_exists?(fields_hash) - Heartbeat.exists?(fields_hash: fields_hash) + def generate_dedup_key(entity, type, project, branch, language, time) + "#{entity}-#{type}-#{project}-#{branch}-#{language}-#{time}" end end diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index e1535596..5c229c8e 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -305,10 +305,13 @@This will migrate your heartbeats from waka.hackclub.com to this platform.
+This will migrate your heartbeats from other services to this platform.