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
44 changes: 33 additions & 11 deletions anton/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,29 @@
Where these events actually land
================================

The collector lambda RELAYS into PostHog — verified 2026-08-06 against
project 355390 ("Anton"), where anton's events appear tagged
``source = mindsdb-zoominfo-lambda``. Two consequences worth knowing before
adding a caller:

* **Every ``extra`` kwarg becomes a queryable PostHog event property.** Not
an allowlist — ``ds_connect_success`` carries its ``engine=postgres``
through, and ``turn_completed`` carries its full token breakdown. So the
parameter names here ARE the analytics schema; renaming one silently breaks
whatever queries or dashboards read it.
The collector relays into PostHog project 355390 ("Anton"), where these events
appear tagged ``source = mindshub-analytics-collector``. Rows written before
2026-08-11 carry ``source = mindsdb-zoominfo-lambda`` instead: that was a
different collector, in a different AWS account, and installs old enough to
still hold the previous ``analytics_url`` keep reporting to it. A query
covering both needs to accept either value.

Consequences worth knowing before adding a caller:

* **Every ``extra`` kwarg becomes a queryable PostHog event property**, apart
from a small denylist of credential- and address-shaped names, and any value
that looks like an email address. ``ds_connect_success`` carries its
``engine=postgres`` through and ``turn_completed`` carries its full token
breakdown, with no collector change needed for either. So the parameter
names here ARE the analytics schema; renaming one silently breaks whatever
queries or dashboards read it.
* **This was not true until 2026-08-11.** The previous collector copied a
fixed list of five property names and relayed only actions starting with
a lowercase ``anton_`` or ``ds_connect_``, discarding everything else and
answering HTTP 200 regardless. ``turn_completed`` and its 27 properties
therefore went nowhere at all from the day they shipped. If a property
seems to be missing, check what the collector accepts before assuming the
caller is at fault.
* **``distinct_id`` is the ``aid`` fingerprint, so these events are
per-INSTALL, not per-user.** They do not join to the Keycloak ``sub`` that
the console, the desktop renderer's PostHog client, and the billing mirror
Expand Down Expand Up @@ -82,6 +95,14 @@

_TIMEOUT = 3 # seconds

# Sent instead of urllib's default ``Python-urllib/3.x``, which Cloudflare's bot
# protection answers with 403 on the mindshub.ai zone. The collector host is
# deliberately not proxied, so this is belt and braces rather than the only
# guard, but ``_fire`` discards its response either way: anything that blocks
# this request disappears without a trace, so it is worth not looking like a
# script.
_USER_AGENT = "anton-analytics/1.0"

# Cached after first computation — the fingerprint never changes within
# a process, so computing it once is sufficient.
_cached_aid: str | None = None
Expand Down Expand Up @@ -194,6 +215,7 @@ def send_event(settings: "AntonSettings", action: str, **extra: str) -> None:
def _fire(url: str) -> None:
"""Perform the actual HTTP GET. Runs inside a daemon thread."""
try:
urllib.request.urlopen(url, timeout=_TIMEOUT)
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
urllib.request.urlopen(req, timeout=_TIMEOUT)
except Exception:
pass
4 changes: 3 additions & 1 deletion anton/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ def _map_minds_cloud_to_openai_compatible(cls, v: object) -> object:

# Analytics — anonymous usage events (set ANTON_ANALYTICS_ENABLED=false to opt out)
analytics_enabled: bool = True
analytics_url: str = "https://x6nik28qi6.execute-api.us-east-2.amazonaws.com/default/zoomInfoCollector"
# A hostname we own rather than an API Gateway id, so the collector can move
# without a release to follow it. Override with ANTON_ANALYTICS_URL.
analytics_url: str = "https://collect.mindshub.ai/collect"

# Minds datasource integration
minds_enabled: bool = True # use Minds server as LLM provider
Expand Down
34 changes: 34 additions & 0 deletions tests/test_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,37 @@ def test_send_event_sends_when_not_ci(monkeypatch):
assert params["action"] == "anton_query"
assert params["llm_provider"] == "openai"
assert "is_ci" not in params # flag removed; CI events aren't sent at all


def test_fire_sends_an_identifying_user_agent(monkeypatch):
"""urllib's default ``Python-urllib/3.x`` agent is answered with 403 by the
bot rules on the mindshub.ai zone, and ``_fire`` throws its response away,
so an event blocked for looking like a script would vanish with no trace.
"""
seen: list[object] = []

def _fake_urlopen(request, timeout=None):
seen.append(request)
return None

monkeypatch.setattr(analytics.urllib.request, "urlopen", _fake_urlopen)

analytics._fire("https://example.test/collect?action=anton_query")

assert len(seen) == 1
agent = seen[0].get_header("User-agent")
assert agent == analytics._USER_AGENT
assert "python-urllib" not in agent.lower()


def test_default_collector_url_is_a_host_we_control():
"""Guards the regression this replaced: the previous default was a raw
``*.execute-api.amazonaws.com`` id in an account nobody could deploy to, so
the endpoint could not be changed without shipping a new release to every
install.
"""
from anton.config.settings import AntonSettings

url = AntonSettings.model_fields["analytics_url"].default
assert url.startswith("https://collect.")
assert "execute-api" not in url
Loading