Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.venv
__pycache__
.idea
22 changes: 13 additions & 9 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ dependencies = { pydantic = "^1.10.4" }

[tool.poetry.group.sentry]
optional = true
dependencies = { sentry-sdk = "^1.11.0" }
dependencies = { sentry-sdk = "^2.13.0"}

[tool.poe.tasks]
format = [{cmd = "black ."}, {cmd = "isort ."}]
Expand Down
26 changes: 24 additions & 2 deletions sentry/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# Sentry Sample

This sample shows how to configure [Sentry](https://sentry.io) to intercept and capture errors from the Temporal SDK.
This sample shows how to configure [Sentry](https://sentry.io) SDK (version 2) to intercept and capture errors from the Temporal SDK
for workflows and activities. The integration adds some useful context to the errors, such as the activity type, task queue, etc.

### Further details

This is a small modification of the original example Sentry integration in this repo based on SDK v1. The integration
didn't work properly with Sentry SDK v2 due to some internal changes in the Sentry SDK that broke the worker sandbox.
Additionally, the v1 SDK has been deprecated and is only receiving security patches and will reach EOL some time in the future.

If you still need to use Sentry SDK v1, check the original example at this [commit](https://github.com/temporalio/samples-python/tree/7b3944926c3743bc0dcb3b781d8cc64e0330bac4/sentry).

Sentry's `Hub` object is now deprecated in the v2 SDK in favour of scopes. See [Activating Current Hub Clone](https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#activating-current-hub-clone)
for more details. The changes are simple, just replace `with Hub(Hub.current):` with `with isolation_scope() as scope:`.
These changes resolve the sandbox issues.

## Running the Sample

For this sample, the optional `sentry` dependency group must be included. To include, run:

Expand All @@ -16,4 +31,11 @@ This will start the worker. Then, in another terminal, run the following to exec
poetry run python starter.py

The workflow should complete with the hello result. If you alter the workflow or the activity to raise an
`ApplicationError` instead, it should appear in Sentry.
`ApplicationError` instead, it should appear in Sentry.

## Screenshot

The screenshot below shows the extra tags and context included in the
Sentry error from the exception thrown in the activity.

![Sentry screenshot](images/sentry.jpeg)
Binary file added sentry/images/sentry.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 30 additions & 26 deletions sentry/interceptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,63 @@
)

with workflow.unsafe.imports_passed_through():
from sentry_sdk import Hub, capture_exception, set_context, set_tag
from sentry_sdk import Scope, isolation_scope


def _set_common_workflow_tags(info: Union[workflow.Info, activity.Info]):
set_tag("temporal.workflow.type", info.workflow_type)
set_tag("temporal.workflow.id", info.workflow_id)
def _set_common_workflow_tags(scope: Scope, info: Union[workflow.Info, activity.Info]):
scope.set_tag("temporal.workflow.type", info.workflow_type)
scope.set_tag("temporal.workflow.id", info.workflow_id)


class _SentryActivityInboundInterceptor(ActivityInboundInterceptor):
async def execute_activity(self, input: ExecuteActivityInput) -> Any:
# https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues
with Hub(Hub.current):
set_tag("temporal.execution_type", "activity")
set_tag("module", input.fn.__module__ + "." + input.fn.__qualname__)
with isolation_scope() as scope:
scope.set_tag("temporal.execution_type", "activity")
scope.set_tag("module", input.fn.__module__ + "." + input.fn.__qualname__)

activity_info = activity.info()
_set_common_workflow_tags(activity_info)
set_tag("temporal.activity.id", activity_info.activity_id)
set_tag("temporal.activity.type", activity_info.activity_type)
set_tag("temporal.activity.task_queue", activity_info.task_queue)
set_tag("temporal.workflow.namespace", activity_info.workflow_namespace)
set_tag("temporal.workflow.run_id", activity_info.workflow_run_id)
_set_common_workflow_tags(scope, activity_info)
scope.set_tag("temporal.activity.id", activity_info.activity_id)
scope.set_tag("temporal.activity.type", activity_info.activity_type)
scope.set_tag("temporal.activity.task_queue", activity_info.task_queue)
scope.set_tag(
"temporal.workflow.namespace", activity_info.workflow_namespace
)
scope.set_tag("temporal.workflow.run_id", activity_info.workflow_run_id)
try:
return await super().execute_activity(input)
except Exception as e:
if len(input.args) == 1 and is_dataclass(input.args[0]):
set_context("temporal.activity.input", asdict(input.args[0]))
set_context("temporal.activity.info", activity.info().__dict__)
capture_exception()
scope.set_context("temporal.activity.input", asdict(input.args[0]))
scope.set_context("temporal.activity.info", activity.info().__dict__)
scope.capture_exception()
Copy link

@kenzmed kenzmed Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't capture anything for me until I've changed this from scope.capture_exception() to sentry_sdk.capture_exception()

Same for workflow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kenzmed, did you see this in your own app?

The code sample works for me with a live Sentry backend:

image

If you're running it in your own app, it's probably because you have Sentry SDK v1 installed. This sample is for Sentry v2. Take a look at the original interceptor.

Keep in mind, the original interceptor doesn't work for Sentry SDK v2 and SDK v1 is EOL.

raise e


class _SentryWorkflowInterceptor(WorkflowInboundInterceptor):
async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any:
# https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues
with Hub(Hub.current):
set_tag("temporal.execution_type", "workflow")
set_tag("module", input.run_fn.__module__ + "." + input.run_fn.__qualname__)
with isolation_scope() as scope:
scope.set_tag("temporal.execution_type", "workflow")
scope.set_tag(
"module", input.run_fn.__module__ + "." + input.run_fn.__qualname__
)
workflow_info = workflow.info()
_set_common_workflow_tags(workflow_info)
set_tag("temporal.workflow.task_queue", workflow_info.task_queue)
set_tag("temporal.workflow.namespace", workflow_info.namespace)
set_tag("temporal.workflow.run_id", workflow_info.run_id)
_set_common_workflow_tags(scope, workflow_info)
scope.set_tag("temporal.workflow.task_queue", workflow_info.task_queue)
scope.set_tag("temporal.workflow.namespace", workflow_info.namespace)
scope.set_tag("temporal.workflow.run_id", workflow_info.run_id)
try:
return await super().execute_workflow(input)
except Exception as e:
if len(input.args) == 1 and is_dataclass(input.args[0]):
set_context("temporal.workflow.input", asdict(input.args[0]))
set_context("temporal.workflow.info", workflow.info().__dict__)
scope.set_context("temporal.workflow.input", asdict(input.args[0]))
scope.set_context("temporal.workflow.info", workflow.info().__dict__)

if not workflow.unsafe.is_replaying():
with workflow.unsafe.sandbox_unrestricted():
capture_exception()
scope.capture_exception()
raise e


Expand Down
1 change: 0 additions & 1 deletion sentry/starter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
import os

from temporalio.client import Client

Expand Down
10 changes: 7 additions & 3 deletions sentry/worker.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import asyncio
import logging
import os
import random
from dataclasses import dataclass
from datetime import timedelta

import sentry_sdk
from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.worker import Worker

from sentry.interceptor import SentryInterceptor
with workflow.unsafe.imports_passed_through():
import sentry_sdk

from sentry.interceptor import SentryInterceptor


@dataclass
Expand All @@ -21,6 +23,8 @@ class ComposeGreetingInput:
@activity.defn
async def compose_greeting(input: ComposeGreetingInput) -> str:
activity.logger.info("Running activity with parameter %s" % input)
if random.random() < 0.9:
raise Exception("Activity failed!")
return f"{input.greeting}, {input.name}!"


Expand Down