diff --git a/.env.example b/.env.example index bf5df90e..df38c878 100644 --- a/.env.example +++ b/.env.example @@ -97,11 +97,13 @@ RATE_LIMIT_REGISTER_PER_HOUR=3 # OIDC_ALLOWED_DOMAINS=example.com # OIDC_AUTO_CREATE_USERS=true -# Opt-in anonymous usage telemetry: one small ping a day (instance id, version, +# Anonymous usage telemetry: one small ping a day (instance id, version, # edition, warehouse engines, bucketed counts), never names, emails, hosts, -# queries or data. Every field: website/docs/run/telemetry.md. +# queries or data. Every field: website/docs/run/telemetry.md. Unset, it is on +# in Community and off in Enterprise; DO_NOT_TRACK=1 turns it off regardless. # TELEMETRY_ENABLED=false # TELEMETRY_ENDPOINT=https://telemetry.tripl.io/v1/ping +# DO_NOT_TRACK=1 # Enterprise: a public demo instance (website/docs/run/public-demo.md): PUBLIC_DEMO=true # refuses whatever would reach outside the instance, and with # DEPLOYMENT_MODE=hosted IDLE_ORG_RETENTION_DAYS (Enterprise) deletes organizations nobody diff --git a/backend/openapi.json b/backend/openapi.json index e9386257..03c7365c 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -31530,7 +31530,7 @@ "type": "object" }, "TelemetryStatusResponse": { - "description": "``GET /platform/settings/telemetry``: the opt-in usage ping, and the last one sent.", + "description": "``GET /platform/settings/telemetry``: the usage ping, and the last one sent.", "properties": { "enabled": { "title": "Enabled", @@ -35412,7 +35412,7 @@ }, "/api/v1/platform/settings/telemetry": { "get": { - "description": "The opt-in usage ping: whether it is sent, where to, and exactly what it last held.", + "description": "The usage ping: whether it is sent (and if not, why), where to, and what it last held.", "operationId": "get_telemetry_api_v1_platform_settings_telemetry_get", "responses": { "200": { diff --git a/backend/src/tripl/api/v1/platform_settings.py b/backend/src/tripl/api/v1/platform_settings.py index fb69a783..fea988c8 100644 --- a/backend/src/tripl/api/v1/platform_settings.py +++ b/backend/src/tripl/api/v1/platform_settings.py @@ -99,5 +99,5 @@ async def get_telemetry( session: SessionDep, _current_user: PlatformAdminUserDep, ) -> TelemetryStatusResponse: - """The opt-in usage ping: whether it is sent, where to, and exactly what it last held.""" + """The usage ping: whether it is sent (and if not, why), where to, and what it last held.""" return TelemetryStatusResponse.model_validate(await telemetry_service.status(session)) diff --git a/backend/src/tripl/config.py b/backend/src/tripl/config.py index 60dcce0f..e43f5598 100644 --- a/backend/src/tripl/config.py +++ b/backend/src/tripl/config.py @@ -324,11 +324,14 @@ class Settings(BaseSettings): oidc_button_label: str = "Sign in with OpenID Connect" oidc_allowed_domains: Annotated[list[str], NoDecode] = Field(default_factory=list) oidc_auto_create_users: bool = True - # Opt-in anonymous usage telemetry (services/telemetry_service.py): off - # unless set; then one small JSON ping a day — instance id, version, - # edition, warehouse engines, bucketed counts — and never names, emails, - # hosts, queries or data. website/docs/run/telemetry.md lists every field. - telemetry_enabled: bool = False + # Anonymous usage telemetry (services/telemetry_service.py): one small JSON + # ping a day — instance id, version, edition, warehouse engines, bucketed + # counts — and never names, emails, hosts, queries or data. + # website/docs/run/telemetry.md lists every field. Unset means the + # edition's default: on in Community, off in Enterprise. DO_NOT_TRACK + # (the cross-tool opt-out) turns it off whatever this says. + telemetry_enabled: bool | None = None + do_not_track: bool = False telemetry_endpoint: str = "https://telemetry.tripl.io/v1/ping" # AI features (LLM-powered descriptions, Q&A). Disabled by default because diff --git a/backend/src/tripl/main.py b/backend/src/tripl/main.py index 48676c9e..b4f7fe88 100644 --- a/backend/src/tripl/main.py +++ b/backend/src/tripl/main.py @@ -41,6 +41,7 @@ from tripl.middleware.request_id import bound_request_id, request_id_from_scope # noqa: E402 from tripl.middleware.security_headers import build_security_headers # noqa: E402 from tripl.observability.metrics import render_metrics # noqa: E402 +from tripl.services import telemetry_service # noqa: E402 # Configure logging now (after overrides are applied) so every log line — including # those emitted while building the app and importing routers, before the async @@ -60,6 +61,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # An extension's cipher (a key management service, say) proves itself now # rather than at the first secret a request reads. Default cipher: no-op. crypto.check_cipher() + telemetry_service.log_startup_notice() logger.info( "tripl starting", extra={ diff --git a/backend/src/tripl/schemas/app_settings.py b/backend/src/tripl/schemas/app_settings.py index 76a29ee5..0e62a083 100644 --- a/backend/src/tripl/schemas/app_settings.py +++ b/backend/src/tripl/schemas/app_settings.py @@ -630,10 +630,11 @@ class OrgTrackerDefaultsUpdate(BaseModel): class TelemetryStatusResponse(BaseModel): - """``GET /platform/settings/telemetry``: the opt-in usage ping, and the last one sent.""" + """``GET /platform/settings/telemetry``: the usage ping, and the last one sent.""" enabled: bool - #: Why nothing is sent (``disabled``, ``no endpoint``, ``public demo``); null when it is. + #: Why nothing is sent (``do not track``, ``disabled``, ``enterprise default``, + #: ``no endpoint``, ``public demo``); null when it is. reason: str | None = None endpoint: str instance_id: str | None = None diff --git a/backend/src/tripl/services/telemetry_service.py b/backend/src/tripl/services/telemetry_service.py index d4a72845..7a57d466 100644 --- a/backend/src/tripl/services/telemetry_service.py +++ b/backend/src/tripl/services/telemetry_service.py @@ -1,8 +1,10 @@ -"""Opt-in anonymous usage telemetry: one small ping a day. +"""Anonymous usage telemetry: one small ping a day. -Off unless the operator turns it on (``TELEMETRY_ENABLED=true``; ``tripl -install`` asks once). Then the ``send-telemetry`` beat task POSTs one JSON -document a day to ``TELEMETRY_ENDPOINT``. What it holds is everything there is +On by default in Community, off by default in Enterprise; ``TELEMETRY_ENABLED`` +says otherwise, and ``DO_NOT_TRACK`` turns it off whatever that says. The API +logs at startup that it is on, and how to turn it off. While it is on, the +``send-telemetry`` beat task POSTs one JSON document a day to +``TELEMETRY_ENDPOINT``. What it holds is everything there is (``website/docs/run/telemetry.md`` lists it, and **Settings → Platform → Runtime** shows the last one sent): @@ -72,10 +74,17 @@ def edition() -> str: return "enterprise" if extensions.extensions() else "community" +TELEMETRY_DOCS_URL = "https://docs.tripl.io/run/telemetry" + + def inactive_reason() -> str | None: """Why nothing is sent, or None when the ping goes out.""" - if not settings.telemetry_enabled: + if settings.do_not_track: + return "do not track" + if settings.telemetry_enabled is False: return "disabled" + if settings.telemetry_enabled is None and edition() == "enterprise": + return "enterprise default" if not settings.telemetry_endpoint: return "no endpoint" if tenancy.public_demo(): @@ -83,6 +92,19 @@ def inactive_reason() -> str | None: return None +def log_startup_notice() -> None: + """Say plainly, once at startup, that the ping is on and how to turn it off.""" + if inactive_reason() is not None: + return + logger.warning( + "Anonymous usage telemetry is ON: one small ping a day to %s - no names, " + "emails, hosts, queries or data. What it holds: %s. " + "Turn it off with TELEMETRY_ENABLED=false (or DO_NOT_TRACK=1) and restart.", + settings.telemetry_endpoint, + TELEMETRY_DOCS_URL, + ) + + def _operator_row() -> Any: return select(AppSetting).where( AppSetting.key == SETTING_KEY, AppSetting.organization_id.is_(None) diff --git a/backend/src/tripl/tests/test_telemetry.py b/backend/src/tripl/tests/test_telemetry.py index fa50f7fd..5b8e45fc 100644 --- a/backend/src/tripl/tests/test_telemetry.py +++ b/backend/src/tripl/tests/test_telemetry.py @@ -1,13 +1,15 @@ -"""Opt-in anonymous usage telemetry (``telemetry_service``). +"""Anonymous usage telemetry (``telemetry_service``). -Off by default; on, one ping a day with nothing that names anyone, counts only -as ranges, and the last one kept for the operator to read. A public demo -sends nothing, and a receiver that is down changes nothing. +On by default in Community, off in Enterprise and under DO_NOT_TRACK; on, one +ping a day with nothing that names anyone, counts only as ranges, and the last +one kept for the operator to read. A public demo sends nothing, and a receiver +that is down changes nothing. """ from __future__ import annotations import json +import logging from datetime import UTC, datetime, timedelta from typing import Any @@ -69,13 +71,52 @@ def test_the_edition_is_enterprise_once_an_extension_loads() -> None: assert telemetry_service.edition() == "enterprise" -async def test_off_by_default_nothing_is_sent_or_stored(receiver: Receiver) -> None: - assert settings.telemetry_enabled is False +async def test_turned_off_nothing_is_sent_or_stored( + receiver: Receiver, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(settings, "telemetry_enabled", False) assert await _send() == {"sent": False, "reason": "disabled"} assert receiver.pings == [] assert await _rows() == [] +def test_unset_it_is_on_in_community_and_off_in_enterprise( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(settings, "telemetry_enabled", None) + with override_extensions([]): + assert telemetry_service.inactive_reason() is None + with override_extensions([Extension()]): + assert telemetry_service.inactive_reason() == "enterprise default" + monkeypatch.setattr(settings, "telemetry_enabled", True) + assert telemetry_service.inactive_reason() is None + + +def test_do_not_track_wins_over_everything(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "telemetry_enabled", True) + monkeypatch.setattr(settings, "do_not_track", True) + assert telemetry_service.inactive_reason() == "do not track" + + +def test_startup_says_it_is_on_and_how_to_turn_it_off( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + monkeypatch.setattr(settings, "telemetry_enabled", True) + with caplog.at_level(logging.WARNING, logger=telemetry_service.__name__): + telemetry_service.log_startup_notice() + (record,) = caplog.records + message = record.getMessage() + assert "telemetry is ON" in message + assert "TELEMETRY_ENABLED=false" in message + assert "DO_NOT_TRACK=1" in message + assert "https://docs.tripl.io/run/telemetry" in message + + caplog.clear() + monkeypatch.setattr(settings, "telemetry_enabled", False) + telemetry_service.log_startup_notice() + assert caplog.records == [] + + async def test_a_ping_names_nobody_and_is_kept_for_the_operator( client: AsyncClient, receiver: Receiver, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -139,7 +180,8 @@ async def test_a_public_demo_sends_nothing( assert receiver.pings == [] -async def test_only_a_platform_admin_reads_it() -> None: +async def test_only_a_platform_admin_reads_it(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "telemetry_enabled", False) world = await build_world() try: assert (await world.bob.get(STATUS)).status_code == 403 diff --git a/backend/src/tripl/worker/celery_app.py b/backend/src/tripl/worker/celery_app.py index 0be685dc..ec5a6868 100644 --- a/backend/src/tripl/worker/celery_app.py +++ b/backend/src/tripl/worker/celery_app.py @@ -161,7 +161,7 @@ }, "send-telemetry": { "task": "tripl.worker.tasks.telemetry.send_telemetry", - # Daily: the opt-in usage ping (TELEMETRY_ENABLED); a no-op otherwise. + # Daily: the usage ping (telemetry_service.inactive_reason); a no-op when off. # Its counts are day-scale, so once a day says all there is. "schedule": crontab(hour=7, minute=13), }, diff --git a/backend/src/tripl/worker/tasks/telemetry.py b/backend/src/tripl/worker/tasks/telemetry.py index 03c68d07..82fdabf7 100644 --- a/backend/src/tripl/worker/tasks/telemetry.py +++ b/backend/src/tripl/worker/tasks/telemetry.py @@ -1,4 +1,4 @@ -"""The daily opt-in usage ping (``telemetry_service``): a no-op unless enabled.""" +"""The daily usage ping (``telemetry_service``): a no-op while it is off.""" from __future__ import annotations diff --git a/cli/src/tripl_cli/commands/install.py b/cli/src/tripl_cli/commands/install.py index 8b23966d..b8e04de5 100644 --- a/cli/src/tripl_cli/commands/install.py +++ b/cli/src/tripl_cli/commands/install.py @@ -37,7 +37,7 @@ from tripl_cli.errors import EXIT_FAILURE, EXIT_OK, TriplConfigError, TriplError from tripl_cli.install import docker, files, secrets from tripl_cli.install.health import HealthOutcome, read_bootstrap, wait_for_health -from tripl_cli.install.plan import APPEND, FileWrite, InstallPlan +from tripl_cli.install.plan import APPEND, CREATE, FileWrite, InstallPlan from tripl_cli.install.render import ( render_directory_header, render_health, @@ -132,11 +132,13 @@ def register( parser.add_argument( "--telemetry", dest="telemetry", - action="store_true", + action=argparse.BooleanOptionalAction, + default=None, help=( - "share anonymous usage telemetry: one small ping a day, no names, emails, hosts " - "or data (TELEMETRY_ENABLED in the new .env; default: off). Asked once, here: an " - "existing .env is never rewritten" + "anonymous usage telemetry: one small ping a day, no names, emails, hosts or " + "data. Neither flag leaves the edition's default (on in community, off in " + "enterprise); either writes TELEMETRY_ENABLED into the new .env. Asked once, " + "here: an existing .env is never rewritten" ), ) add_wait_flag(parser) @@ -375,7 +377,7 @@ def run_install(args: argparse.Namespace, config: Config) -> int: generated_at=generated_at, values=values, force=bool(args.force), - telemetry=bool(args.telemetry), + telemetry=args.telemetry, ) # What will be TRUE when this finishes, which on a re-run is the existing # .env's values - that file is never overwritten. Everything downstream (the @@ -438,6 +440,20 @@ def run_install(args: argparse.Namespace, config: Config) -> int: "with the credentials that came with your subscription before the pull.", file=sys.stderr, ) + creating_env = any( + write.action == CREATE and write.path.name == files.ENV_NAME for write in writes + ) + telemetry_on = args.telemetry is True or ( + args.telemetry is None and plan.image != files.ENTERPRISE_IMAGE + ) + if creating_env and telemetry_on: + print( + "tripl: note: anonymous usage telemetry is on: one small ping a day, no names, " + f"emails, hosts or data ({files.TELEMETRY_DOCS_URL}). Turn it off with " + "TELEMETRY_ENABLED=false in .env, or re-run with --no-telemetry before the " + ".env exists.", + file=sys.stderr, + ) if plan.insecure_scheme: # Loud, and not a refusal: an operator terminating TLS elsewhere or # evaluating on a laptop has a legitimate http:// origin. diff --git a/cli/src/tripl_cli/install/files.py b/cli/src/tripl_cli/install/files.py index 45a570df..d6b421d9 100644 --- a/cli/src/tripl_cli/install/files.py +++ b/cli/src/tripl_cli/install/files.py @@ -173,7 +173,7 @@ def render_env( version: str, generated_at: datetime, values: Mapping[str, str], - telemetry: bool = False, + telemetry: bool | None = None, ) -> str: """The generated ``.env``, in full. @@ -218,7 +218,10 @@ def render_env( "", "# Anonymous usage telemetry: one small ping a day, no names, emails, hosts or", f"# data. What it holds: {TELEMETRY_DOCS_URL}", - env_line("TELEMETRY_ENABLED", "true" if telemetry else "false"), + "# Unset, it is on in the community edition and off in enterprise.", + env_line("TELEMETRY_ENABLED", "true" if telemetry else "false") + if telemetry is not None + else "# TELEMETRY_ENABLED=false", ] ) return "\n".join(lines) + "\n" @@ -249,7 +252,7 @@ def plan_env( version: str, generated_at: datetime, values: Mapping[str, str], - telemetry: bool = False, + telemetry: bool | None = None, ) -> FileWrite: """Create it, append to it, or leave it entirely alone. Never overwrite.""" path = directory / ENV_NAME @@ -369,7 +372,7 @@ def plan_writes( generated_at: datetime, values: Mapping[str, str], force: bool = False, - telemetry: bool = False, + telemetry: bool | None = None, ) -> tuple[FileWrite, ...]: """The three files, in the order they are reported and applied.""" return ( diff --git a/cli/src/tripl_cli/install/files/compose.yaml b/cli/src/tripl_cli/install/files/compose.yaml index dd6b607d..f2adf234 100644 --- a/cli/src/tripl_cli/install/files/compose.yaml +++ b/cli/src/tripl_cli/install/files/compose.yaml @@ -95,6 +95,7 @@ x-app-environment: &app-environment OIDC_AUTO_CREATE_USERS: ${OIDC_AUTO_CREATE_USERS:-} TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-} TELEMETRY_ENDPOINT: ${TELEMETRY_ENDPOINT:-} + DO_NOT_TRACK: ${DO_NOT_TRACK:-} PUBLIC_DEMO: ${PUBLIC_DEMO:-} # The Enterprise license key (read by the Enterprise image only). LICENSE_KEY: ${LICENSE_KEY:-} diff --git a/cli/tests/test_install_cmd.py b/cli/tests/test_install_cmd.py index f5423a43..ea5db597 100644 --- a/cli/tests/test_install_cmd.py +++ b/cli/tests/test_install_cmd.py @@ -445,6 +445,30 @@ def test_an_explicit_version_gets_no_pin_reminder( assert "pin a released tag" not in captured.err +@pytest.mark.parametrize( + ("extra", "said"), + [ + ((), True), + (("--no-telemetry",), False), + (("--edition", "enterprise"), False), + (("--edition", "enterprise", "--telemetry"), True), + ], +) +def test_a_new_env_says_when_telemetry_is_on_and_how_to_turn_it_off( + install_dir: Path, + fake_runner: FakeRunner, + capsys: pytest.CaptureFixture[str], + extra: tuple[str, ...], + said: bool, +) -> None: + assert main(argv(install_dir, *extra, "--dry-run")) == 0 + + err = capsys.readouterr().err + assert ("anonymous usage telemetry is on" in err) is said + if said: + assert "TELEMETRY_ENABLED=false" in err + + def test_no_start_writes_the_files_and_starts_nothing( install_dir: Path, fake_runner: FakeRunner, capsys: pytest.CaptureFixture[str] ) -> None: diff --git a/cli/tests/test_install_files.py b/cli/tests/test_install_files.py index 2e8be62d..16cdab46 100644 --- a/cli/tests/test_install_files.py +++ b/cli/tests/test_install_files.py @@ -96,7 +96,8 @@ def test_the_generated_env_is_exactly_this(install_dir: Path) -> None: "\n" "# Anonymous usage telemetry: one small ping a day, no names, emails, hosts or\n" "# data. What it holds: https://docs.tripl.io/run/telemetry\n" - "TELEMETRY_ENABLED=false\n" + "# Unset, it is on in the community edition and off in enterprise.\n" + "# TELEMETRY_ENABLED=false\n" ) @@ -571,14 +572,18 @@ def test_world_readable_mode_reports_only_loose_modes(install_dir: Path) -> None assert files.world_readable_mode(path) is None -def test_install_telemetry_writes_it_on_and_only_into_a_new_env() -> None: - """``tripl install --telemetry`` asks once: into the .env it creates.""" +@pytest.mark.parametrize( + ("telemetry", "line"), + [(True, "TELEMETRY_ENABLED=true"), (False, "TELEMETRY_ENABLED=false")], +) +def test_install_telemetry_flags_write_it_into_a_new_env(telemetry: bool, line: str) -> None: + """``--telemetry`` / ``--no-telemetry`` ask once: into the .env it creates.""" rendered = files.render_env( app_base_url="https://tripl.example.com", image=files.DEFAULT_IMAGE, version="1.2.3", generated_at=datetime(2026, 1, 1, tzinfo=UTC), values={name: "x" * 44 for name in files.REQUIRED_SECRETS}, - telemetry=True, + telemetry=telemetry, ) - assert rendered.endswith("TELEMETRY_ENABLED=true\n") + assert rendered.endswith(f"{line}\n") diff --git a/compose.dev.yaml b/compose.dev.yaml index d7702243..b3f72448 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -24,6 +24,8 @@ x-backend-environment: &backend-environment HSTS_ENABLED: ${HSTS_ENABLED:-false} CONTENT_SECURITY_POLICY: ${CONTENT_SECURITY_POLICY:-} RATE_LIMIT_ENABLED: ${RATE_LIMIT_ENABLED:-true} + # A development stack is not an install: no usage ping unless asked for. + TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-false} RATE_LIMIT_LOGIN_PER_MINUTE: ${RATE_LIMIT_LOGIN_PER_MINUTE:-5} RATE_LIMIT_REGISTER_PER_HOUR: ${RATE_LIMIT_REGISTER_PER_HOUR:-3} LOG_LEVEL: ${LOG_LEVEL:-INFO} diff --git a/compose.yaml b/compose.yaml index fa277cfd..357448a3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -95,6 +95,7 @@ x-app-environment: &app-environment OIDC_AUTO_CREATE_USERS: ${OIDC_AUTO_CREATE_USERS:-} TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-} TELEMETRY_ENDPOINT: ${TELEMETRY_ENDPOINT:-} + DO_NOT_TRACK: ${DO_NOT_TRACK:-} PUBLIC_DEMO: ${PUBLIC_DEMO:-} # The Enterprise license key (read by the Enterprise image only). LICENSE_KEY: ${LICENSE_KEY:-} diff --git a/frontend/e2e/usage-telemetry.spec.ts b/frontend/e2e/usage-telemetry.spec.ts index ea9a5e51..e601fbe2 100644 --- a/frontend/e2e/usage-telemetry.spec.ts +++ b/frontend/e2e/usage-telemetry.spec.ts @@ -1,8 +1,9 @@ import { expect, signInAsOwner, test } from './fixtures' /** - * Usage telemetry is opt-in: on the CI stack nothing turns it on, and the - * platform admin reads that — and where it would go — under Runtime. + * Usage telemetry is on by default in Community, but the dev stack CI runs + * (compose.dev.yaml) turns it off; the platform admin reads that — and where it + * would go — under Runtime. */ test('a platform admin sees usage telemetry is off and where it would go', async ({ page }) => { await signInAsOwner(page) @@ -16,6 +17,6 @@ test('a platform admin sees usage telemetry is off and where it would go', async await page.goto('/settings/instance/runtime') // A route's first visit compiles it on the dev server: give it time. await expect(page.getByText('Usage telemetry')).toBeVisible({ timeout: 60_000 }) - await expect(page.getByText('Off (TELEMETRY_ENABLED is not set)')).toBeVisible() + await expect(page.getByText('Off (TELEMETRY_ENABLED=false)')).toBeVisible() await expect(page.getByText('https://telemetry.tripl.io/v1/ping')).toBeVisible() }) diff --git a/frontend/src/api/serviceSettings.ts b/frontend/src/api/serviceSettings.ts index 7f7a3f08..f12668ed 100644 --- a/frontend/src/api/serviceSettings.ts +++ b/frontend/src/api/serviceSettings.ts @@ -10,7 +10,7 @@ import type { /** The built-in AI system prompts, for "Restore default". */ export type AiPromptDefaults = components['schemas']['AiPromptDefaultsResponse'] -/** The opt-in usage ping: on or off, where to, and exactly what it last sent. */ +/** The usage ping: on or off, where to, and exactly what it last sent. */ export type TelemetryStatus = components['schemas']['TelemetryStatusResponse'] /** The row caps a scan falls back to, readable by any member of the organization. */ diff --git a/frontend/src/pages/settings-service/TelemetryCard.test.tsx b/frontend/src/pages/settings-service/TelemetryCard.test.tsx index 0a1ea24a..8cf5d8a4 100644 --- a/frontend/src/pages/settings-service/TelemetryCard.test.tsx +++ b/frontend/src/pages/settings-service/TelemetryCard.test.tsx @@ -33,11 +33,19 @@ afterEach(() => { describe('TelemetryCard', () => { it('says it is off, and why', async () => { renderCard(OFF) - expect(await screen.findByText('Off (TELEMETRY_ENABLED is not set)')).toBeInTheDocument() + expect(await screen.findByText('Off (TELEMETRY_ENABLED=false)')).toBeInTheDocument() expect(screen.getByText('Never')).toBeInTheDocument() expect(screen.queryByTestId('telemetry-payload')).toBeNull() }) + it.each([ + ['do not track', 'Off (DO_NOT_TRACK is set)'], + ['enterprise default', 'Off (Enterprise default; TELEMETRY_ENABLED=true turns it on)'], + ])('names %s as the reason', async (reason, label) => { + renderCard({ ...OFF, reason }) + expect(await screen.findByText(label)).toBeInTheDocument() + }) + it('shows exactly what it last sent', async () => { renderCard({ ...OFF, diff --git a/frontend/src/pages/settings-service/TelemetryCard.tsx b/frontend/src/pages/settings-service/TelemetryCard.tsx index acdbd328..a7bdf130 100644 --- a/frontend/src/pages/settings-service/TelemetryCard.tsx +++ b/frontend/src/pages/settings-service/TelemetryCard.tsx @@ -6,13 +6,15 @@ import { SILENT_ERROR_META } from '@/lib/errorFeedback' import { telemetryStatusKey } from '@/lib/queryKeys' const REASON: Record = { - disabled: 'Off (TELEMETRY_ENABLED is not set)', + 'do not track': 'Off (DO_NOT_TRACK is set)', + disabled: 'Off (TELEMETRY_ENABLED=false)', + 'enterprise default': 'Off (Enterprise default; TELEMETRY_ENABLED=true turns it on)', 'no endpoint': 'Off (TELEMETRY_ENDPOINT is empty)', 'public demo': 'Off (a public demo never sends it)', } /** - * Settings › Platform › Runtime: the opt-in usage ping (C2), read-only. Whether + * Settings › Platform › Runtime: the usage ping (C2), read-only. Whether * it is sent and where, and the exact document it last sent — what * website/docs/run/telemetry.md lists, nothing more. */ @@ -27,7 +29,7 @@ export function TelemetryCard() { return (