Sam 8c8bc94ed8
FEATURE: Add automation statistics tracking to Automation (#31921)
introduces comprehensive statistics tracking for the Discourse
Automation plugin, allowing users to monitor the performance and
execution patterns of their automations:

- Add `discourse_automation_stats` table to track execution metrics
including run counts, execution times, and performance data
- Create a new `Stat` model to handle tracking and retrieving automation
statistics
- Update the admin UI to display automation stats (runs today/this
week/month and last run time)
- Modernize the automation list interface using Glimmer components
- Replace the older enable/disable icon with a toggle switch for better
UX
- Add schema annotations to existing models for better code
documentation
- Include extensive test coverage for the new statistics functionality

This helps administrators understand how their automations are
performing and identify potential bottlenecks or optimization
opportunities.

---------

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Ted Johansson <ted@discourse.org>
2025-03-21 12:53:26 +11:00

153 lines
4.3 KiB
Ruby

# frozen_string_literal: true
module DiscourseAutomation
class AutomationSerializer < ApplicationSerializer
attribute :id
attribute :name
attribute :enabled
attribute :script
attribute :trigger
attribute :updated_at
attribute :last_updated_by
attribute :next_pending_automation_at
attribute :placeholders
attribute :stats
def last_updated_by
BasicUserSerializer.new(
User.find_by(id: object.last_updated_by_id) || Discourse.system_user,
root: false,
).as_json
end
def include_next_pending_automation_at?
object.pending_automations.exists?
end
def next_pending_automation_at
object&.pending_automations&.first&.execute_at
end
def placeholders
scriptable_placeholders =
DiscourseAutomation
.filter_by_trigger(scriptable&.placeholders || [], object.trigger)
.map { |placeholder| placeholder[:name] }
triggerable_placeholders = triggerable&.placeholders || []
(scriptable_placeholders + triggerable_placeholders).map do |placeholder|
placeholder.to_s.gsub(/\s+/, "_").underscore
end
end
def script
key = "discourse_automation.scriptables"
doc_key = "#{key}.#{object.script}.doc"
script_with_trigger_key = "#{key}.#{object.script}_with_#{object.trigger}.doc"
{
id: object.script,
version: scriptable.version,
name:
I18n.t(
"#{key}.#{object.script}.title",
default: "Missing translation for #{key}.#{object.script}.title",
),
description: I18n.t("#{key}.#{object.script}.description", default: ""),
doc: I18n.exists?(doc_key, :en) ? I18n.t(doc_key) : nil,
with_trigger_doc:
I18n.exists?(script_with_trigger_key, :en) ? I18n.t(script_with_trigger_key) : nil,
forced_triggerable: scriptable.forced_triggerable,
not_found: scriptable.not_found,
templates:
process_templates(filter_fields_with_priority(scriptable.fields, object.trigger&.to_sym)),
fields: process_fields(object.fields.where(target: "script")),
}
end
def trigger
key = "discourse_automation.triggerables"
doc_key = "#{key}.#{object.trigger}.doc"
{
id: object.trigger,
name:
I18n.t(
"#{key}.#{object.trigger}.title",
default: "Missing translation for #{key}.#{object.trigger}.title",
),
description: I18n.t("#{key}.#{object.trigger}.description", default: ""),
doc: I18n.exists?(doc_key, :en) ? I18n.t(doc_key) : nil,
not_found: triggerable&.not_found,
templates: process_templates(triggerable&.fields || []),
fields: process_fields(object.fields.where(target: "trigger")),
settings: triggerable&.settings,
}
end
def include_stats?
scope&.dig(:stats).present?
end
EMPTY_STATS = {
total_runs: 0,
total_time: 0,
average_run_time: 0,
min_run_time: 0,
max_run_time: 0,
}
def stats
automation_stats = scope&.dig(:stats, object.id) || {}
{
last_day: automation_stats[:last_day] || EMPTY_STATS,
last_week: automation_stats[:last_week] || EMPTY_STATS,
last_month: automation_stats[:last_month] || EMPTY_STATS,
last_run_at: automation_stats[:last_run_at],
}
end
private
def filter_fields_with_priority(arr, trigger)
unique_with_priority = {}
arr.each do |item|
name = item[:name]
if (item[:triggerable]&.to_sym == trigger&.to_sym || item[:triggerable].nil?) &&
(!unique_with_priority.key?(name) || unique_with_priority[name][:triggerable].nil?)
unique_with_priority[name] = item
end
end
unique_with_priority.values
end
def process_templates(fields)
ActiveModel::ArraySerializer.new(
fields,
each_serializer: DiscourseAutomation::TemplateSerializer,
scope: {
automation: object,
},
).as_json
end
def process_fields(fields)
ActiveModel::ArraySerializer.new(
fields || [],
each_serializer: DiscourseAutomation::FieldSerializer,
).as_json || []
end
def scriptable
object.scriptable
end
def triggerable
object.triggerable
end
end
end