From 10d61363522cbbdc2a991fd420b5ac1d20d46b54 Mon Sep 17 00:00:00 2001 From: Jonathan Singer Date: Mon, 20 Jul 2026 15:04:25 -0400 Subject: [PATCH] Forward viewer email funnel events and cta surface to PostHog --- strix/telemetry/posthog.py | 23 ++++++++++++++++++++--- strix/viewer/server.py | 29 ++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py index 73efaf20..ef3c8d4f 100644 --- a/strix/telemetry/posthog.py +++ b/strix/telemetry/posthog.py @@ -153,12 +153,29 @@ def viewer_opened(source: str, live: bool) -> None: ) -def viewer_cta_clicked(cta: str) -> None: +def viewer_cta_clicked(cta: str, surface: str | None = None) -> None: + props = { + **base_props(), + "cta": cta[:64], + } + if surface: + props["surface"] = surface[:64] + _send("viewer_cta_clicked", props) + + +_VIEWER_EMAIL_STEPS = frozenset( + {"email_submitted", "email_verified", "report_sent", "work_email_required"} +) + + +def viewer_email_event(step: str, purpose: str | None = None) -> None: + if step not in _VIEWER_EMAIL_STEPS: + return _send( - "viewer_cta_clicked", + f"viewer_{step}", { **base_props(), - "cta": cta[:64], + **({"purpose": purpose} if purpose else {}), }, ) diff --git a/strix/viewer/server.py b/strix/viewer/server.py index 6922663b..f556136b 100644 --- a/strix/viewer/server.py +++ b/strix/viewer/server.py @@ -166,15 +166,34 @@ def _make_handler(state: _ViewerState) -> type[BaseHTTPRequestHandler]: return {} return body if isinstance(body, dict) else {} + # Funnel events the viewer is allowed to forward. This handler is the + # trust boundary: only these event names, with only their known props, + # ever reach PostHog. Everything else (including any PII) is dropped. + _EMAIL_EVENTS = frozenset( + {"email_submitted", "email_verified", "report_sent", "work_email_required"} + ) + def _handle_event(self) -> None: body = self._read_body() - # Only the viewer's own sign-up/upsell CTA click is forwarded, as an - # anonymous PostHog event that respects the global telemetry opt-out. - if body.get("event") == "cta_clicked": - cta = str(body.get("cta") or "unknown") + # Forwarded as anonymous PostHog events that respect the global + # telemetry opt-out. Never forward the email, code, or report body: + # only the whitelisted event names and their known props are passed. + event = body.get("event") + if event == "cta_clicked": from strix.telemetry import posthog # noqa: PLC0415 - posthog.viewer_cta_clicked(cta) + cta = str(body.get("cta") or "unknown") + surface = body.get("surface") + posthog.viewer_cta_clicked( + cta, surface=str(surface) if surface else None + ) + elif event in self._EMAIL_EVENTS: + from strix.telemetry import posthog # noqa: PLC0415 + + purpose = body.get("purpose") + posthog.viewer_email_event( + str(event), purpose=str(purpose) if purpose else None + ) self.send_response(HTTPStatus.NO_CONTENT) self.end_headers()