mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
fix(frontend): keep private hub project names out of telemetry
An instance pointed at its own hub imports its own projects, and the slug
naming one is the customer's content — `template_import` was recording it
verbatim, which the disclosure ("the name of any public hub project") does
not cover and `hub_script` already avoids by collapsing a private script to
`private`.
`hubProjectUsageKey` gives projects the same treatment, deciding by the
configured hub's host so a port, a scheme's case or a trailing slash cannot
turn a private hub into a public one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
This commit is contained in:
co-authored by
Claude Opus 5
parent
4ca1970ab1
commit
36352c482a
@@ -15,7 +15,7 @@
|
||||
import { useSetupStep } from '$lib/importWizard/setupStep.svelte'
|
||||
import type { ImportPlan } from '$lib/importWizard/plan'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { logFeatureUsage } from '$lib/utils/featureUsage'
|
||||
import { hubProjectUsageKey, logFeatureUsage } from '$lib/utils/featureUsage'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
/**
|
||||
@@ -225,7 +225,7 @@
|
||||
function finish(setupOutcome: SetupOutcome, outstanding = 1) {
|
||||
// On the way out rather than on the pick: what is worth counting is an import that
|
||||
// landed, not a dialog that was opened and abandoned.
|
||||
if (slug) logFeatureUsage('home', 'template_import', { key: slug })
|
||||
if (slug) logFeatureUsage('home', 'template_import', { key: hubProjectUsageKey(slug) })
|
||||
logFeatureUsage('home', 'template_setup', { key: setupKey(setupOutcome, outstanding) })
|
||||
finishing = true
|
||||
// Through the same deferred reload every closing uses. `done` survives a retry, so
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('$lib/gen', () => ({ OpenAPI: { BASE: '/api' } }))
|
||||
vi.mock('$lib/stores', () => ({ workspaceStore: { subscribe: () => () => {} } }))
|
||||
|
||||
// A store `get()` can read, so a test can say which hub the instance points at.
|
||||
const hubBaseUrl = vi.hoisted(() => {
|
||||
let value = 'https://hub.windmill.dev'
|
||||
return {
|
||||
set: (v: string) => (value = v),
|
||||
store: {
|
||||
subscribe: (run: (v: string) => void) => {
|
||||
run(value)
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('$lib/stores', () => ({
|
||||
workspaceStore: { subscribe: () => () => {} },
|
||||
hubBaseUrlStore: hubBaseUrl.store
|
||||
}))
|
||||
|
||||
import {
|
||||
createFeatureUsageBuffer,
|
||||
hubProjectUsageKey,
|
||||
hubScriptUsageKey,
|
||||
type FeatureUsageEventPayload
|
||||
} from './featureUsage'
|
||||
@@ -107,3 +126,34 @@ describe('hubScriptUsageKey', () => {
|
||||
).toBe('acme/list_a_user_s_items_sorted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hubProjectUsageKey', () => {
|
||||
it('reports the slug for every spelling of the public hub', () => {
|
||||
for (const hub of [
|
||||
'https://hub.windmill.dev',
|
||||
'http://hub.windmill.dev/',
|
||||
'HTTPS://hub.windmill.dev',
|
||||
'https://HUB.WINDMILL.DEV',
|
||||
'https://hub.windmill.dev:443',
|
||||
' https://hub.windmill.dev '
|
||||
]) {
|
||||
hubBaseUrl.set(hub)
|
||||
expect(hubProjectUsageKey('stripe-invoices'), hub).toBe('stripe-invoices')
|
||||
}
|
||||
})
|
||||
|
||||
it("keeps a private hub's project names off the wire", () => {
|
||||
// The slug is the customer's own content on an instance running its own hub, and the
|
||||
// disclosure only claims public project names.
|
||||
for (const hub of [
|
||||
'https://hub.internal.example',
|
||||
'https://hub.windmill.dev.evil.example',
|
||||
'https://windmill.dev',
|
||||
'hub.windmill.dev',
|
||||
'not a url'
|
||||
]) {
|
||||
hubBaseUrl.set(hub)
|
||||
expect(hubProjectUsageKey('acme-payroll'), hub).toBe('private')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { get } from 'svelte/store'
|
||||
import { OpenAPI } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
|
||||
import { hubBaseUrlStore, workspaceStore } from '$lib/stores'
|
||||
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
|
||||
|
||||
// Anonymous product-usage counters (e.g. AI session activity), batched into the
|
||||
// backend `feature_usage` accumulator. Only aggregated counts ever leave the
|
||||
@@ -187,3 +187,31 @@ export function hubScriptUsageKey(script: {
|
||||
if (!app) return PRIVATE_HUB_KEY
|
||||
return (summary ? `${app}/${summary}` : app).slice(0, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* A hub project's slug is only reportable when it names something on the public hub. An
|
||||
* instance pointed at its own hub imports its own projects, whose names are the customer's
|
||||
* content — the same reason `hubScriptUsageKey` collapses a private script to `private`,
|
||||
* and what the disclosure means by "the name of any public hub project".
|
||||
*
|
||||
* Compared by host, so the port, scheme and trailing slash an operator may have typed do
|
||||
* not decide it. Anything unparseable answers private.
|
||||
*/
|
||||
export function hubProjectUsageKey(slug: string): string {
|
||||
return isPublicHub(get(hubBaseUrlStore)) ? slug : PRIVATE_HUB_KEY
|
||||
}
|
||||
|
||||
function isPublicHub(hub: string): boolean {
|
||||
const host = (url: string): string | undefined => {
|
||||
try {
|
||||
const parsed = new URL(url.trim())
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
||||
? parsed.hostname.replace(/\.$/, '').toLowerCase()
|
||||
: undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
const configured = host(hub)
|
||||
return configured !== undefined && configured === host(DEFAULT_HUB_BASE_URL)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user