Rewrite sailors log to work with user_id index (#156)

This commit is contained in:
Max Wofford
2025-04-11 14:28:21 -04:00
committed by GitHub
parent dd921dcbac
commit cb5a170c98
9 changed files with 125 additions and 47 deletions

View File

@@ -11,7 +11,7 @@ class UsersController < ApplicationController
@enabled_sailors_logs = SailorsLogNotificationPreference.where(
slack_uid: @user.slack_uid,
enabled: true,
).where.not(slack_channel_id: "C0835AZP9GB")
).where.not(slack_channel_id: SailorsLog::DEFAULT_CHANNELS)
@heartbeats_migration_jobs = @user.data_migration_jobs
end

View File

@@ -0,0 +1,32 @@
class OneTime::LogToSailorsLogChannelJob < ApplicationJob
queue_as :default
def perform
# every user should have a sailors_log &
# every sailors_log should atleast have a notification_preference for the debug sailors_log channel
User.where.missing(:sailors_log)
.where.not(slack_uid: nil)
.pluck(:slack_uid).each do |slack_uid|
puts "creating sailors_log for #{slack_uid}"
SailorsLog.create!(slack_uid: slack_uid)
end
# Find SailorsLogs that don't have a notification preference for the debug channel
debug_channel_id = "C0835AZP9GB"
# Get all SailorsLogs
SailorsLog.find_each do |sailors_log|
# Check if this SailorsLog already has a notification preference for the debug channel
has_preference = sailors_log.notification_preferences.exists?(slack_channel_id: debug_channel_id)
# If not, create one
unless has_preference
puts "Creating notification preference for #{sailors_log.slack_uid}"
sailors_log.notification_preferences.create!(
slack_channel_id: debug_channel_id,
enabled: true
)
end
end
end
end

View File

@@ -0,0 +1,12 @@
class OneTime::ResetSailorsLogJob < ApplicationJob
queue_as :default
def perform
SailorsLog.find_each do |sl|
next if sl.user.blank?
sl.projects_summary = nil
sl.send(:initialize_projects_summary)
sl.save!
end
end
end

View File

@@ -23,7 +23,10 @@ class SailorsLogNotifyJob < ApplicationJob
hours = project_duration / 3600
message = ":boat: `@#{SlackUsername.find_by_uid(slack_uid)}` just coded 1 more hour on *#{project_name}* (total: #{hours}hrs). _#{kudos_message}_"
username = SlackUsername.find_by_uid(slack_uid)
handle = username.blank? ? "<@#{slack_uid}>" : "@#{username}"
message = ":boat: `#{handle}` just coded 1 more hour on *#{project_name}* (total: #{hours}hrs). _#{kudos_message}_"
response = HTTP.auth("Bearer #{ENV['SAILORS_LOG_SLACK_BOT_OAUTH_TOKEN']}")
.post("https://slack.com/api/chat.postMessage",

View File

@@ -3,56 +3,63 @@ class SailorsLogPollForChangesJob < ApplicationJob
def perform
puts "performing SailorsLogPollForChangesJob"
# get all users who've coded in the last minute
users_who_coded = Heartbeat.where("created_at > ?", 1.minutes.ago)
.where(time: 1.minutes.ago..)
users_who_coded = Heartbeat.with_valid_timestamps
.where(time: 10.minutes.ago..)
.distinct.pluck(:user_id)
puts "users_who_coded: #{users_who_coded}"
slack_uids = User.where(id: users_who_coded)
.where.not(slack_uid: nil)
.distinct.pluck(:slack_uid)
# Get all of those with enabled preferences
enabled_users = SailorsLogNotificationPreference.where(enabled: true, slack_uid: slack_uids).distinct.pluck(:slack_uid)
slack_uids = User.where(id: users_who_coded).pluck(:slack_uid)
puts "slack_uids: #{slack_uids}"
puts "enabled_users: #{enabled_users}"
new_notifs = SailorsLog.includes(:user, :notification_preferences)
.where(notification_preferences: { enabled: true })
.where(slack_uid: slack_uids)
.map { |sl| update_sailors_log(sl) }.flatten
logs = SailorsLog.includes(:user).where(slack_uid: enabled_users)
notifs_to_send = SailorsLogSlackNotification.insert_all(new_notifs)
notif_ids = notifs_to_send.result.to_a.map { |r| r["id"] }
user_ids = logs.map(&:user).pluck(:id)
SailorsLogSlackNotification.where(id: notif_ids).map(&:notify_user_later!)
end
puts "logs: #{logs}"
private
logs.each do |log|
# get all projects for the user with duration
new_project_times = Heartbeat.where(user_id: user_ids)
.group(:project)
.duration_seconds
new_project_times.each do |project, new_project_duration|
next if project.blank?
if new_project_duration > (log.projects_summary[project] || 0) + 1.hour
log.notification_preferences.each do |preference|
log.notifications << SailorsLogSlackNotification.new(
slack_uid: log.slack_uid,
slack_channel_id: preference.slack_channel_id,
project_name: project,
project_duration: new_project_duration
)
end
log.projects_summary[project] = new_project_duration
end
end
log.save! if log.changed?
# if multiple notifications for the same project, only the most recent one should be sent
log.notifications.group_by(&:project_name).each do |project_name, notifications|
if notifications.size > 1
# Keep the most recent notification, destroy the older ones
notifications.sort_by(&:created_at)[0..-2].each(&:destroy)
end
def update_sailors_log(sailors_log)
project_updates = []
project_durations = Heartbeat.where(user_id: sailors_log.user.id)
.group(:project).duration_seconds
project_durations.each do |k, v|
old_duration = sailors_log.projects_summary[k] || 0
new_duration = v
puts "#{k}| old_duration: #{old_duration}, new_duration: #{new_duration}"
if old_duration / 3600 < new_duration / 3600
puts "updating #{k} to #{new_duration}"
sailors_log.projects_summary[k] = new_duration
project_updates << { project: k, duration: new_duration }
end
end
notifications_to_create = []
if sailors_log.changed?
sailors_log.notification_preferences.each do |np|
project_updates.map do |pu|
puts "np: #{np.inspect}, pu: #{pu.inspect}"
notifications_to_create << {
slack_uid: sailors_log.user.slack_uid,
slack_channel_id: np.slack_channel_id,
project_name: pu[:project],
project_duration: pu[:duration]
}
end
end
sailors_log.save!
end
notifications_to_create
end
end
# optimizations?
# - index heartbeats on user_id + project so we can call duration_seconds grouping by both
# - investigate lookup by slack_uid, maybe index or computed field?

View File

@@ -1,8 +1,8 @@
class SailorsLog < ApplicationRecord
validates :slack_uid, presence: true, uniqueness: true
validates :projects_summary, presence: true
before_validation :initialize_projects_summary
after_create :ensure_notification_preference_for_debug_channel
belongs_to :user, foreign_key: :slack_uid, primary_key: :slack_uid
@@ -16,10 +16,21 @@ class SailorsLog < ApplicationRecord
foreign_key: :slack_uid,
primary_key: :slack_uid
DEFAULT_CHANNELS = %w[C0835AZP9GB]
private
def initialize_projects_summary
return unless projects_summary.blank?
self.projects_summary = Heartbeat.where(user_id: slack_uid).group(:project).duration_seconds
return if projects_summary.present?
self.projects_summary = Heartbeat.where(user_id: user.id)
.group(:project).duration_seconds
self.projects_summary ||= {}
end
def ensure_notification_preference_for_debug_channel
return if notification_preferences.any? { |np| SailorsLog::DEFAULT_CHANNELS.include?(np.slack_channel_id) && np.enabled }
SailorsLog::DEFAULT_CHANNELS.each do |channel_id|
notification_preferences.create!(slack_channel_id: channel_id, enabled: true)
end
end
end

View File

@@ -4,4 +4,8 @@ class SailorsLogSlackNotification < ApplicationRecord
SailorsLogNotifyJob.perform_now(self.id)
end
def notify_user_later!
SailorsLogNotifyJob.perform_later(self.id)
end
end

View File

@@ -23,6 +23,11 @@ class User < ApplicationRecord
has_many :api_keys
has_one :sailors_log,
foreign_key: :slack_uid,
primary_key: :slack_uid,
class_name: "SailorsLog"
delegate :streak_days, :streak_days_formatted, to: :heartbeats
enum :hackatime_extension_text_type, {

View File

@@ -1,6 +1,8 @@
class SlackUsername
def self.find_by_uid(uid)
cached_name = Rails.cache.fetch("slack_username_#{uid}", expires_in: 1.day) do
key = "slack_username_#{uid}"
cached_name = Rails.cache.fetch(key, expires_in: 1.day) do
response = HTTP.headers(Authorization: "Bearer #{ENV.fetch("SAILORS_LOG_SLACK_BOT_OAUTH_TOKEN")}").get("https://slack.com/api/users.info?user=#{uid}")
data = JSON.parse(response.body)
@@ -10,6 +12,8 @@ class SlackUsername
name
end
Rails.cache.delete(key) unless cached_name.present?
cached_name
end
end