mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 16:15:51 +00:00
feat(telemetry): extend feature-usage tracking beyond AI features (#10681)
* feat(telemetry): extend feature-usage tracking to long-tail features Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: describe telemetry as product feature usage rather than AI usage Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(telemetry): trim disclosure copy and drop unused pick origin Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): count trigger fires per run and key hub picks from hub data Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): slugify hub keys and order both writers' upserts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): key native trigger adoption by service so it matches fires Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref for native trigger adoption fix Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(telemetry): move feature-usage collection into the ee crate * docs: point feature-telemetry at the moved registry and rust writer * docs: correct the trigger-fire gate comment to match measured step counts * docs: put the private-build caveat on the verification step * chore: update ee-repo-ref to f079db9e7962a413b349c4ff8036080894f30771 This commit updates the EE repository reference after PR #725 was merged in windmill-ee-private. Previous ee-repo-ref: 055adb80416f9339c9a28ae7fbaeadad30d74959 New ee-repo-ref: f079db9e7962a413b349c4ff8036080894f30771 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5
Ruben Fiszel
windmill-internal-app[bot]
parent
98bacab907
commit
53eb94659b
@@ -11489,41 +11489,6 @@ struct LogFeatureUsagePayload {
|
||||
events: Vec<FeatureUsageEvent>,
|
||||
}
|
||||
|
||||
// Only registered (feature, kind) actions are accepted, so telemetry stays
|
||||
// limited to predefined feature actions. Keys are shape-checked (identifier-like,
|
||||
// no spaces) rather than pinned to value sets: they come from our own frontend
|
||||
// (modes, tab/draft kinds, tool names, provider:model) and pinning every value
|
||||
// server-side was not worth the maintenance.
|
||||
const FEATURE_USAGE_KINDS: &[(&str, &str)] = &[
|
||||
("ai_session", "created"),
|
||||
("ai_session", "message"),
|
||||
("ai_session", "autonomy"),
|
||||
("ai_session", "tab"),
|
||||
("ai_session", "tokens"),
|
||||
("ai_session", "deployed"),
|
||||
("ai_session", "archived"),
|
||||
("ai_session", "deleted"),
|
||||
("ai_session", "beta_optout"),
|
||||
("ai_session", "beta_optin"),
|
||||
("ai_chat", "message"),
|
||||
("ai_chat", "model"),
|
||||
("ai_chat", "tool"),
|
||||
("flow_editor", "panel_placement"),
|
||||
];
|
||||
|
||||
fn is_identifier_shaped(s: &str, max_len: usize) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.len() <= max_len
|
||||
&& s.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | ':' | '.' | '/'))
|
||||
}
|
||||
|
||||
fn valid_feature_usage_event(e: &FeatureUsageEvent) -> bool {
|
||||
FEATURE_USAGE_KINDS.contains(&(e.feature.as_str(), e.kind.as_str()))
|
||||
&& (e.key.is_empty() || is_identifier_shaped(&e.key, 100))
|
||||
&& (e.entity_id.is_empty() || is_identifier_shaped(&e.entity_id, 50))
|
||||
}
|
||||
|
||||
async fn log_feature_usage(
|
||||
Extension(db): Extension<DB>,
|
||||
Json(payload): Json<LogFeatureUsagePayload>,
|
||||
@@ -11532,7 +11497,15 @@ async fn log_feature_usage(
|
||||
// single INSERT error out ("cannot affect row a second time").
|
||||
let mut agg: HashMap<(String, String, String, String), i64> = HashMap::new();
|
||||
for e in payload.events.into_iter().take(MAX_FEATURE_USAGE_EVENTS) {
|
||||
if !valid_feature_usage_event(&e) {
|
||||
// Which actions may be recorded lives in
|
||||
// `windmill_common::feature_usage`, shared with the in-process writer so
|
||||
// both admit exactly the same events.
|
||||
if !windmill_common::feature_usage::is_recordable_event(
|
||||
&e.feature,
|
||||
&e.kind,
|
||||
&e.key,
|
||||
&e.entity_id,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let value = e.value.unwrap_or(1).clamp(1, 1_000_000);
|
||||
@@ -11542,12 +11515,18 @@ async fn log_feature_usage(
|
||||
if agg.is_empty() {
|
||||
return Ok(StatusCode::NO_CONTENT);
|
||||
}
|
||||
let mut features = Vec::with_capacity(agg.len());
|
||||
let mut kinds = Vec::with_capacity(agg.len());
|
||||
let mut keys = Vec::with_capacity(agg.len());
|
||||
let mut entity_ids = Vec::with_capacity(agg.len());
|
||||
let mut values = Vec::with_capacity(agg.len());
|
||||
for ((feature, kind, key, entity_id), value) in agg {
|
||||
// Sorted for the same reason as `flush_feature_usage`: this endpoint and the
|
||||
// backend flusher upsert the same rows, and two batches touching them in
|
||||
// opposite orders deadlock.
|
||||
let mut rows: Vec<((String, String, String, String), i64)> = agg.into_iter().collect();
|
||||
rows.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let mut features = Vec::with_capacity(rows.len());
|
||||
let mut kinds = Vec::with_capacity(rows.len());
|
||||
let mut keys = Vec::with_capacity(rows.len());
|
||||
let mut entity_ids = Vec::with_capacity(rows.len());
|
||||
let mut values = Vec::with_capacity(rows.len());
|
||||
for ((feature, kind, key, entity_id), value) in rows {
|
||||
features.push(feature);
|
||||
kinds.push(kind);
|
||||
keys.push(key);
|
||||
|
||||
Reference in New Issue
Block a user