Repository navigation
feat: enable PostHog observability for the demo #208
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 |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| """Opt-in PostHog error tracking and OpenTelemetry application logs.""" | ||
|
|
||
| import atexit | ||
| import logging | ||
| import os | ||
| import time | ||
|
|
||
| _client = None | ||
|
|
||
|
|
||
| def configure_observability(): | ||
| """Initialize once per process; leave non-demo installations unchanged.""" | ||
| global _client | ||
| if _client is not None: | ||
| return _client | ||
| key = os.getenv("POSTHOG_API_KEY") | ||
| if not key or os.getenv("POSTHOG_OBSERVABILITY_ENABLED", "false").lower() != "true": | ||
| return None | ||
|
|
||
| from posthog import Posthog | ||
|
|
||
| host = os.getenv("POSTHOG_HOST", "https://us.i.posthog.com").rstrip("/") | ||
| _client = Posthog( | ||
| key, | ||
| host=host, | ||
| enable_exception_autocapture=True, | ||
| super_properties={"app": "anomstack", "environment": "demo"}, | ||
| ) | ||
| atexit.register(_client.shutdown) | ||
|
|
||
| from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter | ||
| from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler | ||
| from opentelemetry.sdk._logs.export import BatchLogRecordProcessor | ||
| from opentelemetry.sdk.resources import Resource | ||
|
|
||
| provider = LoggerProvider( | ||
| resource=Resource.create( | ||
| { | ||
| "service.name": "anomstack-dashboard", | ||
| "deployment.environment.name": "demo", | ||
| } | ||
| ) | ||
| ) | ||
| provider.add_log_record_processor( | ||
| BatchLogRecordProcessor( | ||
| OTLPLogExporter( | ||
| endpoint=f"{host}/i/v1/logs", | ||
| headers={"Authorization": f"Bearer {key}"}, | ||
| timeout=5, | ||
| ) | ||
| ) | ||
| ) | ||
| handler = LoggingHandler(level=logging.INFO, logger_provider=provider) | ||
| # Export application logs only, avoiding exporter recursion and SDK payload logs. | ||
| for name in ("anomstack", "anomstack_dashboard"): | ||
| logger = logging.getLogger(name) | ||
| logger.addHandler(handler) | ||
| logger.setLevel(logging.INFO) | ||
| atexit.register(provider.shutdown) | ||
| logging.getLogger("anomstack").info("PostHog observability initialized") | ||
| return _client | ||
|
|
||
|
|
||
| class ObservabilityMiddleware: | ||
| """Capture request failures without changing responses or recording request bodies.""" | ||
|
|
||
| def __init__(self, app, client): | ||
| self.app = app | ||
| self.client = client | ||
|
|
||
| async def __call__(self, scope, receive, send): | ||
| if scope["type"] != "http": | ||
| return await self.app(scope, receive, send) | ||
| started = time.monotonic() | ||
| status = 500 | ||
|
|
||
| async def track_send(message): | ||
| nonlocal status | ||
| if message["type"] == "http.response.start": | ||
| status = message["status"] | ||
| await send(message) | ||
|
|
||
| try: | ||
| await self.app(scope, receive, track_send) | ||
| except Exception as exc: | ||
| self.client.capture_exception(exc, distinct_id="anomstack-demo-server") | ||
| raise | ||
| finally: | ||
| if scope.get("path") != "/health": | ||
| route = scope.get("route") | ||
| logging.getLogger("anomstack.http").log( | ||
| logging.ERROR if status >= 500 else logging.INFO, | ||
| "HTTP request completed", | ||
| extra={ | ||
| "http.request.method": scope["method"], | ||
| "http.route": getattr(route, "path", "unmatched"), | ||
| "http.response.status_code": status, | ||
| "duration_ms": round((time.monotonic() - started) * 1000, 2), | ||
| }, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,3 +28,32 @@ To stop: | |||||||||||||||
| ```bash | ||||||||||||||||
| make kill-dashboardd | ||||||||||||||||
| ``` | ||||||||||||||||
|
|
||||||||||||||||
| ### Demo observability | ||||||||||||||||
|
|
||||||||||||||||
| The Fly demo sends browser analytics and server telemetry to Deepwell's PostHog | ||||||||||||||||
| `dev` project (148051). `profiles/demo.env` holds the deployment configuration. | ||||||||||||||||
|
|
||||||||||||||||
| - `POSTHOG_FRONTEND_API_KEY`: browser analytics, exceptions, and session replay. | ||||||||||||||||
| - `POSTHOG_API_KEY` / `POSTHOG_HOST`: existing LLM telemetry and server errors/logs. | ||||||||||||||||
| - `POSTHOG_OBSERVABILITY_ENABLED=true`: opt in to dashboard request logs and server | ||||||||||||||||
| exception capture. Other installations remain opted out by default. | ||||||||||||||||
|
|
||||||||||||||||
| Application logs use OpenTelemetry HTTP export to `/i/v1/logs`, with service name | ||||||||||||||||
| `anomstack-dashboard`. HTTP logs include method, route template, status, and duration; | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Document the The middleware uses Proposed wording-HTTP logs include method, route template, status, and duration;
+HTTP logs include method, route template (or `unmatched` when route metadata is unavailable),
+status, and duration;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| request bodies, query strings, and headers are not included. This covers dashboard | ||||||||||||||||
| application logs, not nginx, Dagster daemon, or machine-wide stdout. | ||||||||||||||||
|
|
||||||||||||||||
| Replay uses the project's recording settings and masks input values. The existing | ||||||||||||||||
| Replay Vision scanners process eligible recordings and emit Signals. Those scanners | ||||||||||||||||
| are shared with other apps using the dev project. Filter analytics/replays to | ||||||||||||||||
|
Comment on lines
+47
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win State that Replay Vision output is not yet verified. The PR objectives identify completed Replay Vision analysis as unverified, but this text states that scanners emit Signals as confirmed behavior. Qualify the statement so operators do not treat unverified demo output as confirmed. Proposed wording-Replay uses the project's recording settings and masks input values. The existing
-Replay Vision scanners process eligible recordings and emit Signals.
+Replay uses the project's recording settings and masks input values. The existing
+Replay Vision scanners are configured for eligible recordings, but completed analysis
+and Signal output for this demo are not yet verified.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| `anomstack-live-demo.fly.dev` when inspecting this demo. | ||||||||||||||||
|
|
||||||||||||||||
| Useful project links: | ||||||||||||||||
| - https://us.posthog.com/project/148051/error_tracking | ||||||||||||||||
| - https://us.posthog.com/project/148051/replay | ||||||||||||||||
| - https://us.posthog.com/project/148051/replay-vision | ||||||||||||||||
| - https://us.posthog.com/project/148051/logs | ||||||||||||||||
|
|
||||||||||||||||
| Setup references: https://posthog.com/docs/logs/installation/python and | ||||||||||||||||
| https://posthog.com/docs/error-tracking/installation/python. | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,6 +88,12 @@ def get_posthog_script(): | |
| log=log, | ||
| ) | ||
|
|
||
| from anomstack.observability import ObservabilityMiddleware, configure_observability | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The standard Useful? React with 👍 / 👎. |
||
|
|
||
| telemetry = configure_observability() | ||
| if telemetry is not None: | ||
| app.add_middleware(ObservabilityMiddleware, client=telemetry) | ||
|
|
||
| # Set the app state | ||
| app.state = AppState() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| """Request telemetry must preserve application behavior.""" | ||
|
|
||
| import asyncio | ||
| from unittest.mock import Mock | ||
|
|
||
| import pytest | ||
|
|
||
| from anomstack.observability import ObservabilityMiddleware, configure_observability | ||
|
|
||
|
|
||
| def test_disabled_by_default(monkeypatch): | ||
| monkeypatch.delenv("POSTHOG_OBSERVABILITY_ENABLED", raising=False) | ||
| assert configure_observability() is None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reset the cached observability client before this assertion.
Proposed test isolation fix def test_disabled_by_default(monkeypatch):
monkeypatch.delenv("POSTHOG_OBSERVABILITY_ENABLED", raising=False)
+ monkeypatch.setattr("anomstack.observability._client", None)
assert configure_observability() is None🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def test_response_is_preserved(): | ||
| client = Mock() | ||
| messages = [] | ||
|
|
||
| async def send(message): | ||
| messages.append(message) | ||
|
|
||
| async def application(scope, receive, send): | ||
| await send({"type": "http.response.start", "status": 204}) | ||
| await send({"type": "http.response.body", "body": b""}) | ||
|
|
||
| asyncio.run( | ||
| ObservabilityMiddleware(application, client)( | ||
| {"type": "http", "method": "GET", "path": "/"}, None, send | ||
| ) | ||
| ) | ||
| assert messages[0]["status"] == 204 | ||
| assert messages[1]["body"] == b"" | ||
| client.capture_exception.assert_not_called() | ||
|
|
||
|
|
||
| def test_exception_is_captured_and_reraised(): | ||
| client = Mock() | ||
| error = ValueError("controlled test") | ||
|
|
||
| async def application(scope, receive, send): | ||
| raise error | ||
|
|
||
| with pytest.raises(ValueError, match="controlled test"): | ||
| asyncio.run( | ||
| ObservabilityMiddleware(application, client)( | ||
| {"type": "http", "method": "GET", "path": "/"}, None, None | ||
| ) | ||
| ) | ||
| client.capture_exception.assert_called_once_with(error, distinct_id="anomstack-demo-server") | ||
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.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🤖 get_repo_knowledge executed:
get_repo_knowledge andrewm4894/anomstack /tmp/coderabbit-repo-knowledge/andrewm4894-anomstack-8dbfff43/conventionsLength of output: 3203
🏁 Script executed:
Repository: andrewm4894/anomstack
Length of output: 11065
Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information
Reachability: Internal · Exploitability: Difficult
Require HTTPS before sending telemetry credentials.
If
POSTHOG_HOSTuseshttp://, the exporter sendsPOSTHOG_API_KEYand application logs without transport encryption. Reject non-HTTPS hosts before creating either telemetry client.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents