feat: download encrypted usage (#7804)

This commit is contained in:
hugocasa
2026-02-10 10:50:24 +01:00
committed by GitHub
parent cf596f370a
commit 8363ff1eee
10 changed files with 103 additions and 15 deletions
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -1 +1 @@
7596cefdba81482c0b0c0b61be26369f112d8009
7596cefdba81482c0b0c0b61be26369f112d8009
@@ -0,0 +1 @@
-- No-op: cannot restore deleted telemetry data
@@ -0,0 +1,2 @@
-- Delete all saved telemetry data from metrics table
DELETE FROM metrics WHERE id = 'telemetry';
+14
View File
@@ -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
+20
View File
@@ -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<DB>, authed: ApiAuthed) -> Resu
Ok("Sent stats".to_string())
}
#[cfg(feature = "enterprise")]
pub async fn get_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Result<String> {
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<String> {
Err(error::Error::BadRequest(
"Downloading telemetry is only available on enterprise edition".to_string(),
))
}
#[derive(serde::Serialize)]
pub struct KeyRenewalAttempt {
result: String,
+16
View File
@@ -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> {
// stats details are closed source
Ok(Stats {})
}
#[cfg(not(feature = "private"))]
pub fn encrypt_stats(_stats: &Stats) -> Result<String> {
// stats details are closed source
Ok(String::new())
}
@@ -177,7 +177,7 @@
{/snippet}
<!-- {JSON.stringify($values, null, 2)} -->
{#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'}
<div>
{@render LabelSnippet()}
@@ -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 @@
<div class="text-primary pb-4 text-xs">
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.
</div>
<div class="flex gap-2 mb-4">
<Button
on:click={sendStats}
variant="default"
btnClasses="w-auto"
loading={sendingStats}
size="xs"
>
Send usage
</Button>
<Button
on:click={downloadStats}
variant="default"
btnClasses="w-auto"
loading={downloadingStats}
size="xs"
>
Download usage
</Button>
</div>
<Button
on:click={sendStats}
variant="default"
btnClasses="w-auto"
wrapperClasses="mb-4"
loading={sendingStats}
size="xs"
>
Send usage
</Button>
{/if}
{:else if category == 'Auth/OAuth/SAML'}
<AuthSettings
@@ -58,6 +58,7 @@ export interface Setting {
}
hiddenIfNull?: boolean
hiddenIfEmpty?: boolean
hiddenInEe?: boolean
requiresReloadOnChange?: boolean
isValid?: (value: any) => boolean
error?: string
@@ -497,7 +498,8 @@ export const settings: Record<string, Setting[]> = {
label: 'Disable telemetry',
key: 'disable_stats',
fieldType: 'boolean',
storage: 'setting'
storage: 'setting',
hiddenInEe: true
}
],
'Secret Storage': [