Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions solutions/ess-maker-skills/scripts/adk_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,8 +600,40 @@ def _emit_sync(event_name: str, data: dict[str, Any]) -> dict[str, Any]:
"event": event_name, "reason": f"{type(e).__name__}: {e}"}


def _notice_before_emit() -> None:
"""Show the one-time telemetry notice on the first emit from this install.

Called from ``_emit`` before any dispatch so the maker sees the consent
text *before* any network round-trip has a chance to leak data. Skipped
when telemetry is already opted out — no point telling someone who
turned it off how to turn it off, and we won't be sending anyway.

Idempotent via the persisted ``noticeShown`` config flag written by
``maybe_print_notice`` (or by the installer's ``Show-EssTelemetryNotice``
at install-time), so this is a cheap no-op after the first run.
Best-effort: any IO error is swallowed — a notice-config write must
never break a caller's flow.
"""
try:
if not telemetry_enabled():
return
maybe_print_notice()
except Exception: # noqa: BLE001 - notice must never break a caller
pass


def _emit(event_name: str, data: dict[str, Any], *, block: bool = False) -> dict[str, Any]:
"""Dispatch an emit. Async (daemon thread) unless sync/block requested."""
# Notice MUST fire before we dispatch so the maker has a realistic chance
# to see the opt-out instructions before the very first event leaves this
# install. Installer-bootstrapped users already have ``noticeShown=true``
# in ``~/.adk/config`` (set by ``setup/telemetry/install-telemetry.ps1``'s
# ``Show-EssTelemetryNotice``) so this is a cheap no-op for them; the
# direct-clone bypass path (git-clone without running the installer) is
# the case this call actually catches — previously the notice was printed
# deep inside ``scripts/setup.py`` well after ``emit_capability_use``
# had already flushed the first event.
_notice_before_emit()
if _SYNC or block:
return _emit_sync(event_name, data)
t = threading.Thread(target=_emit_sync, args=(event_name, data), daemon=True)
Expand Down
52 changes: 52 additions & 0 deletions tests/test_adk_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,58 @@ def test_notice_shown_once(capsys):
assert second is False


def test_emit_shows_notice_before_dispatch(monkeypatch, captured_post):
"""Regression: the opt-out notice MUST reach the maker before the first
network round-trip. Prior order was ``emit -> notice`` inside setup.py,
which meant the direct-clone bypass path (no installer) sent one event
with no disclosure. Fix routes notice through ``_emit`` so every emit
path is disclosed first.
"""
# Fresh install: no ``noticeShown`` flag yet.
order: list[str] = []
real_maybe = adk.maybe_print_notice
real_post = adk._fc._post

def spy_notice(stream=None):
order.append("notice")
return real_maybe(stream=stream)

def spy_post(ikey, envs):
order.append("post")
return real_post(ikey, envs)

monkeypatch.setattr(adk, "maybe_print_notice", spy_notice)
monkeypatch.setattr(adk._fc, "_post", spy_post)

adk.emit_capability_use("setup", block=True)

assert order[:2] == ["notice", "post"], (
f"expected notice before post, got {order}"
)


def test_emit_skips_notice_when_opted_out(monkeypatch):
"""Users who already opted out shouldn't be pestered with the notice
again from the emit path — there's nothing to disclose."""
monkeypatch.setenv("ESS_ADK_TELEMETRY", "off")
called: list[int] = []
monkeypatch.setattr(
adk, "maybe_print_notice", lambda stream=None: called.append(1) or False
)
adk.emit_capability_use("setup", block=True)
assert called == []


def test_emit_notice_swallows_errors(monkeypatch, captured_post):
"""A failure in ``maybe_print_notice`` must never break a caller's emit."""
def boom(stream=None):
raise OSError("disk full")

monkeypatch.setattr(adk, "maybe_print_notice", boom)
res = adk.emit_capability_use("setup", block=True)
assert res.get("sent") is True


def test_disabled_emit_does_not_post(monkeypatch, captured_post):
monkeypatch.setenv("ESS_ADK_TELEMETRY", "off")
res = adk.emit_capability_use("setup", block=True)
Expand Down
Loading