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
31 changes: 31 additions & 0 deletions .github/workflows/build-tui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,36 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Clean Python build artifacts
shell: bash
run: rm -rf build dist *.egg-info

- name: Validate no stale telemetry config
shell: bash
run: |
set -euo pipefail
if [[ -f openswarm_telemetry_config.py ]]; then
echo "openswarm_telemetry_config.py exists before npm CI injection; remove it before publishing." >&2
exit 1
fi
python3 scripts/check_telemetry_artifact.py build dist

- name: Validate telemetry secrets
env:
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${POSTHOG_API_KEY}" ]]; then
echo "POSTHOG_API_KEY GitHub secret is required before publishing the npm artifact." >&2
exit 1
fi

- name: Generate npm telemetry config
env:
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
run: python3 scripts/write_telemetry_config.py

- name: Publish npm package
run: npm publish --access public --tag "$NPM_TAG"
31 changes: 31 additions & 0 deletions .github/workflows/publish-npm-on-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,36 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Clean Python build artifacts
shell: bash
run: rm -rf build dist *.egg-info

- name: Validate no stale telemetry config
shell: bash
run: |
set -euo pipefail
if [[ -f openswarm_telemetry_config.py ]]; then
echo "openswarm_telemetry_config.py exists before npm CI injection; remove it before publishing." >&2
exit 1
fi
python3 scripts/check_telemetry_artifact.py build dist

- name: Validate telemetry secrets
env:
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${POSTHOG_API_KEY}" ]]; then
echo "POSTHOG_API_KEY GitHub secret is required before publishing the npm artifact." >&2
exit 1
fi

- name: Generate npm telemetry config
env:
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
run: python3 scripts/write_telemetry_config.py

- name: Publish npm package
run: npm publish --access public --tag "$NPM_TAG"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules/
mnt/
.bun-cache/
.playwright-browsers/
openswarm_telemetry_config.py

# TUI binaries — downloaded automatically on first run from GitHub Releases
agency-windows-x64.exe
Expand Down Expand Up @@ -183,4 +184,4 @@ cython_debug/

.agency_swarm/
third_party/
.claude/
.claude/
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exclude openswarm_telemetry_config.py
prune build
prune dist
134 changes: 134 additions & 0 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# OpenSwarm Telemetry

OpenSwarm has backend-only PostHog telemetry for product analytics. It is enabled only when a PostHog capture key is available.

## Distribution Behavior

- npm releases can include a generated `openswarm_telemetry_config.py` file created by CI from `POSTHOG_API_KEY` and `POSTHOG_HOST` GitHub secrets.
- GitHub/source installs do not include a capture key and do not send telemetry unless the user explicitly sets `POSTHOG_API_KEY`.
- Python wheel/sdist artifacts intentionally exclude `openswarm_telemetry_config.py`; default telemetry injection is npm-release-only.
- Environment variables always win over the generated npm config.
- The runtime PostHog capture key shipped in npm is treated as public. Never use a personal API key for runtime telemetry.

Runtime config:

- `POSTHOG_API_KEY`: PostHog project capture key.
- `POSTHOG_HOST`: PostHog ingestion host. Defaults to `https://us.i.posthog.com`.

## Opt Out

Telemetry sends nothing when any of these are set:

- `OPENSWARM_TELEMETRY=false`
- `OPENSWARM_TELEMETRY=0`
- `OPENSWARM_TELEMETRY=no`
- `OPENSWARM_TELEMETRY=off`
- `DO_NOT_TRACK=1`

Telemetry is also disabled automatically under CI and pytest unless `OPENSWARM_TELEMETRY_ALLOW_TESTS=1` is set.

## Runtime Status

To inspect local telemetry status without printing any key:

```bash
python - <<'PY'
import telemetry

print({
"enabled": telemetry.is_enabled(),
"config_source": telemetry.config_source() or "missing",
"has_capture_key": telemetry.has_posthog_key(),
})
PY
```

`config_source` is `env`, `npm_config`, or `missing`. This check does not print the capture key.

## Identity

OpenSwarm stores an anonymous local install UUID and install secret in `~/.openswarm/telemetry.json` by default. Set `OPENSWARM_CONFIG_DIR` to change that location.

Only derived identifiers are sent:

- `user_id`: SHA-256 of the anonymous install UUID.
- `workspace_id`: HMAC of the workspace path using the local install secret.
- `agent_id` and `parent_agent_id`: HMACs derived from agent names.

Raw workspace paths, raw user identifiers, API keys, emails, and Composio IDs are never sent.

## Events

- `app_started`
- `install_created`
- `onboarding_completed`
- `provider_configured`
- `message_sent`
- `swarm_run_started`
- `swarm_run_completed`
- `agent_run_started`
- `agent_run_completed`
- `llm_generation_completed`
- `tool_invoked`
- `handoff`
- `error`
- `telemetry_smoke_test` (manual smoke script only)

`agent_name` is sent as a raw, first-class property on agent-relevant events so product analytics can group usage by agent. `agent_id` remains HMAC-derived.

Runtime telemetry rejects event names outside this allowlist before initializing PostHog or building event properties.

## Allowed Properties

Only allowlisted scalar properties are sent:

- IDs: `user_id`, `workspace_id`, `session_id`, `thread_id`, `run_trace_id`, `agent_run_id`, `parent_run_id`
- Agent/model: `agent_name`, `agent_id`, `parent_agent_id`, `caller_agent_name`, `model`, `provider`, `tool_name`
- Message metadata: `message_role`, `message_type`
- Usage/performance: `tokens_input`, `tokens_output`, `cost_usd`, `latency_ms`, `stop_reason`, `status`, `is_streaming`
- Setup/auth: `auth_method`, `provider`, `has_provider_key`, `install_source`
- Errors: `error_type`, `error_category`, `status`, `http_status`

`model` and `provider` are additionally validated as short identifier-like values. Values that look like file paths, URLs, email addresses, API keys, tokens, or secrets are dropped. Numeric fields such as token counts, latency, HTTP status, and cost must be real numbers, and boolean fields must be real booleans.

## Privacy Policy

Telemetry must not include message contents, prompts, tool arguments, tool results, generated content, exception messages, stack traces, tracebacks, file paths, API keys, emails, Composio IDs, raw workspace paths, raw user IDs, or `telemetry_opted_out`.

If a user opts out, OpenSwarm emits no telemetry event at all.

## Dashboard

Run this with a PostHog personal API key to create the default dashboard:

```bash
POSTHOG_PERSONAL_API_KEY=phx_... \
POSTHOG_ENVIRONMENT_ID=12345 \
python scripts/create_posthog_dashboard.py
```

Optional:

```bash
POSTHOG_APP_HOST=https://us.posthog.com
```

The dashboard is named `OpenSwarm Product Analytics`. It creates tagged `OpenSwarm / ...` insights for compact 24-hour KPI cards, product activity by day, messages by role, agent usage grouped by raw `agent_name`, tool usage, error category/type breakdowns, error rate, and a recent safe telemetry sample table. The dashboard script also applies a compact layout so the KPI cards appear in one row. HogQL dashboard tiles include PostHog's dashboard `{filters}` placeholder, so dashboard-level filters can narrow them by date, event, agent, tool, role, error type, or status.

To inspect the exact API payloads without creating anything:

```bash
python scripts/create_posthog_dashboard.py --dry-run
```

## Smoke Test

To verify runtime telemetry can queue a safe manual event:

```bash
POSTHOG_API_KEY=phc_... \
POSTHOG_HOST=https://us.i.posthog.com \
python scripts/smoke_telemetry.py
```

The smoke script sends `telemetry_smoke_test` with only allowlisted metadata. It does not send message contents, prompts, tool arguments/results, exception messages, file paths, or keys.
16 changes: 16 additions & 0 deletions onboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import getpass
import os
import sys
from pathlib import Path

Expand Down Expand Up @@ -290,6 +291,21 @@ def run_onboarding() -> None:

# ── write .env ────────────────────────────────────────────────────────────
_write_env(updates)
for key, value in updates.items():
if value:
os.environ[key] = value

try:
import telemetry

if updates.get(provider["env_key"]):
telemetry.capture_provider_configured(provider=provider["name"])
for addon in selected_addons:
if any(updates.get(key_spec["env"]) for key_spec in addon["keys"]):
telemetry.capture_provider_configured(provider=addon["id"])
telemetry.capture_onboarding_completed(provider=provider["name"])
except Exception:
pass

# ── summary ───────────────────────────────────────────────────────────────
console.print()
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"run_utils.py",
"onboard.py",
"swarm.py",
"telemetry.py",
"telemetry_hooks.py",
"openswarm_telemetry_config.py",
"config.py",
"helpers.py",
"server.py",
Expand All @@ -28,6 +31,10 @@
"video_generation_agent/",
"virtual_assistant/",
"patches/",
"docs/telemetry.md",
"scripts/",
"setup.py",
"MANIFEST.in",
"pyproject.toml",
"package.json",
"package-lock.json"
Expand Down
28 changes: 26 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies = [
"agency-swarm[fastapi,jupyter,litellm]>=1.9.8",
"questionary>=2.0.0",
"python-dotenv",
"posthog>=7,<8",
"rich",
"fastapi",
"uvicorn",
Expand Down Expand Up @@ -60,11 +61,34 @@ dependencies = [
openswarm = "run_utils:main"

[tool.setuptools]
py-modules = ["agency", "swarm", "helpers", "config", "onboard", "server"]
py-modules = [
"agency",
"swarm",
"helpers",
"config",
"onboard",
"server",
"telemetry",
"telemetry_hooks",
]

[tool.setuptools.packages.find]
where = ["."]
exclude = ["agentswarm-cli*", "venv*", ".venv*", ".agency_swarm*", "node_modules*", "*.node_modules*", "activity*", "mnt*", "pptx*", "slides"]
exclude = [
"agentswarm-cli*",
"venv*",
".venv*",
".agency_swarm*",
"node_modules*",
"*.node_modules*",
"activity*",
"mnt*",
"pptx*",
"slides",
"build*",
"dist*",
"*.egg-info*",
]

[tool.setuptools.package-data]
"*" = ["*.md", "*.json", "agency-*"]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
agency-swarm[fastapi,jupyter,litellm]>=1.9.7
questionary>=2.0.0
posthog>=7,<8
fastapi
uvicorn
composio==0.8.0
Expand Down
59 changes: 59 additions & 0 deletions scripts/check_telemetry_artifact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import sys
import tarfile
import zipfile
from pathlib import Path

GENERATED_CONFIG = "openswarm_telemetry_config.py"


def contains_generated_config(path: Path) -> bool:
if not path.exists():
return False
if path.is_dir():
return any(child.name == GENERATED_CONFIG for child in path.rglob(GENERATED_CONFIG))
if path.name == GENERATED_CONFIG:
return True
if zipfile.is_zipfile(path):
with zipfile.ZipFile(path) as archive:
return any(Path(name).name == GENERATED_CONFIG for name in archive.namelist())
if tarfile.is_tarfile(path):
with tarfile.open(path) as archive:
return any(Path(member.name).name == GENERATED_CONFIG for member in archive.getmembers())
return False


def main() -> int:
parser = argparse.ArgumentParser(description="Guard against accidentally shipping generated telemetry config.")
parser.add_argument("paths", nargs="+", help="Artifact files or directories to inspect.")
parser.add_argument(
"--allow-generated-config",
action="store_true",
help="Allow openswarm_telemetry_config.py. Use only for intentional npm CI injection checks.",
)
parser.add_argument(
"--require-generated-config",
action="store_true",
help="Fail unless at least one inspected path contains openswarm_telemetry_config.py.",
)
args = parser.parse_args()

matches = [str(Path(path)) for path in args.paths if contains_generated_config(Path(path))]
if matches and not args.allow_generated_config:
print(
"Refusing to ship generated telemetry config outside the intentional npm CI injection path:\n"
+ "\n".join(f" - {match}" for match in matches),
file=sys.stderr,
)
return 1
if args.require_generated_config and not matches:
print("Expected generated telemetry config, but none was found.", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading