From c9e59311ae18ce90be323cac993ebbab95c56e39 Mon Sep 17 00:00:00 2001 From: Vladislav Denisov Date: Fri, 9 Oct 2026 09:58:23 +0200 Subject: [PATCH 1/2] feat: usage telemetry on by default in Community, said loudly Community instances send the anonymous daily ping unless told not to; Enterprise stays off unless TELEMETRY_ENABLED=true. DO_NOT_TRACK turns it off whatever else says. When it is on, the API logs it at startup with how to turn it off, and tripl install says so when it writes a new .env (which leaves TELEMETRY_ENABLED commented out; --telemetry / --no-telemetry write it). The dev stack keeps it off. Runtime settings name each reason. Co-Authored-By: Claude Opus 5.5 --- .env.example | 6 +- backend/openapi.json | 4 +- backend/src/tripl/api/v1/platform_settings.py | 2 +- backend/src/tripl/config.py | 13 +++-- backend/src/tripl/main.py | 2 + backend/src/tripl/schemas/app_settings.py | 5 +- .../src/tripl/services/telemetry_service.py | 32 +++++++++-- backend/src/tripl/tests/test_telemetry.py | 56 ++++++++++++++++--- backend/src/tripl/worker/celery_app.py | 2 +- backend/src/tripl/worker/tasks/telemetry.py | 2 +- cli/src/tripl_cli/commands/install.py | 28 ++++++++-- cli/src/tripl_cli/install/files.py | 11 ++-- cli/src/tripl_cli/install/files/compose.yaml | 1 + cli/tests/test_install_cmd.py | 24 ++++++++ cli/tests/test_install_files.py | 15 +++-- compose.dev.yaml | 2 + compose.yaml | 1 + frontend/e2e/usage-telemetry.spec.ts | 7 ++- frontend/src/api/serviceSettings.ts | 2 +- .../settings-service/TelemetryCard.test.tsx | 10 +++- .../pages/settings-service/TelemetryCard.tsx | 8 ++- frontend/src/types/api.gen.ts | 4 +- website/docs/run/cli.md | 10 ++-- website/docs/run/configuration.md | 3 +- website/docs/run/telemetry.md | 44 +++++++++++---- 25 files changed, 225 insertions(+), 69 deletions(-) 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 ( Date: Fri, 9 Oct 2026 09:58:48 +0200 Subject: [PATCH 2/2] docs: deployment says Community sends the usage ping and how to stop it Co-Authored-By: Claude Opus 5.5 --- website/docs/run/deployment.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/run/deployment.md b/website/docs/run/deployment.md index 3abbba92..dae32828 100644 --- a/website/docs/run/deployment.md +++ b/website/docs/run/deployment.md @@ -66,6 +66,7 @@ Every flag, the plan output, the file actions and the safety rules live in [`tri - **The `compose.yaml` it writes is the one described [below](#the-compose-stack)**, minus the `mcp` service's `build:` block: a fresh host has no source tree, so `--profile mcp` pulls the published image instead of building it. - **Re-running converges.** A second run leaves `.env` alone, reports the other two files as `unchanged` or `kept`, and still runs `pull` and `up -d` — which is how you apply an edit to `compose.yaml` or a `compose.override.yaml`. - **It stops at a running, empty instance.** The owner account and the warehouse connection are browser steps; see [Connecting a warehouse](#connecting-a-warehouse). +- **Community sends one anonymous usage ping a day** unless you turn it off: `--no-telemetry`, or `TELEMETRY_ENABLED=false` in `.env` later. `install` and the API's startup log both say so. What the ping holds, and what it never does, is in [Telemetry](./telemetry.md). ### The variables the stack needs