mirror of
https://github.com/discourse/discourse-rewind.git
synced 2025-12-11 02:05:32 +00:00
FEATURE: Add seven new metrics to Discourse Rewind reports (#26)
## Summary Adds seven new metrics to the Discourse Rewind plugin, significantly expanding the types of insights users can get about their annual activity. These new reports cover temporal patterns, community engagement, plugin integrations, and content analysis. ## New Reports ### 1. Time of Day Activity (`time-of-day-activity`) - Analyzes user activity by hour in their timezone - Determines personality type: "early bird" (6-9am), "night owl" (10pm-2am), or "balanced" - Aggregates posts, chat messages, and page views ### 2. New User Interactions (`new-user-interactions`) - Tracks veteran mentorship and community building behavior - Measures likes, replies, and mentions to users who joined this year - Shows unique new users interacted with and total engagement ### 3. Chat Usage (`chat-usage`) - Total messages and average message length - Top 5 favorite channels with message counts - DM statistics (message count, unique conversations) - Reactions received on messages ### 4. AI Usage (`ai-usage`) - Integrates with `discourse-ai` plugin - Total requests, tokens consumed (request/response breakdown) - Top 5 most used features and AI models - Success rate calculation ### 5. Favorite GIFs (`favorite-gifs`) - Extracts GIFs from posts and chat messages - Ranks by engagement score (usage × 10 + likes + reactions) - Supports Giphy, Tenor, and direct GIF URLs - Shows top 5 GIFs with usage statistics ### 6. Assignments (`assignments`) - Integrates with `discourse-assign` plugin - Tracks assignments received and given - Shows completion rate and pending assignments ### 7. Invites (`invites`) - Total invites sent and redemption rate - Impact metrics: invitee posts, topics, and likes created - Most active invitee identification - Average trust level of invitees ## Technical Details - All reports extend `BaseReport` class - Include proper enablement checks for plugin dependencies - Use efficient database queries with proper joins and aggregations - Return `nil` when no relevant data exists Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
This commit is contained in:
parent
ea11f40e61
commit
93b37e4069
72
app/services/discourse_rewind/action/ai_usage.rb
Normal file
72
app/services/discourse_rewind/action/ai_usage.rb
Normal file
@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# AI usage statistics from discourse-ai plugin
|
||||
# Shows total usage, favorite features, token consumption, etc.
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class AiUsage < BaseReport
|
||||
def call
|
||||
return if !enabled?
|
||||
|
||||
base_query = AiApiAuditLog.where(user_id: user.id).where(created_at: date)
|
||||
|
||||
# Get aggregated stats in a single query
|
||||
stats =
|
||||
base_query.select(
|
||||
"COUNT(*) as total_requests",
|
||||
"COALESCE(SUM(request_tokens), 0) as total_request_tokens",
|
||||
"COALESCE(SUM(response_tokens), 0) as total_response_tokens",
|
||||
"COUNT(CASE WHEN response_tokens > 0 THEN 1 END) as successful_requests",
|
||||
).take
|
||||
|
||||
return if stats.total_requests == 0
|
||||
|
||||
total_tokens = stats.total_request_tokens + stats.total_response_tokens
|
||||
success_rate =
|
||||
(
|
||||
if stats.total_requests > 0
|
||||
(stats.successful_requests.to_f / stats.total_requests * 100).round(1)
|
||||
else
|
||||
0
|
||||
end
|
||||
)
|
||||
|
||||
# Most used features (top 5)
|
||||
feature_usage =
|
||||
base_query
|
||||
.group(:feature_name)
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(5)
|
||||
.pluck(:feature_name, Arel.sql("COUNT(*)"))
|
||||
.to_h
|
||||
|
||||
# Most used AI model (top 5)
|
||||
model_usage =
|
||||
base_query
|
||||
.where.not(language_model: nil)
|
||||
.group(:language_model)
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(5)
|
||||
.pluck(:language_model, Arel.sql("COUNT(*)"))
|
||||
.to_h
|
||||
|
||||
{
|
||||
data: {
|
||||
total_requests: stats.total_requests,
|
||||
total_tokens: total_tokens,
|
||||
request_tokens: stats.total_request_tokens,
|
||||
response_tokens: stats.total_response_tokens,
|
||||
feature_usage: feature_usage,
|
||||
model_usage: model_usage,
|
||||
success_rate: success_rate,
|
||||
},
|
||||
identifier: "ai-usage",
|
||||
}
|
||||
end
|
||||
|
||||
def enabled?
|
||||
defined?(AiApiAuditLog) && SiteSetting.discourse_ai_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
62
app/services/discourse_rewind/action/assignments.rb
Normal file
62
app/services/discourse_rewind/action/assignments.rb
Normal file
@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Assignment statistics using discourse-assign plugin data
|
||||
# Shows how many assignments, completed, pending, etc.
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class Assignments < BaseReport
|
||||
def call
|
||||
return if !enabled?
|
||||
|
||||
# Assignments made to the user
|
||||
assignments_scope =
|
||||
Assignment
|
||||
.where(assigned_to_id: user.id, assigned_to_type: "User")
|
||||
.where(created_at: date)
|
||||
|
||||
total_assigned = assignments_scope.count
|
||||
|
||||
# Completed assignments (topics that were assigned and then closed or unassigned)
|
||||
completed_count =
|
||||
assignments_scope
|
||||
.joins(:topic)
|
||||
.where(
|
||||
"topics.closed = true OR assignments.active = false OR assignments.updated_at > assignments.created_at",
|
||||
)
|
||||
.distinct
|
||||
.count
|
||||
|
||||
# Currently pending (still open and assigned)
|
||||
pending_count =
|
||||
Assignment
|
||||
.where(assigned_to_id: user.id, assigned_to_type: "User", active: true)
|
||||
.joins(:topic)
|
||||
.where(topics: { closed: false })
|
||||
.count
|
||||
|
||||
# Assignments made by the user to others
|
||||
assigned_by_user =
|
||||
Assignment
|
||||
.where(assigned_by_user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.count
|
||||
|
||||
{
|
||||
data: {
|
||||
total_assigned: total_assigned,
|
||||
completed: completed_count,
|
||||
pending: pending_count,
|
||||
assigned_by_user: assigned_by_user,
|
||||
completion_rate:
|
||||
total_assigned > 0 ? (completed_count.to_f / total_assigned * 100).round(1) : 0,
|
||||
},
|
||||
identifier: "assignments",
|
||||
}
|
||||
end
|
||||
|
||||
def enabled?
|
||||
defined?(Assignment) && SiteSetting.assign_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
82
app/services/discourse_rewind/action/chat_usage.rb
Normal file
82
app/services/discourse_rewind/action/chat_usage.rb
Normal file
@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Chat usage statistics
|
||||
# Shows message counts, favorite channels, DM activity, etc.
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class ChatUsage < BaseReport
|
||||
def call
|
||||
return if !enabled?
|
||||
|
||||
messages =
|
||||
Chat::Message.where(user_id: user.id).where(created_at: date).where(deleted_at: nil)
|
||||
|
||||
total_messages = messages.count
|
||||
return if total_messages == 0
|
||||
|
||||
# Get favorite channels (public channels)
|
||||
channel_usage =
|
||||
messages
|
||||
.joins(:chat_channel)
|
||||
.where(chat_channels: { type: "CategoryChannel" })
|
||||
.group("chat_channels.id", "chat_channels.name")
|
||||
.count
|
||||
.sort_by { |_, count| -count }
|
||||
.first(5)
|
||||
.map do |(id, name), count|
|
||||
{ channel_id: id, channel_name: name, message_count: count }
|
||||
end
|
||||
|
||||
# DM statistics
|
||||
dm_message_count =
|
||||
messages.joins(:chat_channel).where(chat_channels: { type: "DirectMessageChannel" }).count
|
||||
|
||||
# Unique DM conversations
|
||||
unique_dm_channels =
|
||||
messages
|
||||
.joins(:chat_channel)
|
||||
.where(chat_channels: { type: "DirectMessageChannel" })
|
||||
.distinct
|
||||
.count(:chat_channel_id)
|
||||
|
||||
# Messages with reactions received
|
||||
messages_with_reactions =
|
||||
Chat::MessageReaction
|
||||
.joins(:chat_message)
|
||||
.where(chat_messages: { user_id: user.id })
|
||||
.where(chat_messages: { created_at: date })
|
||||
.distinct
|
||||
.count(:chat_message_id)
|
||||
|
||||
# Total reactions received
|
||||
total_reactions_received =
|
||||
Chat::MessageReaction
|
||||
.joins(:chat_message)
|
||||
.where(chat_messages: { user_id: user.id })
|
||||
.where(chat_messages: { created_at: date })
|
||||
.count
|
||||
|
||||
# Average message length
|
||||
avg_message_length =
|
||||
messages.where("LENGTH(message) > 0").average("LENGTH(message)")&.to_f&.round(1) || 0
|
||||
|
||||
{
|
||||
data: {
|
||||
total_messages: total_messages,
|
||||
favorite_channels: channel_usage,
|
||||
dm_message_count: dm_message_count,
|
||||
unique_dm_channels: unique_dm_channels,
|
||||
messages_with_reactions: messages_with_reactions,
|
||||
total_reactions_received: total_reactions_received,
|
||||
avg_message_length: avg_message_length,
|
||||
},
|
||||
identifier: "chat-usage",
|
||||
}
|
||||
end
|
||||
|
||||
def enabled?
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
122
app/services/discourse_rewind/action/favorite_gifs.rb
Normal file
122
app/services/discourse_rewind/action/favorite_gifs.rb
Normal file
@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Find the user's most used GIFs in posts and chat
|
||||
# Ranks by usage count and engagement (likes/reactions)
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class FavoriteGifs < BaseReport
|
||||
GIF_URL_PATTERN =
|
||||
%r{
|
||||
https?://[^\s]+\.(?:gif|gifv)
|
||||
|
|
||||
https?://(?!(?:developers|support|blog)\.) (?:[^/\s]+\.)?giphy\.com/(?!dashboard\b)[^\s]+
|
||||
|
|
||||
https?://(?!(?:support)\.) (?:[^/\s]+\.)?tenor\.com/(?!gifapi\b)[^\s]+
|
||||
}ix
|
||||
MAX_RESULTS = 5
|
||||
|
||||
def call
|
||||
gif_data = {}
|
||||
|
||||
# Get GIFs from posts
|
||||
post_gifs = extract_gifs_from_posts
|
||||
post_gifs.each do |url, data|
|
||||
gif_data[url] ||= { url: url, usage_count: 0, likes: 0, reactions: 0 }
|
||||
gif_data[url][:usage_count] += data[:count]
|
||||
gif_data[url][:likes] += data[:likes]
|
||||
end
|
||||
|
||||
# Get GIFs from chat messages if chat is enabled
|
||||
if SiteSetting.chat_enabled
|
||||
chat_gifs = extract_gifs_from_chat
|
||||
chat_gifs.each do |url, data|
|
||||
gif_data[url] ||= { url: url, usage_count: 0, likes: 0, reactions: 0 }
|
||||
gif_data[url][:usage_count] += data[:count]
|
||||
gif_data[url][:reactions] += data[:reactions]
|
||||
end
|
||||
end
|
||||
|
||||
return if gif_data.empty?
|
||||
|
||||
# Sort by engagement score (usage * 10 + likes + reactions)
|
||||
sorted_gifs =
|
||||
gif_data
|
||||
.values
|
||||
.sort_by { |gif| -(gif[:usage_count] * 10 + gif[:likes] + gif[:reactions]) }
|
||||
.first(MAX_RESULTS)
|
||||
|
||||
{
|
||||
data: {
|
||||
favorite_gifs: sorted_gifs,
|
||||
total_gif_usage: gif_data.values.sum { |g| g[:usage_count] },
|
||||
},
|
||||
identifier: "favorite-gifs",
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_gifs_from_posts
|
||||
gif_usage = {}
|
||||
|
||||
posts =
|
||||
Post
|
||||
.where(user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.where("raw ~* ?", gif_sql_pattern)
|
||||
.select(:id, :raw, :like_count)
|
||||
|
||||
posts.each do |post|
|
||||
gif_urls = post.raw.scan(GIF_URL_PATTERN).uniq.select { |url| content_gif_url?(url) }
|
||||
gif_urls.each do |url|
|
||||
gif_usage[url] ||= { count: 0, likes: 0 }
|
||||
gif_usage[url][:count] += 1
|
||||
gif_usage[url][:likes] += post.like_count || 0
|
||||
end
|
||||
end
|
||||
|
||||
gif_usage
|
||||
end
|
||||
|
||||
def extract_gifs_from_chat
|
||||
gif_usage = {}
|
||||
|
||||
messages =
|
||||
Chat::Message
|
||||
.where(user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.where("message ~* ?", gif_sql_pattern)
|
||||
.select(:id, :message)
|
||||
|
||||
messages.each do |message|
|
||||
gif_urls =
|
||||
message.message.scan(GIF_URL_PATTERN).uniq.select { |url| content_gif_url?(url) }
|
||||
gif_urls.each do |url|
|
||||
gif_usage[url] ||= { count: 0, reactions: 0 }
|
||||
gif_usage[url][:count] += 1
|
||||
|
||||
# Count reactions on this message
|
||||
reaction_count = Chat::MessageReaction.where(chat_message_id: message.id).count
|
||||
gif_usage[url][:reactions] += reaction_count
|
||||
end
|
||||
end
|
||||
|
||||
gif_usage
|
||||
end
|
||||
|
||||
def gif_sql_pattern
|
||||
@gif_sql_pattern ||= GIF_URL_PATTERN.source.gsub(/\s+/, "")
|
||||
end
|
||||
|
||||
def content_gif_url?(url)
|
||||
return true if url.match?(/\.(gif|gifv)(?:\?|$)/i)
|
||||
return true if url.match?(%r{giphy\.com/(?:gifs?|media|embed|stickers|clips)}i)
|
||||
return true if url.match?(%r{tenor\.com/(?:view|watch|embed|gif)}i)
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
85
app/services/discourse_rewind/action/invites.rb
Normal file
85
app/services/discourse_rewind/action/invites.rb
Normal file
@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Invite statistics
|
||||
# Shows how many users this user invited and the impact of those invitees
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class Invites < BaseReport
|
||||
def call
|
||||
# Get all invites created by this user in the date range
|
||||
invites = Invite.where(invited_by_id: user.id).where(created_at: date)
|
||||
|
||||
total_invites = invites.count
|
||||
return if total_invites == 0
|
||||
|
||||
# Redeemed invites (users who actually joined)
|
||||
redeemed_count = invites.where.not(redeemed_at: nil).count
|
||||
|
||||
# Get the users who were invited (via InvitedUser or redeemed invites)
|
||||
invited_user_ids = InvitedUser.where(invite: invites).pluck(:user_id).compact
|
||||
|
||||
invited_users = User.where(id: invited_user_ids)
|
||||
|
||||
# Calculate impact of invitees
|
||||
invitee_post_count =
|
||||
Post
|
||||
.where(user_id: invited_user_ids)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.count
|
||||
|
||||
invitee_topic_count =
|
||||
Topic
|
||||
.where(user_id: invited_user_ids)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.count
|
||||
|
||||
invitee_like_count =
|
||||
UserAction
|
||||
.where(user_id: invited_user_ids)
|
||||
.where(action_type: UserAction::LIKE)
|
||||
.where(created_at: date)
|
||||
.count
|
||||
|
||||
# Calculate average trust level of invitees
|
||||
avg_trust_level = invited_users.average(:trust_level)&.to_f&.round(1) || 0
|
||||
|
||||
# Most active invitee
|
||||
most_active_invitee = nil
|
||||
if invited_user_ids.any?
|
||||
most_active_id =
|
||||
Post
|
||||
.where(user_id: invited_user_ids)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.group(:user_id)
|
||||
.count
|
||||
.max_by { |_, count| count }
|
||||
&.first
|
||||
|
||||
if most_active_id
|
||||
most_active_user = User.find_by(id: most_active_id)
|
||||
most_active_invitee =
|
||||
BasicUserSerializer.new(most_active_user, root: false).as_json if most_active_user
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
data: {
|
||||
total_invites: total_invites,
|
||||
redeemed_count: redeemed_count,
|
||||
redemption_rate:
|
||||
total_invites > 0 ? (redeemed_count.to_f / total_invites * 100).round(1) : 0,
|
||||
invitee_post_count: invitee_post_count,
|
||||
invitee_topic_count: invitee_topic_count,
|
||||
invitee_like_count: invitee_like_count,
|
||||
avg_trust_level: avg_trust_level,
|
||||
most_active_invitee: most_active_invitee,
|
||||
},
|
||||
identifier: "invites",
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Tracks how much this user interacted with new users (created this year)
|
||||
# Shows veteran mentorship and community building behavior
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class NewUserInteractions < BaseReport
|
||||
def call
|
||||
year_start = Date.new(date.first.year, 1, 1)
|
||||
|
||||
# Find users who created accounts this year
|
||||
new_user_ids =
|
||||
User
|
||||
.real
|
||||
.where("created_at >= ? AND created_at <= ?", year_start, date.last)
|
||||
.where("id != ?", user.id)
|
||||
.pluck(:id)
|
||||
|
||||
return if new_user_ids.empty?
|
||||
|
||||
# Count likes given to new users
|
||||
likes_scope =
|
||||
UserAction.where(
|
||||
acting_user_id: user.id,
|
||||
user_id: new_user_ids,
|
||||
action_type: UserAction::WAS_LIKED,
|
||||
).where(created_at: date)
|
||||
likes_given = likes_scope.count
|
||||
liked_user_ids = likes_scope.distinct.pluck(:user_id)
|
||||
|
||||
# Count replies to new users' posts
|
||||
replies_scope =
|
||||
Post
|
||||
.joins(
|
||||
"INNER JOIN posts AS parent_posts ON posts.reply_to_post_number = parent_posts.post_number AND posts.topic_id = parent_posts.topic_id",
|
||||
)
|
||||
.where(posts: { user_id: user.id, deleted_at: nil, created_at: date })
|
||||
.where("parent_posts.user_id": new_user_ids)
|
||||
replies_to_new_users = replies_scope.count
|
||||
replied_user_ids = replies_scope.distinct.pluck("parent_posts.user_id")
|
||||
|
||||
# Count topics created by user that new users participated in
|
||||
topics_with_new_users =
|
||||
Topic
|
||||
.joins(:posts)
|
||||
.where(topics: { user_id: user.id, deleted_at: nil })
|
||||
.where(posts: { user_id: new_user_ids, deleted_at: nil })
|
||||
.where(topics: { created_at: date })
|
||||
.distinct
|
||||
.count
|
||||
|
||||
# Count direct messages/mentions to new users
|
||||
mentions_scope =
|
||||
Post
|
||||
.joins(
|
||||
"INNER JOIN user_actions ON user_actions.target_post_id = posts.id AND user_actions.action_type = #{UserAction::MENTION}",
|
||||
)
|
||||
.where(posts: { user_id: user.id, deleted_at: nil, created_at: date })
|
||||
.where(user_actions: { user_id: new_user_ids })
|
||||
mentions_to_new_users = mentions_scope.distinct.count
|
||||
mentioned_user_ids = mentions_scope.distinct.pluck("user_actions.user_id")
|
||||
|
||||
# Unique new users interacted with
|
||||
unique_new_users = (liked_user_ids + replied_user_ids + mentioned_user_ids).uniq.count
|
||||
|
||||
total_interactions = likes_given + replies_to_new_users + mentions_to_new_users
|
||||
|
||||
return if total_interactions == 0
|
||||
|
||||
{
|
||||
data: {
|
||||
total_interactions: total_interactions,
|
||||
likes_given: likes_given,
|
||||
replies_to_new_users: replies_to_new_users,
|
||||
mentions_to_new_users: mentions_to_new_users,
|
||||
topics_with_new_users: topics_with_new_users,
|
||||
unique_new_users: unique_new_users,
|
||||
new_users_count: new_user_ids.count,
|
||||
},
|
||||
identifier: "new-user-interactions",
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
106
app/services/discourse_rewind/action/time_of_day_activity.rb
Normal file
106
app/services/discourse_rewind/action/time_of_day_activity.rb
Normal file
@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Time of day activity analysis
|
||||
# Shows when a user is most active (considering their timezone)
|
||||
# Determines if they are a night owl or early bird
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class TimeOfDayActivity < BaseReport
|
||||
EARLY_BIRD_THRESHOLD = 6..9
|
||||
NIGHT_OWL_THRESHOLD_PM = 22..23
|
||||
NIGHT_OWL_THRESHOLD_AM = 0..2
|
||||
|
||||
def call
|
||||
# Get activity by hour of day (in user's timezone)
|
||||
activity_by_hour = get_activity_by_hour
|
||||
|
||||
return if activity_by_hour.empty?
|
||||
|
||||
total_activities = activity_by_hour.values.sum
|
||||
most_active_hour = activity_by_hour.max_by { |_, count| count }&.first
|
||||
personality = determine_personality(activity_by_hour, total_activities)
|
||||
|
||||
{
|
||||
data: {
|
||||
activity_by_hour: activity_by_hour,
|
||||
most_active_hour: most_active_hour,
|
||||
personality: personality,
|
||||
total_activities: total_activities,
|
||||
},
|
||||
identifier: "time-of-day-activity",
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_activity_by_hour
|
||||
# Get user timezone offset
|
||||
user_timezone = user.user_option&.timezone || "UTC"
|
||||
quoted_timezone = ActiveRecord::Base.connection.quote(user_timezone)
|
||||
hour_extract_sql =
|
||||
Arel.sql(
|
||||
"EXTRACT(HOUR FROM created_at AT TIME ZONE 'UTC' AT TIME ZONE #{quoted_timezone})::integer",
|
||||
)
|
||||
|
||||
# Initialize hash with all hours
|
||||
activity = (0..23).to_h { |hour| [hour, 0] }
|
||||
|
||||
# Posts created
|
||||
post_hours =
|
||||
Post
|
||||
.where(user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.pluck(hour_extract_sql)
|
||||
.tally
|
||||
|
||||
# User visits (page views)
|
||||
visit_hours =
|
||||
UserHistory
|
||||
.where(acting_user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.where(action: UserHistory.actions[:page_view])
|
||||
.pluck(hour_extract_sql)
|
||||
.tally
|
||||
|
||||
# Chat messages if chat is enabled
|
||||
if SiteSetting.chat_enabled
|
||||
chat_hours =
|
||||
Chat::Message
|
||||
.where(user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.pluck(hour_extract_sql)
|
||||
.tally
|
||||
|
||||
chat_hours.each { |hour, count| activity[hour] += count }
|
||||
end
|
||||
|
||||
post_hours.each { |hour, count| activity[hour] += count }
|
||||
visit_hours.each { |hour, count| activity[hour] += count }
|
||||
|
||||
activity
|
||||
end
|
||||
|
||||
def determine_personality(activity_by_hour, total_activities)
|
||||
return nil if total_activities == 0
|
||||
|
||||
early_bird_activity = EARLY_BIRD_THRESHOLD.sum { |hour| activity_by_hour[hour] || 0 }
|
||||
night_owl_activity =
|
||||
NIGHT_OWL_THRESHOLD_PM.sum { |hour| activity_by_hour[hour] || 0 } +
|
||||
NIGHT_OWL_THRESHOLD_AM.sum { |hour| activity_by_hour[hour] || 0 }
|
||||
|
||||
early_bird_percentage = (early_bird_activity.to_f / total_activities * 100).round(1)
|
||||
night_owl_percentage = (night_owl_activity.to_f / total_activities * 100).round(1)
|
||||
|
||||
if early_bird_percentage > 20
|
||||
{ type: "early_bird", percentage: early_bird_percentage }
|
||||
elsif night_owl_percentage > 20
|
||||
{ type: "night_owl", percentage: night_owl_percentage }
|
||||
else
|
||||
{ type: "balanced", percentage: 0 }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -31,6 +31,13 @@ module DiscourseRewind
|
||||
Action::BestTopics,
|
||||
Action::BestPosts,
|
||||
Action::ActivityCalendar,
|
||||
Action::TimeOfDayActivity,
|
||||
Action::NewUserInteractions,
|
||||
Action::ChatUsage,
|
||||
Action::AiUsage,
|
||||
Action::FavoriteGifs,
|
||||
Action::Assignments,
|
||||
Action::Invites,
|
||||
]
|
||||
|
||||
model :year
|
||||
|
||||
@ -35,10 +35,10 @@ RSpec.describe(DiscourseRewind::FetchReports) do
|
||||
before { freeze_time DateTime.parse("2021-12-22") }
|
||||
|
||||
it "returns the cached reports" do
|
||||
expect(result.reports.length).to eq(9)
|
||||
expect(result.reports.length).to eq(16)
|
||||
|
||||
allow(DiscourseRewind::Action::TopWords).to receive(:call)
|
||||
expect(result.reports.length).to eq(9)
|
||||
expect(result.reports.length).to eq(16)
|
||||
expect(DiscourseRewind::Action::TopWords).to_not have_received(:call)
|
||||
end
|
||||
end
|
||||
@ -51,7 +51,7 @@ RSpec.describe(DiscourseRewind::FetchReports) do
|
||||
|
||||
it "returns the reports" do
|
||||
allow(DiscourseRewind::Action::TopWords).to receive(:call)
|
||||
expect(result.reports.length).to eq(9)
|
||||
expect(result.reports.length).to eq(16)
|
||||
expect(DiscourseRewind::Action::TopWords).to have_received(:call)
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user