diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 1e47f04d6d..43ec791585 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -419,7 +419,7 @@ const_format = { version = "0.2.35", features = ["rust_1_64", "rust_1_51"] } const-str = "0.5" constant_time_eq = "0.3.1" rsa = "^0" -aes-gcm = "^0" +aes-gcm = "0.10.3" async_zip = { version = "0.0.17", features = ["tokio", "tokio-fs", "deflate", "chrono"] } once_cell = "1.17.1" dashmap = "6.1.0" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 6496156c79..fd060cbd4e 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -7596cefdba81482c0b0c0b61be26369f112d8009 \ No newline at end of file +7596cefdba81482c0b0c0b61be26369f112d8009 diff --git a/backend/migrations/20260204181621_delete_saved_telemetry.down.sql b/backend/migrations/20260204181621_delete_saved_telemetry.down.sql new file mode 100644 index 0000000000..0435203b02 --- /dev/null +++ b/backend/migrations/20260204181621_delete_saved_telemetry.down.sql @@ -0,0 +1 @@ +-- No-op: cannot restore deleted telemetry data diff --git a/backend/migrations/20260204181621_delete_saved_telemetry.up.sql b/backend/migrations/20260204181621_delete_saved_telemetry.up.sql new file mode 100644 index 0000000000..aae45e937f --- /dev/null +++ b/backend/migrations/20260204181621_delete_saved_telemetry.up.sql @@ -0,0 +1,2 @@ +-- Delete all saved telemetry data from metrics table +DELETE FROM metrics WHERE id = 'telemetry'; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index f041b5dcff..0047421126 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1310,6 +1310,20 @@ paths: schema: type: string + /settings/get_stats: + get: + summary: get encrypted telemetry stats (EE only) + operationId: getStats + tags: + - setting + responses: + "200": + description: base64-encoded encrypted telemetry blob + content: + text/plain: + schema: + type: string + /settings/latest_key_renewal_attempt: get: summary: get latest key renewal attempt diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 5c43e33d30..d68797f7ee 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -58,6 +58,7 @@ pub fn global_service() -> Router { .route("/test_smtp", post(test_email)) .route("/test_license_key", post(test_license_key)) .route("/send_stats", post(send_stats)) + .route("/get_stats", get(get_stats)) .route( "/latest_key_renewal_attempt", get(get_latest_key_renewal_attempt), @@ -434,6 +435,25 @@ pub async fn send_stats(Extension(db): Extension, authed: ApiAuthed) -> Resu Ok("Sent stats".to_string()) } +#[cfg(feature = "enterprise")] +pub async fn get_stats(Extension(db): Extension, authed: ApiAuthed) -> Result { + require_super_admin(&db, &authed.email).await?; + let stats = windmill_common::stats_oss::get_stats_payload( + &db, + &windmill_common::stats_oss::SendStatsReason::Manual, + ) + .await?; + let encrypted = windmill_common::stats_oss::encrypt_stats(&stats)?; + Ok(encrypted) +} + +#[cfg(not(feature = "enterprise"))] +pub async fn get_stats() -> Result { + Err(error::Error::BadRequest( + "Downloading telemetry is only available on enterprise edition".to_string(), + )) +} + #[derive(serde::Serialize)] pub struct KeyRenewalAttempt { result: String, diff --git a/backend/windmill-common/src/stats_oss.rs b/backend/windmill-common/src/stats_oss.rs index 4e388daf5c..161c78f81b 100644 --- a/backend/windmill-common/src/stats_oss.rs +++ b/backend/windmill-common/src/stats_oss.rs @@ -50,3 +50,19 @@ pub async fn get_user_usage<'c, E: sqlx::Executor<'c, Database = Postgres>>( let usage = ActiveUserUsage { author_count: None, operator_count: None }; Ok(usage) } + +#[cfg(not(feature = "private"))] +#[derive(serde::Serialize)] +pub struct Stats {} + +#[cfg(not(feature = "private"))] +pub async fn get_stats_payload(_db: &DB, _reason: &SendStatsReason) -> Result { + // stats details are closed source + Ok(Stats {}) +} + +#[cfg(not(feature = "private"))] +pub fn encrypt_stats(_stats: &Stats) -> Result { + // stats details are closed source + Ok(String::new()) +} diff --git a/frontend/src/lib/components/InstanceSetting.svelte b/frontend/src/lib/components/InstanceSetting.svelte index 9425d3be59..10d9fd08e8 100644 --- a/frontend/src/lib/components/InstanceSetting.svelte +++ b/frontend/src/lib/components/InstanceSetting.svelte @@ -177,7 +177,7 @@ {/snippet} -{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null) && !(setting.hiddenIfEmpty && !$values[setting.key])} +{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null) && !(setting.hiddenIfEmpty && !$values[setting.key]) && !(setting.hiddenInEe && $enterpriseLicense)} {#if setting.fieldType == 'select'}
{@render LabelSnippet()} diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index f3b3f90d82..586336d876 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -231,6 +231,28 @@ } } + let downloadingStats = $state(false) + async function downloadStats() { + try { + downloadingStats = true + const encryptedData = await SettingService.getStats() + const blob = new Blob([encryptedData], { type: 'application/octet-stream' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `windmill-telemetry-${new Date().toISOString().split('T')[0]}.enc` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + sendUserToast('Telemetry data downloaded') + } catch (err) { + throw err + } finally { + downloadingStats = false + } + } + function isValidTeamsChannel(value: any): value is TeamsChannel { return ( typeof value === 'object' && @@ -341,18 +363,29 @@
On Enterprise Edition, you must send data to check that usage is in line with the terms of the subscription. You can either enable telemetry or regularly send usage - data by clicking the button below. + data by clicking the button below. For air-gapped instances, you can download the + telemetry data and send it manually. +
+
+ +
- {/if} {:else if category == 'Auth/OAuth/SAML'} boolean error?: string @@ -497,7 +498,8 @@ export const settings: Record = { label: 'Disable telemetry', key: 'disable_stats', fieldType: 'boolean', - storage: 'setting' + storage: 'setting', + hiddenInEe: true } ], 'Secret Storage': [