mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
Initial new activity log (#562)
This commit is contained in:
2
Gemfile
2
Gemfile
@@ -72,6 +72,8 @@ gem "dotenv-rails"
|
||||
# Added from the code block
|
||||
gem "http"
|
||||
|
||||
gem "public_activity"
|
||||
|
||||
# Bulk import
|
||||
gem "activerecord-import"
|
||||
|
||||
|
||||
@@ -345,6 +345,11 @@ GEM
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_activity (3.0.1)
|
||||
actionpack (>= 6.1.0)
|
||||
activerecord (>= 6.1)
|
||||
i18n (>= 0.5.0)
|
||||
railties (>= 6.1.0)
|
||||
public_suffix (6.0.2)
|
||||
puma (7.0.4)
|
||||
nio4r (~> 2.0)
|
||||
@@ -592,6 +597,7 @@ DEPENDENCIES
|
||||
paper_trail
|
||||
pg
|
||||
propshaft
|
||||
public_activity
|
||||
puma (>= 5.0)
|
||||
query_count
|
||||
rack-attack
|
||||
|
||||
@@ -258,6 +258,7 @@ class Api::Hackatime::V1::HackatimeController < ApplicationController
|
||||
new_heartbeat.save! if new_heartbeat.changed?
|
||||
end
|
||||
queue_project_mapping(heartbeat[:project])
|
||||
queue_heartbeat_public_activity(@user.id, heartbeat[:project]) if new_heartbeat.persisted?
|
||||
results << [ new_heartbeat.attributes, 201 ]
|
||||
rescue => e
|
||||
Rails.logger.error("Error creating heartbeat: #{e.class.name} #{e.message}")
|
||||
@@ -276,6 +277,16 @@ class Api::Hackatime::V1::HackatimeController < ApplicationController
|
||||
Rails.logger.error("Error queuing project mapping: #{e.class.name} #{e.message}")
|
||||
end
|
||||
|
||||
def queue_heartbeat_public_activity(user_id, project_name)
|
||||
# only queue the job once per minute
|
||||
Rails.cache.fetch("heartbeat_public_activity_#{user_id}_#{project_name}", expires_in: 30.seconds) do
|
||||
CreateHeartbeatActivityJob.perform_later(user_id, project_name)
|
||||
end
|
||||
rescue => e
|
||||
# never raise an error here because it will break the heartbeat flow
|
||||
Rails.logger.error("Error queuing heartbeat public activity: #{e.class.name} #{e.message}")
|
||||
end
|
||||
|
||||
def set_user
|
||||
api_header = request.headers["Authorization"]
|
||||
raw_token = api_header&.split(" ")&.last
|
||||
|
||||
@@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base
|
||||
before_action :initialize_cache_counters
|
||||
before_action :try_rack_mini_profiler_enable
|
||||
before_action :track_request
|
||||
before_action :set_public_activity
|
||||
after_action :track_action
|
||||
|
||||
around_action :switch_time_zone, if: :current_user
|
||||
@@ -19,6 +20,11 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def set_public_activity
|
||||
return unless Flipper.enabled?(:public_activity_log, current_user)
|
||||
@activities = PublicActivity::Activity.all
|
||||
end
|
||||
|
||||
def honeybadger_context
|
||||
Honeybadger.context(
|
||||
user_id: current_user.id,
|
||||
|
||||
73
app/jobs/create_heartbeat_activity_job.rb
Normal file
73
app/jobs/create_heartbeat_activity_job.rb
Normal file
@@ -0,0 +1,73 @@
|
||||
class CreateHeartbeatActivityJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user_id, project_name)
|
||||
@user_id = user_id
|
||||
|
||||
# Look for future coding activity only (not past events that are already showing)
|
||||
recent_activity = PublicActivity::Activity.with_future
|
||||
.where(owner_id: user_id, trackable_type: "Heartbeat", key: "coding_session")
|
||||
.where("created_at > ?", Time.current)
|
||||
.first
|
||||
|
||||
if recent_activity
|
||||
# Keep pushing 5 minutes into future and update project/timing
|
||||
last_updated = Time.current.to_i
|
||||
started_at = recent_activity.parameters["started_at"]
|
||||
duration_seconds = last_updated - started_at
|
||||
|
||||
recent_activity.update!(
|
||||
created_at: Time.current + 5.minutes,
|
||||
parameters: recent_activity.parameters.merge(
|
||||
project: project_name,
|
||||
last_updated: last_updated,
|
||||
duration_seconds: duration_seconds
|
||||
)
|
||||
)
|
||||
else
|
||||
return unless user
|
||||
|
||||
# Create immediate "started working" activity - person just resumed coding
|
||||
PublicActivity::Activity.create!(
|
||||
trackable_type: "Heartbeat",
|
||||
trackable_id: nil,
|
||||
owner: user,
|
||||
key: "started_working",
|
||||
parameters: { project: project_name }
|
||||
)
|
||||
|
||||
# Create new session 5 minutes in future
|
||||
started_at = Time.current.to_i
|
||||
activity = PublicActivity::Activity.create!(
|
||||
trackable_type: "Heartbeat",
|
||||
trackable_id: nil, # Not tied to specific heartbeat
|
||||
owner: user,
|
||||
key: "coding_session",
|
||||
parameters: {
|
||||
project: project_name,
|
||||
started_at: started_at,
|
||||
last_updated: started_at,
|
||||
duration_seconds: 0
|
||||
},
|
||||
created_at: Time.current + 5.minutes
|
||||
)
|
||||
|
||||
# Check if this is the user's first heartbeat ever
|
||||
if user.heartbeats.count == 1
|
||||
PublicActivity::Activity.create!(
|
||||
trackable_type: "Heartbeat",
|
||||
trackable_id: nil,
|
||||
owner: user,
|
||||
key: "first_heartbeat",
|
||||
parameters: { project: project_name }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user
|
||||
@user ||= User.find_by(id: @user_id)
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ class Heartbeat < ApplicationRecord
|
||||
|
||||
include Heartbeatable
|
||||
include TimeRangeFilterable
|
||||
include PublicActivity::Common
|
||||
|
||||
time_range_filterable_field :time
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
class PhysicalMail < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
include PublicActivity::Model
|
||||
tracked only: [ :create, :update ], owner: :user, params: proc { |controller, model|
|
||||
{
|
||||
mission_type: model.mission_type,
|
||||
humanized_mission_type: model.humanized_mission_type
|
||||
}
|
||||
}
|
||||
|
||||
after_create :create_streak_activity, if: :first_time_7_streak?
|
||||
after_update :create_sent_activity, if: :became_sent?
|
||||
|
||||
scope :going_out, -> { where(status: :pending).or(where(status: :sent)) }
|
||||
|
||||
enum :status, {
|
||||
@@ -92,4 +103,22 @@ class PhysicalMail < ApplicationRecord
|
||||
def user_address
|
||||
user.mailing_address
|
||||
end
|
||||
|
||||
def became_sent?
|
||||
saved_change_to_status? && status == "sent" && status_before_last_save == "pending"
|
||||
end
|
||||
|
||||
def create_streak_activity
|
||||
create_activity :first_streak_achieved, owner: user, params: {
|
||||
mission_type: mission_type,
|
||||
humanized_mission_type: humanized_mission_type
|
||||
}
|
||||
end
|
||||
|
||||
def create_sent_activity
|
||||
create_activity :mail_sent, owner: user, params: {
|
||||
mission_type: mission_type,
|
||||
humanized_mission_type: humanized_mission_type
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
class User < ApplicationRecord
|
||||
include TimezoneRegions
|
||||
include PublicActivity::Model
|
||||
|
||||
has_paper_trail
|
||||
|
||||
after_create :create_signup_activity
|
||||
encrypts :slack_access_token, :github_access_token
|
||||
|
||||
validates :slack_uid, uniqueness: true, allow_nil: true
|
||||
@@ -502,4 +505,8 @@ class User < ApplicationRecord
|
||||
def invalidate_activity_graph_cache
|
||||
Rails.cache.delete("user_#{id}_daily_durations")
|
||||
end
|
||||
|
||||
def create_signup_activity
|
||||
create_activity :first_signup, owner: self
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<%= render "shared/user_mention", user: activity.owner %>
|
||||
just finished coding on <%= activity.parameters['project'] %>
|
||||
for <%= short_time_simple(activity.parameters['duration_seconds']) %>
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= render "shared/user_mention", user: activity.owner %>
|
||||
just started tracking their coding time on <%= activity.parameters['project'] %>!
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= render "shared/user_mention", user: activity.owner %>
|
||||
just started working on <%= activity.parameters['project'] %>
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= render "shared/user_mention", user: activity.owner %>
|
||||
just hit their first 7 day coding streak!
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= render "shared/user_mention", user: activity.owner %>
|
||||
was just sent a letter for '<%= activity.parameters[:humanized_mission_type] %>'
|
||||
2
app/views/public_activity/user/_first_signup.html.erb
Normal file
2
app/views/public_activity/user/_first_signup.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<%= render "shared/user_mention", user: activity.owner %>
|
||||
just signed in for the first time
|
||||
@@ -151,6 +151,8 @@
|
||||
Feature Flags
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render_activities(@activities) if defined?(@activities) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,4 +12,9 @@ Rails.configuration.to_prepare do
|
||||
end
|
||||
|
||||
Doorkeeper::ApplicationsController.layout "application" # show oauth2 admin in normal hackatime ui
|
||||
|
||||
PublicActivity::Activity.class_eval do
|
||||
default_scope { where("created_at <= ?", Time.current) }
|
||||
scope :with_future, -> { unscope(where: :created_at) }
|
||||
end
|
||||
end
|
||||
|
||||
25
db/migrate/20251003215127_create_activities.rb
Normal file
25
db/migrate/20251003215127_create_activities.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Migration responsible for creating a table with activities
|
||||
class CreateActivities < ActiveRecord::Migration[6.1]
|
||||
def self.up
|
||||
create_table :activities do |t|
|
||||
t.belongs_to :trackable, polymorphic: true
|
||||
t.belongs_to :owner, polymorphic: true
|
||||
t.string :key
|
||||
t.text :parameters
|
||||
t.belongs_to :recipient, polymorphic: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :activities, %i[trackable_id trackable_type]
|
||||
add_index :activities, %i[owner_id owner_type]
|
||||
add_index :activities, %i[recipient_id recipient_type]
|
||||
end
|
||||
|
||||
# Drop table
|
||||
def self.down
|
||||
drop_table :activities
|
||||
end
|
||||
end
|
||||
21
db/schema.rb
generated
21
db/schema.rb
generated
@@ -10,13 +10,32 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_10_03_161836) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_10_03_215127) do
|
||||
create_schema "pganalyze"
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
enable_extension "pg_stat_statements"
|
||||
|
||||
create_table "activities", force: :cascade do |t|
|
||||
t.string "trackable_type"
|
||||
t.bigint "trackable_id"
|
||||
t.string "owner_type"
|
||||
t.bigint "owner_id"
|
||||
t.string "key"
|
||||
t.text "parameters"
|
||||
t.string "recipient_type"
|
||||
t.bigint "recipient_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["owner_id", "owner_type"], name: "index_activities_on_owner_id_and_owner_type"
|
||||
t.index ["owner_type", "owner_id"], name: "index_activities_on_owner"
|
||||
t.index ["recipient_id", "recipient_type"], name: "index_activities_on_recipient_id_and_recipient_type"
|
||||
t.index ["recipient_type", "recipient_id"], name: "index_activities_on_recipient"
|
||||
t.index ["trackable_id", "trackable_type"], name: "index_activities_on_trackable_id_and_trackable_type"
|
||||
t.index ["trackable_type", "trackable_id"], name: "index_activities_on_trackable"
|
||||
end
|
||||
|
||||
create_table "admin_api_keys", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.text "name", null: false
|
||||
|
||||
Reference in New Issue
Block a user