diff --git a/app/jobs/one_time/recalc_heartbeat_field_hash_job.rb b/app/jobs/one_time/recalc_heartbeat_field_hash_job.rb new file mode 100644 index 0000000..c91a3c4 --- /dev/null +++ b/app/jobs/one_time/recalc_heartbeat_field_hash_job.rb @@ -0,0 +1,15 @@ +class OneTime::RecalcHeartbeatFieldHashJob < ApplicationJob + queue_as :default + + def perform + Heartbeat.find_each(batch_size: 100) do |heartbeat| + begin + heartbeat.send(:set_fields_hash!) + heartbeat.save! + rescue ActiveRecord::RecordNotUnique + # If we have a duplicate fields_hash, soft delete this record + heartbeat.soft_delete + end + end + end +end diff --git a/app/models/heartbeat.rb b/app/models/heartbeat.rb index 262d055..960aebb 100644 --- a/app/models/heartbeat.rb +++ b/app/models/heartbeat.rb @@ -6,8 +6,13 @@ class Heartbeat < ApplicationRecord time_range_filterable_field :time + # Default scope to exclude deleted records + default_scope { where(deleted_at: nil) } + scope :today, -> { where(time: Time.current.beginning_of_day..Time.current.end_of_day) } scope :recent, -> { where("created_at > ?", 24.hours.ago) } + scope :with_deleted, -> { unscope(where: :deleted_at) } + scope :only_deleted, -> { with_deleted.where.not(deleted_at: nil) } enum :source_type, { direct_entry: 0, @@ -97,11 +102,25 @@ class Heartbeat < ApplicationRecord end def self.generate_fields_hash(attributes) - Digest::MD5.hexdigest(attributes.except(*self.unindexed_attributes).to_json) + string_attributes = attributes.transform_keys(&:to_s) + indexed_attributes = string_attributes.slice(*self.indexed_attributes) + Digest::MD5.hexdigest(indexed_attributes.to_json) end - def self.unindexed_attributes - %w[id created_at updated_at source_type fields_hash ysws_program] + def self.indexed_attributes + %w[user_id branch category dependencies editor entity language machine operating_system project type user_agent line_additions line_deletions lineno lines cursorpos project_root_count time is_write] + end + + def set_raw_data + self.raw_data = self.attributes.slice(*self.class.indexed_attributes) + end + + def soft_delete + update_column(:deleted_at, Time.current) + end + + def restore + update_column(:deleted_at, nil) end private diff --git a/db/migrate/20250507182617_add_deleted_at_to_heartbeats.rb b/db/migrate/20250507182617_add_deleted_at_to_heartbeats.rb new file mode 100644 index 0000000..c3d1815 --- /dev/null +++ b/db/migrate/20250507182617_add_deleted_at_to_heartbeats.rb @@ -0,0 +1,5 @@ +class AddDeletedAtToHeartbeats < ActiveRecord::Migration[8.0] + def change + add_column :heartbeats, :deleted_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 52a2074..7ee6544 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_05_07_174855) do +ActiveRecord::Schema[8.0].define(version: 2025_05_07_182617) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -207,6 +207,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_07_174855) do t.integer "source_type", null: false t.inet "ip_address" t.integer "ysws_program", default: 0, null: false + t.datetime "deleted_at" t.index ["category", "time"], name: "index_heartbeats_on_category_and_time" t.index ["fields_hash"], name: "index_heartbeats_on_fields_hash", unique: true t.index ["user_id"], name: "index_heartbeats_on_user_id"