Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions .github/workflows/dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
- name: Set version and build config
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
run: |
VERSION=$(git describe --tags --abbrev=0)
VERSION="${VERSION#v}"
Expand All @@ -42,6 +43,7 @@ jobs:
sed -i -E "s|^__version__ = .*|__version__ = \"${VERSION}\"|" src/blaxel/__init__.py
sed -i -E "s|^__commit__ = .*|__commit__ = \"${COMMIT}\"|" src/blaxel/__init__.py
sed -i -E "s|^__sentry_dsn__ = .*|__sentry_dsn__ = \"${SENTRY_DSN}\"|" src/blaxel/__init__.py
sed -i -E "s|^__posthog_key__ = .*|__posthog_key__ = \"${POSTHOG_KEY}\"|" src/blaxel/__init__.py
- name: Test package installation
run: |
uv venv
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- name: Set version and build config
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
run: |
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
Expand All @@ -34,6 +35,7 @@ jobs:
sed -i -E "s|^__version__ = .*|__version__ = \"${VERSION}\"|" src/blaxel/__init__.py
sed -i -E "s|^__commit__ = .*|__commit__ = \"${COMMIT}\"|" src/blaxel/__init__.py
sed -i -E "s|^__sentry_dsn__ = .*|__sentry_dsn__ = \"${SENTRY_DSN}\"|" src/blaxel/__init__.py
sed -i -E "s|^__posthog_key__ = .*|__posthog_key__ = \"${POSTHOG_KEY}\"|" src/blaxel/__init__.py
- name: Test package installation
run: |
uv venv
Expand Down
1 change: 1 addition & 0 deletions src/blaxel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
__version__ = ""
__commit__ = ""
__sentry_dsn__ = ""
__posthog_key__ = ""
__all__ = ["autoload", "settings", "env"]

autoload()
6 changes: 6 additions & 0 deletions src/blaxel/core/common/autoload.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
response_interceptors_sync,
)
from ..sandbox.client import client as client_sandbox
from .posthog import track_sdk_installed
from .sentry import init_sentry
from .settings import settings

Expand Down Expand Up @@ -43,6 +44,11 @@ def autoload() -> None:
except Exception:
pass

try:
track_sdk_installed()
except Exception:
pass

try:
telemetry()
except Exception:
Expand Down
191 changes: 191 additions & 0 deletions src/blaxel/core/common/posthog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import json
import logging
import platform
import threading
import uuid
from pathlib import Path

import httpx

from .settings import settings

logger = logging.getLogger(__name__)

# PostHog API key injected at build time via sed in CI
_POSTHOG_KEY = ""

# PostHog API endpoint
_POSTHOG_HOST = "https://us.i.posthog.com"

# Telemetry state file path: ~/.blaxel/telemetry.json
_telemetry_state: dict | None = None
_telemetry_lock = threading.Lock()


def _get_posthog_key() -> str:
"""Return the PostHog API key injected at build time."""
import blaxel

return getattr(blaxel, "__posthog_key__", "") or _POSTHOG_KEY


def _get_telemetry_path() -> Path | None:
"""Return the path to the telemetry state file."""
try:
return Path.home() / ".blaxel" / "telemetry.json"
except Exception:
return None


def _load_telemetry_state() -> dict:
"""Load the telemetry state from disk."""
global _telemetry_state
with _telemetry_lock:
if _telemetry_state is not None:
return _telemetry_state

_telemetry_state = {"distinct_id": "", "sdks": {}}

telemetry_path = _get_telemetry_path()
if not telemetry_path:
return _telemetry_state

try:
data = telemetry_path.read_text(encoding="utf-8")
parsed = json.loads(data)
_telemetry_state = {
"distinct_id": parsed.get("distinct_id", ""),
"cli": parsed.get("cli"),
"sdks": parsed.get("sdks") or {},
}
except Exception:
# File doesn't exist or is invalid - use defaults
pass

return _telemetry_state


def _save_telemetry_state(state: dict) -> None:
"""Save the telemetry state to disk."""
telemetry_path = _get_telemetry_path()
if not telemetry_path:
return

try:
telemetry_path.parent.mkdir(parents=True, exist_ok=True)
telemetry_path.write_text(
json.dumps(state, indent=2),
encoding="utf-8",
)
telemetry_path.chmod(0o600)
except Exception:
# Silently fail
pass


def _get_distinct_id() -> str:
"""Return a persistent anonymous UUID for PostHog events.

The UUID is generated on first use and stored in ~/.blaxel/telemetry.json.
"""
state = _load_telemetry_state()
if state["distinct_id"]:
return state["distinct_id"]

state["distinct_id"] = str(uuid.uuid4())
_save_telemetry_state(state)
return state["distinct_id"]


def _get_os_arch() -> str:
"""Get OS and architecture string."""
try:
system = platform.system().lower()
machine = platform.machine().lower()
if machine in ("x86_64", "amd64"):
arch = "amd64"
elif machine in ("aarch64", "arm64"):
arch = "arm64"
else:
arch = machine
return f"{system}/{arch}"
except Exception:
return "unknown/unknown"


def _capture_posthog_event(event: str, properties: dict | None = None) -> None:
"""Fire-and-forget HTTP POST to PostHog capture endpoint."""
api_key = _get_posthog_key()
if not api_key:
return

distinct_id = _get_distinct_id()
payload = {
"api_key": api_key,
"event": event,
"distinct_id": distinct_id,
"properties": {
"$lib": "blaxel-sdk-python",
"$lib_version": settings.version,
"os_arch": _get_os_arch(),
**(properties or {}),
},
}

def send() -> None:
try:
httpx.post(
f"{_POSTHOG_HOST}/capture/",
json=payload,
headers={"Content-Type": "application/json"},
timeout=5.0,
)
except Exception:
# Silently fail - telemetry should never break the SDK
pass

thread = threading.Thread(target=send, daemon=True)
thread.start()


def track_sdk_installed() -> None:
"""Track 'Installed SDK' event, deduplicated by version.

Only fires once per SDK version. Respects DO_NOT_TRACK env var
and ~/.blaxel/config.yaml tracking setting.
"""
try:
# Check tracking consent
if not settings.tracking:
return

api_key = _get_posthog_key()
if not api_key:
return

version = settings.version
if not version or version == "unknown":
return

state = _load_telemetry_state()
sdk_key = "python"

# Check if we already reported this version
if state.get("sdks", {}).get(sdk_key) == version:
return

# Update state and save
if "sdks" not in state:
state["sdks"] = {}
state["sdks"][sdk_key] = version
_save_telemetry_state(state)

# Fire event
_capture_posthog_event("Installed SDK", {
"sdk": "python",
"version": version,
"environment": settings.env,
})
except Exception:
# Silently fail - telemetry should never break the SDK
pass
Loading