0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/lib/period_count_helper.rb
Régis Hanol 527a13566f
DEV: Add usage metrics for workflows, automations, and data explorer (#41437)
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.
2026-07-13 11:41:58 +02:00

16 lines
583 B
Ruby
Vendored

# frozen_string_literal: true
module PeriodCountHelper
def period_counts(scope, column, count: true, &aggregate)
aggregate ||= ->(relation) { relation.count }
col = scope.arel_table[column]
result = {
last_day: aggregate.call(scope.where(col.gt(1.day.ago))),
"7_days": aggregate.call(scope.where(col.gt(7.days.ago))),
"30_days": aggregate.call(scope.where(col.gt(30.days.ago))),
previous_30_days: aggregate.call(scope.where(col.between(60.days.ago..30.days.ago))),
}
result[:count] = aggregate.call(scope) if count
result
end
end