-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add PostHog telemetry for wrapper usage tracking #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ | |
| import sys | ||
| from typing import NoReturn, Optional | ||
|
|
||
| from .telemetry import record_wrapper_used | ||
|
|
||
| _WRAPPER_ENV = "PROMPTFOO_PY_WRAPPER" | ||
| _WINDOWS_SHELL_EXTENSIONS = (".bat", ".cmd") | ||
|
|
||
|
|
@@ -184,16 +186,19 @@ def main() -> NoReturn: | |
| # Build command: try external promptfoo first, fall back to npx | ||
| promptfoo_path = None if os.environ.get(_WRAPPER_ENV) else _find_external_promptfoo() | ||
| if promptfoo_path: | ||
| record_wrapper_used("global") | ||
| cmd = [promptfoo_path] + sys.argv[1:] | ||
| env = os.environ.copy() | ||
| env[_WRAPPER_ENV] = "1" | ||
| result = _run_command(cmd, env=env) | ||
| else: | ||
| npx_path = shutil.which("npx") | ||
| if npx_path: | ||
| record_wrapper_used("npx") | ||
| cmd = [npx_path, "-y", "promptfoo@latest"] + sys.argv[1:] | ||
| result = _run_command(cmd) | ||
| else: | ||
| record_wrapper_used("error") | ||
|
Comment on lines
+189
to
+201
|
||
| print("ERROR: Neither promptfoo nor npx is available.", file=sys.stderr) | ||
| print("Please install promptfoo: npm install -g promptfoo", file=sys.stderr) | ||
| print("Or ensure Node.js is properly installed.", file=sys.stderr) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,204 @@ | ||||||||||||||
| """ | ||||||||||||||
| Telemetry module for the promptfoo Python wrapper. | ||||||||||||||
|
|
||||||||||||||
| Sends anonymous usage analytics to PostHog to help improve promptfoo. | ||||||||||||||
| Telemetry can be disabled by setting PROMPTFOO_DISABLE_TELEMETRY=1. | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| import atexit | ||||||||||||||
| import os | ||||||||||||||
| import platform | ||||||||||||||
| import sys | ||||||||||||||
| import uuid | ||||||||||||||
| from pathlib import Path | ||||||||||||||
| from typing import Any, Optional | ||||||||||||||
|
|
||||||||||||||
| import yaml | ||||||||||||||
| from posthog import Posthog | ||||||||||||||
|
|
||||||||||||||
| from . import __version__ | ||||||||||||||
|
|
||||||||||||||
| # PostHog configuration - same as the main promptfoo TypeScript project. | ||||||||||||||
| # NOTE: This is an intentionally public PostHog project API key: | ||||||||||||||
| # - Safe to commit to source control (client-side telemetry key) | ||||||||||||||
| # - Only allows sending anonymous usage events to the promptfoo PostHog project | ||||||||||||||
| # - Does not grant administrative access to the PostHog account | ||||||||||||||
| # - Abuse is mitigated by PostHog's built-in rate limiting | ||||||||||||||
| # - Telemetry can be disabled via PROMPTFOO_DISABLE_TELEMETRY=1 | ||||||||||||||
| _POSTHOG_HOST = "https://a.promptfoo.app" | ||||||||||||||
| _POSTHOG_KEY = "phc_E5n5uHnDo2eREJL1uqX1cIlbkoRby4yFWt3V94HqRRg" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _get_env_bool(name: str) -> bool: | ||||||||||||||
| """Check if an environment variable is set to a truthy value.""" | ||||||||||||||
| value = os.environ.get(name, "").lower() | ||||||||||||||
| return value in ("1", "true", "yes", "on") | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _is_ci() -> bool: | ||||||||||||||
| """Detect if running in a CI environment.""" | ||||||||||||||
| ci_env_vars = [ | ||||||||||||||
| "CI", | ||||||||||||||
| "CONTINUOUS_INTEGRATION", | ||||||||||||||
| "GITHUB_ACTIONS", | ||||||||||||||
| "GITLAB_CI", | ||||||||||||||
| "CIRCLECI", | ||||||||||||||
| "TRAVIS", | ||||||||||||||
| "JENKINS_URL", | ||||||||||||||
| "BUILDKITE", | ||||||||||||||
| "TEAMCITY_VERSION", | ||||||||||||||
| "TF_BUILD", # Azure Pipelines | ||||||||||||||
| ] | ||||||||||||||
| return any(os.environ.get(var) for var in ci_env_vars) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _get_config_dir() -> Path: | ||||||||||||||
| """Get the promptfoo config directory path.""" | ||||||||||||||
| return Path.home() / ".promptfoo" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _read_global_config() -> dict[str, Any]: | ||||||||||||||
| """Read the global promptfoo config from ~/.promptfoo/promptfoo.yaml.""" | ||||||||||||||
| config_file = _get_config_dir() / "promptfoo.yaml" | ||||||||||||||
| if config_file.exists(): | ||||||||||||||
| try: | ||||||||||||||
| with open(config_file) as f: | ||||||||||||||
| config = yaml.safe_load(f) | ||||||||||||||
| return config if isinstance(config, dict) else {} | ||||||||||||||
| except Exception: | ||||||||||||||
|
||||||||||||||
| except Exception: | |
| except (OSError, yaml.YAMLError) as exc: | |
| # Log unexpected errors but do not let telemetry break the CLI | |
| sys.stderr.write( | |
| f"Warning: unable to read telemetry config file {config_file}: {exc}\n" | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The telemetry calls in the main function could fail if there are import errors from the telemetry module, which would break the CLI functionality. Consider wrapping the import in a try-except block or ensuring the telemetry module is designed to fail gracefully even on import errors. This is especially important since telemetry is a non-critical feature that should never prevent the core CLI from functioning.