Skip to content

Telemetry & error reporting

Pythinker Code can emit anonymous telemetry to help us spot crashes, regressions, and silent failures. Telemetry is on by default; this page documents exactly what's collected, where it's sent, and how to turn it off.

Backends

StackEndpointWhat it carries
OpenTelemetry → SigNoz (logs / metrics / traces)https://otel.pythinker.comStructured events emitted via track(...), periodic metric snapshots, and (when sampled) trace spans.
Sentry-compatible Bugsink (errors)https://errors.pythinker.com/1Unhandled exceptions, asyncio task failures, and explicitly-reported handled exceptions.
Hosted feedback endpoint<platform>/feedbackFree-form text submitted via the /feedback slash command.

The OTel collector and Bugsink are pythinker-operated. The OTel ingest token embedded in the binary is public by design (same pattern used by Datadog RUM and PostHog public keys); rate limiting and PII scrubbing happen server-side at the collector edge.

What's collected

OTel events

Emitted from pythinker_code.telemetry.track(event_name, **properties). Every event carries:

FieldSource
event_idUUIDv4 generated client-side.
device_idStable per-install hash (no user identity).
session_idPer-process random ID.
eventThe event name (e.g. session_started, feedback_submitted, error).
properties.*Primitive enum-like attributes the call site passed.
context.*Static attributes added by the sink (version, OS, Python, locale).
timestampWall-clock time on the client.

Property values must be primitives (bool/int/float/str/None). The call sites are coded to never pass user input, file paths, or code snippets.

Sentry events

Emitted from pythinker_code.telemetry.sentry.capture_exception(exc) (and implicitly via sys.excepthook and the asyncio exception handler). Carries:

  • Exception class, message, and stack trace.
  • Release tag (pythinker-code@<version>).
  • Lifecycle phase (startup / runtime / shutdown).
  • A handful of static tags (ui_mode, model).

The before_send hook in telemetry/sentry.py:

  • Strips file-path prefixes above site-packages/ or pythinker_code/, collapsing them to <env>/ so home directories don't leak.
  • Removes server_name (which is usually the user's hostname).
  • Disables send_default_pii and request-frame variable capture.

Reported handled errors

pythinker_code.telemetry.errors.report_handled_error(exc, *, site, tool=None, **attrs) is the canonical helper for any except Exception: block that catches an exception, renders a graceful failure to the user (ToolError, red TUI line, warning), and currently has no monitoring visibility.

It:

  1. Emits an OTel error event with {site, exc_class, expected, tool, **attrs}.
  2. Calls sentry.capture_exception(exc) only when the error is not expected. Expected user-environment failures — bad/expired credentials, exhausted quotas, rate limits, request timeouts, offline network, abandoned OAuth flows, MCP servers lacking an optional capability (see errors.is_expected_error) — still flow to the OTel error stream with expected=True, but are withheld from Sentry/Bugsink, which is reserved for actionable defects.

Both calls are wrapped in contextlib.suppress(Exception) so monitoring can never break the host program.

Common site values (extend with care — these are dashboard query keys):

SiteWhere
tool.readtools/file/read.py
tool.read_mediatools/file/read_media.py
tool.writetools/file/write.py
tool.replacetools/file/replace.py
tool.globtools/file/glob.py
tool.greptools/file/grep_local.py
tool.grep.rg_exectools/file/grep_local.py (ripgrep subprocess failure)
tool.shell.exectools/shell/__init__.py (foreground command)
tool.shell.background_starttools/shell/__init__.py (background spawn)
tool.agent.foregroundtools/agent/__init__.py
tool.ask_usertools/ask_user/__init__.py
tool.plan.entertools/plan/enter.py
tool.plan.exittools/plan/__init__.py
tool.plan.handofftools/plan/__init__.py (ExitPlanMode handoff)
auth.keyring.readauth/oauth.py (_load_from_keyring)
auth.oauth.device_authorizeauth/oauth.py (device-code request)
auth.oauth.device_pollauth/oauth.py (device-code poll loop)
auth.models.fetchauth/oauth.py (list_models post-login)
auth.oauth.refreshauth/oauth.py (foreground token refresh)
auth.oauth.refresh.backgroundauth/oauth.py (background refresh loop)
auth.platforms.refresh.pre_syncauth/platforms.py (pre-sync refresh)
auth.platforms.refresh.after_401auth/platforms.py (refresh-on-401)
auth.platforms.syncauth/platforms.py (model sync fallback)
auth.openai.discover_chatgpt_modelsauth/openai/login.py
auth.openai.browser_loginauth/openai/login.py
auth.openai.device_startauth/openai/login.py
auth.openai.device_pollauth/openai/login.py
soul.btw.executesoul/btw.py (side question)
soul.btw.run_wiresoul/btw.py (wire-based side question)
soul.toolset.register_externalsoul/toolset.py (external tool registration)
soul.toolset.mcp.connectsoul/toolset.py (MCP server connect)
soul.toolset.mcp.callsoul/toolset.py (MCP tool call)
soul.toolset.mcp.call.timeoutsoul/toolset.py (MCP tool call — timeout-classified)
soul.toolset.external_toolsoul/toolset.py (WireExternalTool)
soul.injection.getsoul/pythinkersoul.py (injection provider get)
soul.injection.on_context_compactedsoul/pythinkersoul.py
soul.injection.on_auto_changedsoul/pythinkersoul.py
soul.context.compactsoul/pythinkersoul.py (compaction inside step)
soul.context.prunesoul/pythinkersoul.py (context prune failure)
soul.step.errorsoul/pythinkersoul.py (any step exception)
soul.chat.recoversoul/pythinkersoul.py (provider recovery)
soul.deliberation.advisorsoul/deliberation.py (advisor deliberation failure)
acp.session.promptacp/session.py
acp.session.approvalacp/session.py
acp.host.terminalacp/host.py
hooks.runnerhooks/runner.py
hooks.engine.runhooks/engine.py (top-level fail-open wrap)
hooks.engine.triggered_cbhooks/engine.py (HookTriggered callback)
hooks.engine.resolved_cbhooks/engine.py (HookResolved callback)
hooks.engine.wirehooks/engine.py (wire hook dispatch)
subagents.run.soulsubagents/runner.py (soul-run wrapper)
subagents.run.backgroundsubagents/runner.py (background failure)

When you add a new catch site, prefer reusing an existing prefix and add a specific suffix. Do not put free-form messages in site — it must remain a small, stable enumeration for dashboard slicing.

/feedback slash command

Distinct from automatic telemetry. The user types feedback explicitly; it is POSTed to the configured feedback endpoint along with session_id, version, OS, and current model. No code or file context is attached.

Endpoint resolution order:

  1. PYTHINKER_FEEDBACK_URL environment variable.
  2. feedback.endpoint_url in config.toml.
  3. The built-in Pythinker platform endpoint.

When using a configured endpoint, set PYTHINKER_FEEDBACK_API_KEY or feedback.api_key to send an Authorization: Bearer ... header. feedback.custom_headers can add static headers. The built-in Pythinker provider remains supported but OAuth is no longer required for self-hosted/dev feedback routing.

/report-error slash command

A user-invoked complement to automatic Sentry/OTel capture, designed for the case where the user knows something is wrong but the code path didn't raise — or where they want to attach a comment to a recently-seen failure.

The runtime keeps a process-local ring buffer of the last 10 errors that were forwarded through report_handled_error (pythinker_code/telemetry/errors.py). Each entry stores: timestamp, site, exc_class, a 200-char-truncated message, and the optional tool name. The buffer is in-memory only — it does not persist across processes and does not back the OTel/Sentry pipeline (those already received the full event when it occurred).

When the user runs /report-error:

  1. The TUI prints the buffer's contents (site + class + tool).
  2. The user types a free-form description and hits enter.
  3. The runtime POSTs to <platform>/feedback with type=error, the comment, the same context as /feedback (version, OS, model, session_id), and a recent_errors[] array constructed from the ring buffer.
  4. On success, the buffer is cleared.

If submission is unavailable or fails, the slash falls back to opening the GitHub issue tracker in a browser.

Turning it off

Telemetry is on by default. Use any of the following to turn off Sentry and OTel emission:

  • pythinker --no-telemetry — disables it for that invocation.
  • PYTHINKER_DISABLE_TELEMETRY=1 — set before launching pythinker; kills both Sentry and OTel emission for the process. The /feedback slash command is not affected (it only fires on explicit user invocation).
  • config.telemetry = false in your config file (TOML) — same effect, persisted across runs.

Explicit feedback settings are independent of automatic telemetry:

  • PYTHINKER_FEEDBACK_URL — overrides where explicit /feedback and /report-error submissions are sent.
  • PYTHINKER_FEEDBACK_API_KEY — optional bearer token for that feedback endpoint.

Override individual endpoints when running your own collectors:

VariableDefault
PYTHINKER_SENTRY_DSNhttps://...@errors.pythinker.com/1
PYTHINKER_OTEL_ENDPOINThttps://otel.pythinker.com
PYTHINKER_OTEL_TOKEN(embedded)
PYTHINKER_OTEL_TRACE_SAMPLE_RATE1.0 (always-on)

PYTHINKER_OTEL_TRACE_SAMPLE_RATE accepts a float in [0.0, 1.0]. Values are clamped; malformed input falls back to the default. Sampling is ParentBased on top of TraceIdRatioBased, so a parent's sample decision (e.g. an upstream ACP/wire request that already chose to sample) is honored — otherwise the rate decides at the root.

Spans

Currently emitted (when traces are sampled):

SpanWhereKey attributes
pythinker.turnsoul/pythinkersoul.pysession.id, agent.role, model, plan_mode, turn.stop_reason, turn.step_count
pythinker.llmsoul/pythinkersoul.pygen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.response.id, llm.tool_calls
pythinker.toolsoul/toolset.pytool.name, tool.success, tool.error_type
pythinker.mcp.callsoul/toolset.pymcp.server, mcp.tool, mcp.timeout_ms, mcp.is_error

What's not collected

  • Prompts, completions, or any LLM input/output.
  • File contents, paths above site-packages/ (Sentry path-scrubbing), or command arguments.
  • Local environment variable values.
  • The user's hostname or username.
  • Anything from the /feedback slash command unless the user explicitly typed it.