mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 17:53:55 +08:00
Adds basic usage metrics for **workflows**, **automations**, and **data explorer** so we can measure adoption across all sites, following the pattern established for Kanban (dev topic `t/185911`). Each plugin registers stats via the freeform `stat_type` API, so the values flow automatically into our site statistics pipeline. No infrastructure changes are needed. ## Metrics For each feature: `total` (running count), `created`, `edited`, `executed` (distinct objects run), and `executions` (total run count) — each windowed over `last_day / 7_days / 30_days / previous_30_days`, with a lifetime `count` on `total` and `executions`. Resulting columns: | | Workflows | Automations | Data Explorer | |---|---|---|---| | total | `workflows_total_count` | `automations_total_count` | `de_queries_total_count` | | created | `workflows_created_*` | `automations_created_*` | `de_queries_created_*` | | edited | `workflows_edited_*` | `automations_edited_*` | `de_queries_edited_*` | | executed | `workflows_executed_*` | `automations_executed_*` | `de_queries_executed_*` | | executions | `workflows_executions_*` | `automations_executions_*` | `de_executions_*` | ## How executions are counted Automations already have a daily rollup (`discourse_automation_stats`), so they reuse it. Workflows and Data Explorer get a small daily rollup table each (`discourse_workflows_execution_stats`, `data_explorer_query_stats`) recording `total_runs` per object per day. This deliberately avoids two traps: - **Workflow executions are purged** after `workflow_executions_retention_days` (default 30), and the purged rows carry heavy payloads (full node-graph snapshot + step I/O). Counting from a lightweight rollup keeps `previous_30_days`/lifetime metrics valid without retaining those payloads, and decouples the metrics from a per-site retention setting. - **Data Explorer has no run log** — only a single `last_run_at`. The rollup gives us real execution counts and a durable `previous_30_days` window. Workflow runs are recorded via an `after_create` on `Execution` (one row per execution, rate-limited executions excluded). Data Explorer runs go through a new `Query#record_run!` used by the three run sites. ## Data Explorer `updated_at` fix Query runs previously did `query.update!(last_run_at:)`, which bumped `updated_at` on every run and made "edited" indistinguishable from "executed". `record_run!` now uses `update_columns`, so `updated_at` reflects genuine edits again. Default (unpersisted) queries are still persisted on first run as before, and the `DeleteHiddenQueries` job is unaffected (it also gates on `last_run_at`). ## Core change `register_stat` deduplicated by name only, so the three plugins couldn't all register generic names like `total`/`executions` — whichever activated first won and the rest were silently dropped. The identity check now includes `stat_type`, so a name can be reused across stat types (columns stay unique because they are prefixed with the stat type). Kanban avoided this only because its names happened to be globally unique. ## Notes for review - **Single `de` stat_type** (yielding `de_queries_*` and `de_executions_*`) rather than splitting into `de_queries`/`de_executions` — happy to change if you'd rather query by two separate `stat_type_freeform` values. cc measurement folks. - `structure.sql` and model annotations were hand-updated (local box is on PG17, which can't run the temp-DB dump/annotate tasks); CI will verify. ## Tests - New specs for all three `Statistics` modules and both rollup models (`.log`, the `after_create`/`record_run!` hooks, the `updated_at` behavior). - New core spec covering same-name/different-`stat_type` registration.
157 lines
6.1 KiB
Ruby
Vendored
157 lines
6.1 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
# name: discourse-workflows
|
|
# about: Workflow automation system for Discourse
|
|
# meta_topic_id: 406990
|
|
# version: 0.1
|
|
# authors: Discourse
|
|
# url: https://github.com/discourse/discourse-workflows
|
|
|
|
enabled_site_setting :enable_discourse_workflows
|
|
|
|
module ::DiscourseWorkflows
|
|
PLUGIN_NAME = "discourse-workflows"
|
|
TEMPLATES_PATH = File.expand_path("config/templates", __dir__)
|
|
end
|
|
|
|
require_relative "lib/discourse_workflows/engine"
|
|
require_relative "lib/discourse_workflows/plugin_node_registration"
|
|
|
|
register_asset "stylesheets/common/index.scss"
|
|
register_svg_icon "bolt"
|
|
register_svg_icon "arrows-split-up-and-left"
|
|
register_svg_icon "list"
|
|
register_svg_icon "arrow-rotate-right"
|
|
register_svg_icon "arrows-turn-to-dots"
|
|
register_svg_icon "globe"
|
|
register_svg_icon "table"
|
|
register_svg_icon "table-cells"
|
|
register_svg_icon "expand"
|
|
register_svg_icon "magnifying-glass-minus"
|
|
register_svg_icon "magnifying-glass-plus"
|
|
register_svg_icon "user-check"
|
|
register_svg_icon "calendar-days"
|
|
register_svg_icon "rectangle-list"
|
|
register_svg_icon "trash-can"
|
|
register_svg_icon "broom"
|
|
register_svg_icon "arrow-pointer"
|
|
register_svg_icon "note-sticky"
|
|
register_svg_icon "palette"
|
|
register_svg_icon "reply"
|
|
register_svg_icon "triangle-exclamation"
|
|
register_svg_icon "clock"
|
|
register_svg_icon "dollar-sign"
|
|
register_svg_icon "comments"
|
|
register_svg_icon "pause"
|
|
register_svg_icon "window-maximize"
|
|
register_svg_icon "user-plus"
|
|
register_svg_icon "user-minus"
|
|
register_svg_icon "grip-vertical"
|
|
register_svg_icon "paragraph"
|
|
register_svg_icon "arrow-down-a-z"
|
|
register_svg_icon "copy"
|
|
register_svg_icon "paste"
|
|
register_svg_icon "scissors"
|
|
|
|
add_admin_route "discourse_workflows.admin.title", "discourse-workflows", use_new_show_route: true
|
|
|
|
DiscoursePluginRegistry.define_filtered_register(:discourse_workflows_nodes)
|
|
DiscoursePluginRegistry.define_filtered_register(:discourse_workflows_credential_types)
|
|
|
|
after_initialize do
|
|
Rails.application.config.filter_parameters += %i[signature]
|
|
|
|
add_to_class(:guardian, :can_manage_workflows?) { is_admin? }
|
|
|
|
nodes_dir = File.join(File.dirname(__FILE__), "lib/discourse_workflows/nodes")
|
|
Dir.glob(File.join(nodes_dir, "**/*.rb")).each { |f| Rails.autoloaders.main.load_file(f) }
|
|
|
|
DiscourseWorkflows::NodeType.registered_nodes.each do |node_class|
|
|
DiscoursePluginRegistry.register_discourse_workflows_node(node_class, self)
|
|
|
|
next unless node_class.respond_to?(:event_name) && node_class.event_name
|
|
on(node_class.event_name) do |*args|
|
|
DiscourseWorkflows::EventListener.handle(node_class, *args)
|
|
end
|
|
end
|
|
|
|
DiscourseWorkflows.node_registration_ready = true
|
|
DiscourseWorkflows.flush_plugin_node_registrations!
|
|
DiscourseWorkflows::Registry.reset_indexes!
|
|
|
|
DiscoursePluginRegistry.register_discourse_workflows_credential_type(
|
|
DiscourseWorkflows::CredentialTypes::BasicAuth,
|
|
self,
|
|
)
|
|
DiscoursePluginRegistry.register_discourse_workflows_credential_type(
|
|
DiscourseWorkflows::CredentialTypes::BearerToken,
|
|
self,
|
|
)
|
|
DiscoursePluginRegistry.register_discourse_workflows_credential_type(
|
|
DiscourseWorkflows::CredentialTypes::HeaderAuth,
|
|
self,
|
|
)
|
|
|
|
if defined?(DiscourseAi)
|
|
require_relative "lib/discourse_workflows/ai/tools/base"
|
|
require_relative "lib/discourse_workflows/ai/graph_digest"
|
|
require_relative "lib/discourse_workflows/ai/progress_publisher"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_node_catalog"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_ai_agent_catalog"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_graph_context"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_validate_patch"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_ask_questions"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_resolve_entity"
|
|
require_relative "lib/discourse_workflows/ai/tools/search_chat_channels"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_script_context"
|
|
require_relative "lib/discourse_workflows/ai/tools/workflow_validate_script"
|
|
require_relative "lib/discourse_workflows/ai_workflow_author"
|
|
|
|
DiscourseAi.register_feature(
|
|
module_name: :discourse_workflows,
|
|
feature: :workflow_authoring,
|
|
agent_klass: DiscourseWorkflows::AiWorkflowAuthor,
|
|
enabled_by_setting: "discourse_workflows_ai_authoring_enabled",
|
|
plugin: self,
|
|
)
|
|
end
|
|
|
|
add_to_serializer :site,
|
|
:topic_admin_button_workflows,
|
|
include_condition: -> { scope.is_admin? } do
|
|
DiscourseWorkflows::WorkflowDependency.cached_topic_admin_buttons
|
|
end
|
|
|
|
add_to_serializer :current_user,
|
|
:discourse_workflows_user_modal_last_id,
|
|
include_condition: -> do
|
|
DiscourseWorkflows::WorkflowDependency.cached_user_modals?
|
|
end do
|
|
MessageBus.last_id(DiscourseWorkflows::Nodes::Modal::V1.user_channel(object.id))
|
|
end
|
|
|
|
on(:site_setting_changed) do |name, old_value, new_value|
|
|
next if name != :enable_discourse_workflows
|
|
next unless new_value && !old_value
|
|
|
|
DiscourseWorkflows::PluginEnableHandler.handle!
|
|
end
|
|
|
|
# Automatic promotion of an upcoming change does not write the site setting,
|
|
# so :site_setting_changed never fires for it. See UpcomingChanges::NotifyPromotion.
|
|
on(:upcoming_change_enabled) do |name|
|
|
next if name != :enable_discourse_workflows
|
|
|
|
# A manual opt-in writes the setting before this event fires, so the
|
|
# :site_setting_changed hook above has already run.
|
|
next if SiteSetting.setting_modified_from_default?(:enable_discourse_workflows)
|
|
|
|
DiscourseWorkflows::PluginEnableHandler.handle!
|
|
end
|
|
|
|
register_stat("total", stat_type: :workflows) { DiscourseWorkflows::Statistics.total }
|
|
register_stat("created", stat_type: :workflows) { DiscourseWorkflows::Statistics.created }
|
|
register_stat("edited", stat_type: :workflows) { DiscourseWorkflows::Statistics.edited }
|
|
register_stat("executed", stat_type: :workflows) { DiscourseWorkflows::Statistics.executed }
|
|
register_stat("executions", stat_type: :workflows) { DiscourseWorkflows::Statistics.executions }
|
|
end
|