feat(backend): Add Sentry user and tag tracking to node execution (#11170)

Integrates Sentry SDK to set user and contextual tags during node
execution for improved error tracking and user count analytics. Ensures
Sentry context is properly set and restored, and exceptions are captured
with relevant context before scope restoration.

<!-- Clearly explain the need for these changes: -->

### Changes 🏗️
Adds sentry tracking to block failures
<!-- Concisely describe all of the changes made in this pull request:
-->

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [x] Test to make sure the userid and block details show up in Sentry
  - [x] make sure other errors aren't contaminated 


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Added conditional support for feature flags when configured, enabling
targeted rollouts and experiments without impacting unconfigured
environments.

- Chores
- Enhanced error monitoring with richer contextual data during node
execution to improve stability and diagnostics.
- Updated metrics initialization to dynamically include feature flag
integrations when available, without altering behavior for unconfigured
setups.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Nicholas Tindle
2025-10-15 09:33:08 -05:00
committed by GitHub
parent 1925e77733
commit b230b1b5cf
3 changed files with 40 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ from contextlib import asynccontextmanager
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 @@ class ExecutionProcessor:
await persist_output(
"error", str(stats.error) or type(stats.error).__name__
)
return status
@func_retry

View File

@@ -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:

View File

@@ -5,8 +5,10 @@ import sentry_sdk
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,
)