count trigger suspend, resume and discard (#11405)

* feat(telemetry): count trigger suspend, resume and discard

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: shorten the telemetry disclosure to one line per category

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: correct the resume branch comments and note the pre-commit fire count

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: name feature adoption in the telemetry disclosure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 9855e1b7a43a0a33e04f8accf1c497af3fd9b139

This commit updates the EE repository reference after PR #834 was merged in windmill-ee-private.

Previous ee-repo-ref: 1d5b128ec956c156fe549cf099ba0dbc1b6467bf

New ee-repo-ref: 9855e1b7a43a0a33e04f8accf1c497af3fd9b139

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
hugocasa
2026-09-29 12:26:11 +02:00
committed by GitHub
co-authored by Claude Opus 5.5 windmill-internal-app[bot]
parent f367eaf6d0
commit 17448c97d3
8 changed files with 88 additions and 122 deletions
+2 -2
View File
@@ -119,8 +119,8 @@ Form components (TextInput, Toggle, Select, etc.) should use the unified size sy
New user-facing UX is the main source of `feature_usage` counters — propose them in the plan, not
as a separate question, and read `docs/feature-telemetry.md` first. `logFeatureUsage()` from
`$lib/utils/featureUsage` is only half the change: the `(feature, kind)` pair must also be
registered in the backend allowlist or every event is silently discarded, and the disclosure copy
in `InstanceSettings.svelte` must name what you added.
registered in the backend allowlist or every event is silently discarded. The disclosure copy in
`InstanceSettings.svelte` stays at the category level; see the recipe's step 3.
## Svelte MCP Server
@@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT 'schedule' AS \"kind!\", COUNT(*)::BIGINT AS \"count!\" FROM schedule\n UNION ALL SELECT 'http', COUNT(*)::BIGINT FROM http_trigger\n UNION ALL SELECT 'websocket', COUNT(*)::BIGINT FROM websocket_trigger\n UNION ALL SELECT 'kafka', COUNT(*)::BIGINT FROM kafka_trigger\n UNION ALL SELECT 'nats', COUNT(*)::BIGINT FROM nats_trigger\n UNION ALL SELECT 'postgres', COUNT(*)::BIGINT FROM postgres_trigger\n UNION ALL SELECT 'mqtt', COUNT(*)::BIGINT FROM mqtt_trigger\n UNION ALL SELECT 'sqs', COUNT(*)::BIGINT FROM sqs_trigger\n UNION ALL SELECT 'gcp', COUNT(*)::BIGINT FROM gcp_trigger\n UNION ALL SELECT 'azure', COUNT(*)::BIGINT FROM azure_trigger\n UNION ALL SELECT 'amqp', COUNT(*)::BIGINT FROM amqp_trigger\n UNION ALL SELECT 'email', COUNT(*)::BIGINT FROM email_trigger\n -- Grouped, not a single 'native' key: these fire as nextcloud/google/github,\n -- so a lone key would not line up with the `trigger`/`fired` series.\n UNION ALL SELECT service_name::text, COUNT(*)::BIGINT FROM native_trigger GROUP BY service_name\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "kind!",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null
]
},
"hash": "1d346a14ad5586af347b8e7ac413500a39efa20e4915ffa56fd40537597db36e"
}
@@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT 'schedule' AS \"kind!\", COUNT(*)::BIGINT AS \"count!\", 0::BIGINT AS \"suspended!\" FROM schedule\n UNION ALL SELECT 'http', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM http_trigger\n UNION ALL SELECT 'websocket', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM websocket_trigger\n UNION ALL SELECT 'kafka', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM kafka_trigger\n UNION ALL SELECT 'nats', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM nats_trigger\n UNION ALL SELECT 'postgres', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM postgres_trigger\n UNION ALL SELECT 'mqtt', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM mqtt_trigger\n UNION ALL SELECT 'sqs', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM sqs_trigger\n UNION ALL SELECT 'gcp', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM gcp_trigger\n UNION ALL SELECT 'azure', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM azure_trigger\n UNION ALL SELECT 'amqp', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM amqp_trigger\n UNION ALL SELECT 'email', COUNT(*)::BIGINT, COUNT(*) FILTER (WHERE mode = 'suspended')::BIGINT FROM email_trigger\n -- Grouped, not a single 'native' key: these fire as nextcloud/google/github,\n -- so a lone key would not line up with the `trigger`/`fired` series.\n UNION ALL SELECT service_name::text, COUNT(*)::BIGINT, 0::BIGINT FROM native_trigger GROUP BY service_name\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "kind!",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "count!",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "suspended!",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null
]
},
"hash": "872c06b3098cf59797bcaa90d6da5c6acbf5468c190fa72b6b6707d950267752"
}
+1 -1
View File
@@ -1 +1 @@
421cf2a8b4f98b421e93c0fc7c1c378314a66e50
9855e1b7a43a0a33e04f8accf1c497af3fd9b139
+11 -1
View File
@@ -6869,9 +6869,19 @@ async fn push_inner<'c, 'd>(
// `schedule_path` (see `FlowJob::schedule_path`), so counting per push would
// score one run as a fire per step job — a loop pushes two of those per
// iteration — burying every other kind, and would sit on the per-step path.
//
// A job a suspended trigger parks is not a fire: it counts as `fired` only
// when `resume_suspended_trigger_jobs` releases it, or never if discarded.
// Both are counted before the queue caps below and the caller's commit, so a
// rejected push still counts; accepted for a telemetry counter.
if flow_step_id.is_none() {
if let Some(kind) = trigger_kind.as_ref() {
windmill_common::feature_usage::log_feature_usage("trigger", "fired", kind.as_str());
let action = if suspended_mode.unwrap_or(false) {
"suspended"
} else {
"fired"
};
windmill_common::feature_usage::log_feature_usage("trigger", action, kind.as_str());
}
}
+19 -3
View File
@@ -14,6 +14,7 @@ use windmill_api_jobs::execution::cancel_jobs;
use windmill_common::{
db::{UserDB, DB},
error::{self, Error, Result},
feature_usage::log_feature_usage,
jobs::{delete_jobs, JobTriggerKind},
triggers::TriggerMetadata,
};
@@ -168,14 +169,16 @@ pub async fn resume_suspended_trigger_jobs(
.await?
};
let trigger_metadata = TriggerMetadata::new(Some(trigger_path.clone()), trigger_kind);
let trigger_metadata = TriggerMetadata::new(Some(trigger_path.clone()), trigger_kind.clone());
let l = jobs.len();
let mut unsuspended_in_place = 0;
for job in jobs {
// If job was created before trigger was edited, simply update it to unsuspend
// If job was created after trigger was edited, simply update it to unsuspend
// instead of deleting and repushing
if job.created_at > trigger.edited_at {
unsuspended_in_place += 1;
// Map the placeholder unassigned kind back to its assigned counterpart so
// singlestepflow wrappers (retry/error_handler/skip_handler) keep their
// flow-orchestrator identity.
@@ -209,7 +212,7 @@ pub async fn resume_suspended_trigger_jobs(
.execute(&mut *tx)
.await?;
} else {
// Job was created after trigger edit - delete and repush with new configuration
// Job was created before trigger edit - delete and repush with new configuration
// Pass the transaction to trigger_runnable_inner so everything is in the same transaction
let (_uuid, _delete_after_use, _early_return, _has_failure_module, tx_o) =
trigger_runnable_inner(
@@ -274,6 +277,16 @@ pub async fn resume_suspended_trigger_jobs(
tx.commit().await?;
// A repushed job is counted as `fired` inside `push`, before this commit, so
// a resume that rolls back still counts it; accepted for a telemetry counter.
// One unsuspended in place never goes through `push`, so it is counted here.
for _ in 0..unsuspended_in_place {
log_feature_usage("trigger", "fired", trigger_kind.as_str());
}
for _ in 0..l {
log_feature_usage("trigger", "resumed", trigger_kind.as_str());
}
Ok(Json(format!("Reassigned {} jobs", l)))
}
@@ -334,6 +347,9 @@ pub async fn cancel_suspended_trigger_jobs(
true,
)
.await?;
for _ in 0..cancelled_jobs.0.len() {
log_feature_usage("trigger", "discarded", trigger_kind.as_str());
}
Ok(Json(format!("Canceled {} jobs", cancelled_jobs.0.len())))
} else {
Ok(Json(format!("No jobs to cancel")))
+6 -5
View File
@@ -4,7 +4,7 @@
anonymous usage-stats payload. It answers "does anyone use this, and which variant do they pick"
without any identifying data leaving the instance.
It currently carries 58 registered actions across twenty features (`ai_session`, `ai_chat`,
It currently carries 61 registered actions across twenty features (`ai_session`, `ai_chat`,
`ai_fix`, `ai_agent`, `ai_agent_eval`, `app_sandbox`, `datatable`, `db_manager`, `flow_editor`,
`flow_run`, `flow_step`, `home`, `run_form`, `debugger`, `trigger`, `command_script`,
`hub_script`, `usage_meter`, `sso_groups_claim`, `cloud_trial_offer`). Nearly all of the
@@ -48,7 +48,7 @@ vocabulary closed and small — enumerate the values in a TS union next to the c
## The recipe
Four steps. Skipping step 1 or 3 fails quietly.
Four steps. Skipping step 1 fails quietly.
**1. Register the pair** in `FEATURE_USAGE_KINDS`
(`backend/windmill-common/src/feature_usage_ee.rs`, tracked in `windmill-ee-private`). An
@@ -68,9 +68,10 @@ Fire-and-forget. Events sum locally per `(workspace, feature, kind, key, entityI
every 30s, on `visibilitychange` → hidden, and on `pagehide`; 50 events per request, and a failed
batch is dropped rather than retried.
**3. Update the disclosure.** `InstanceSettings.svelte` lists what a non-minimal payload contains
(two places — the copy appears twice). A new counter that isn't named there means the instance
under-discloses what it sends. This has already drifted once.
**3. Check the disclosure.** `InstanceSettings.svelte` describes a non-minimal payload by
category, not counter by counter (two places — the copy appears twice); keep it short. A plain
counter needs no edit. Only a key that sends a new kind of value beyond a count — a name or an
identifier, like the AI model and hub names it already lists — must be added there.
**4. Verify a row lands.** The silent-drop path means "no error" proves nothing:
@@ -1065,54 +1065,19 @@
Telemetry is required on Enterprise Edition for license compliance. When minimal telemetry
is enabled, only the following data is sent:
<ul class="list-disc list-inside pl-2">
<li>version of your instance</li>
<li>instance base URL</li>
<li>login type usage (login type, count)</li>
<li>worker usage (worker, worker instance, vCPUs, memory)</li>
<li
>user usage (author count, operator count, the distinct guests of the last 30 days,
the seats they add past the free allowance, and the workspaces that allow guests)</li
>
<li>superadmin email addresses</li>
<li>development instance status</li>
<li>Instance version and base URL</li>
<li>Login types, user and seat counts</li>
<li>Worker count, vCPUs and memory</li>
<li>Superadmin email addresses</li>
<li>Development instance status</li>
</ul>
<br />When minimal telemetry is disabled, the following is also collected:
<ul class="list-disc list-inside pl-2">
<li>job usage (language, total duration, count)</li>
<li>git sync repo count (sync vs promotion mode)</li>
<li
>feature usage (counts of which product features are used, including AI provider and
model identifiers, the names of public hub scripts used, the languages debug sessions
are started for, whether AI chat skills are turned on or off and how often one is
loaded, whether an AI agent run narrows the tools it may call and whether that leaves
it with none, whether SSO logins evaluate an IdP groups claim (SAML or OIDC) and
change a membership, the plan tier and quota shown when the execution meter is opened,
whether app sandbox isolation is turned on, whether a step's workspace script is
edited from the flow editor, which skin approval steps are given, how many AI sessions
are brought back from the workspace object storage backup, how data tables and their
migrations are set up and used, how often the database manager is switched between its
data and schema diagram views, how often an empty workspace home is seen, how often
the home page’s create menu and hub-project picker are opened and from which entry
point, the name of any public hub project imported from the home page and how far that
import got, whether a pre-approved trial offer was opened, whether data tables are put
under roles and whether callers name a role or take the default, which kinds of access
change (grant, revoke, ownership, default privileges) are applied to data tables, and
whether a data table under roles is cloned into a fork with its schema only or with
its data, last 30 days)</li
>
<li
>feature adoption (counts of which flow, script, trigger, worker and data table
features your deployed items use, including how many apps run sandboxed, how many data
tables exist per database kind, how many use migrations, and what references them)</li
>
<li
>resource counts (workspaces, scripts per language, flows, workflows as code, low-code
apps, raw apps)</li
>
<li
>infrastructure info (container runtime, managed database provider, database version,
size and cluster size, max and active connections, object storage backend)</li
>
<li>Job counts and durations per language</li>
<li>Git sync repository counts</li>
<li>Feature usage and adoption counts, including AI model and public Hub names</li>
<li>Counts of workspaces, scripts, flows and apps</li>
<li>Database, runtime and object storage details</li>
</ul>
<br />For air-gapped instances, you can download the telemetry data and send it manually.
</div>
@@ -1141,45 +1106,13 @@
Anonymous usage data is collected to help improve Windmill.
<br />The following information is collected:
<ul class="list-disc list-inside pl-2">
<li>version of your instance</li>
<li>instance base URL</li>
<li>job usage (language, total duration, count)</li>
<li>login type usage (login type, count)</li>
<li>worker usage (worker, worker instance, vCPUs, memory)</li>
<li
>user usage (author count, operator count, the distinct guests of the last 30 days,
the seats they add past the free allowance, and the workspaces that allow guests)</li
>
<li>development instance status</li>
<li
>feature usage (counts of which product features are used, including AI provider and
model identifiers, the names of public hub scripts used, the languages debug sessions
are started for, whether AI chat skills are turned on or off and how often one is
loaded, whether an AI agent run narrows the tools it may call and whether that leaves
it with none, whether SSO logins evaluate an IdP groups claim (SAML or OIDC) and
change a membership, the plan tier and quota shown when the execution meter is opened,
whether app sandbox isolation is turned on, whether a step's workspace script is
edited from the flow editor, which skin approval steps are given, how many AI sessions
are brought back from the workspace object storage backup, how data tables and their
migrations are set up and used, how often the database manager is switched between its
data and schema diagram views, how often an empty workspace home is seen, how often
the home page’s create menu and hub-project picker are opened and from which entry
point, the name of any public hub project imported from the home page and how far that
import got, whether a pre-approved trial offer was opened, whether data tables are put
under roles and whether callers name a role or take the default, which kinds of access
change (grant, revoke, ownership, default privileges) are applied to data tables, and
whether a data table under roles is cloned into a fork with its schema only or with
its data, last 30 days)</li
>
<li
>feature adoption (counts of which flow, script, trigger, worker and data table
features your deployed items use, including how many apps run sandboxed, how many data
tables exist per database kind, how many use migrations, and what references them)</li
>
<li
>resource counts (workspaces, scripts per language, flows, workflows as code, low-code
apps, raw apps)</li
>
<li>Instance version and base URL</li>
<li>Job counts and durations per language</li>
<li>Login types, user and seat counts</li>
<li>Worker count, vCPUs and memory</li>
<li>Development instance status</li>
<li>Feature usage and adoption counts, including AI model and public Hub names</li>
<li>Counts of workspaces, scripts, flows and apps</li>
</ul>
</div>
{/if}