diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 7c97378bbe8a..7415d9f1a8ed 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast +import sentry_sdk from pika.adapters.blocking_connection import BlockingChannel from pika.spec import Basic, BasicProperties from prometheus_client import Gauge, start_http_server @@ -224,6 +225,23 @@ async def execute_node( extra_exec_kwargs[field_name] = credentials output_size = 0 + + # sentry tracking nonsense to get user counts for blocks because isolation scopes don't work :( + scope = sentry_sdk.get_current_scope() + + # save the tags + original_user = scope._user + original_tags = dict(scope._tags) if scope._tags else {} + # Set user ID for error tracking + scope.set_user({"id": user_id}) + + scope.set_tag("graph_id", graph_id) + scope.set_tag("node_id", node_id) + scope.set_tag("block_name", node_block.name) + scope.set_tag("block_id", node_block.id) + for k, v in (data.user_context or UserContext(timezone="UTC")).model_dump().items(): + scope.set_tag(f"user_context.{k}", v) + try: async for output_name, output_data in node_block.execute( input_data, **extra_exec_kwargs @@ -232,6 +250,12 @@ async def execute_node( output_size += len(json.dumps(output_data)) log_metadata.debug("Node produced output", **{output_name: output_data}) yield output_name, output_data + except Exception: + # Capture exception WITH context still set before restoring scope + sentry_sdk.capture_exception(scope=scope) + sentry_sdk.flush() # Ensure it's sent before we restore scope + # Re-raise to maintain normal error flow + raise finally: # Ensure credentials are released even if execution fails if creds_lock and (await creds_lock.locked()) and (await creds_lock.owned()): @@ -246,6 +270,10 @@ async def execute_node( execution_stats.input_size = input_size execution_stats.output_size = output_size + # Restore scope AFTER error has been captured + scope._user = original_user + scope._tags = original_tags + async def _enqueue_next_nodes( db_client: "DatabaseManagerAsyncClient", @@ -570,7 +598,6 @@ async def persist_output(output_name: str, output_data: Any) -> None: await persist_output( "error", str(stats.error) or type(stats.error).__name__ ) - return status @func_retry diff --git a/autogpt_platform/backend/backend/util/feature_flag.py b/autogpt_platform/backend/backend/util/feature_flag.py index 8f1bb476bc43..6a0af88b1d96 100644 --- a/autogpt_platform/backend/backend/util/feature_flag.py +++ b/autogpt_platform/backend/backend/util/feature_flag.py @@ -37,6 +37,11 @@ class Flag(str, Enum): AGENT_ACTIVITY = "agent-activity" +def is_configured() -> bool: + """Check if LaunchDarkly is configured with an SDK key.""" + return bool(settings.secrets.launch_darkly_sdk_key) + + def get_client() -> LDClient: """Get the LaunchDarkly client singleton.""" if not _is_initialized: diff --git a/autogpt_platform/backend/backend/util/metrics.py b/autogpt_platform/backend/backend/util/metrics.py index 820d7a13f3fb..7922fef516ec 100644 --- a/autogpt_platform/backend/backend/util/metrics.py +++ b/autogpt_platform/backend/backend/util/metrics.py @@ -5,8 +5,10 @@ from pydantic import SecretStr from sentry_sdk.integrations.anthropic import AnthropicIntegration from sentry_sdk.integrations.asyncio import AsyncioIntegration +from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration from sentry_sdk.integrations.logging import LoggingIntegration +from backend.util.feature_flag import get_client, is_configured from backend.util.settings import Settings settings = Settings() @@ -19,6 +21,9 @@ class DiscordChannel(str, Enum): def sentry_init(): sentry_dsn = settings.secrets.sentry_dsn + integrations = [] + if is_configured(): + integrations.append(LaunchDarklyIntegration(get_client())) sentry_sdk.init( dsn=sentry_dsn, traces_sample_rate=1.0, @@ -31,7 +36,8 @@ def sentry_init(): AnthropicIntegration( include_prompts=False, ), - ], + ] + + integrations, )