apache / apache/airflow

Sentry before_send/transport dotted-path config never resolved to a callable in Task SDK (silently drops all events)

Open Beginner friendly
#69,464 1 comment 1 reaction 0 assignees View on GitHub
kind:bug
Dominant language
Python
Stars
46.9k
Forks
17.8k
Avg merge
2d 9h
Merged PRs (30d)
472

Description

### Under which category would you file this issue?

Task SDK

### Apache Airflow version

3.2.2 (also reproduced, unchanged, on 3.3.0rc2)

### What happened and how to reproduce it?

When `[sentry] before_send` (e.g. `AIRFLOW__SENTRY__BEFORE_SEND=`) is configured,
`ConfiguredSentry.prepare_to_enrich_errors()` in
`task-sdk/src/airflow/sdk/execution_time/sentry/configured.py` never resolves it into an
actual callable — it gets passed to `sentry_sdk.init()` as the raw string:

```python
sentry_config_opts: dict[str, Any] = conf.getsection("sentry") or {}
if sentry_config_opts:
sentry_config_opts.pop("sentry_on")
old_way_dsn = sentry_config_opts.pop("sentry_dsn", None)
new_way_dsn = sentry_config_opts.pop("dsn", None)
dsn = old_way_dsn or new_way_dsn
...
else:
dsn = None
if before_send := conf.getimport("sentry", "before_send", fallback=None):
sentry_config_opts["before_send"] = before_send
if transport := conf.getimport("sentry", "transport", fallback=None):
sentry_config_opts["transport"] = transport

if dsn:
sentry_sdk.init(dsn=dsn, integrations=integrations, **sentry_config_opts)
```

`conf.getsection("sentry")` always returns a non-empty dict — it starts from the section's
schema defaults (`sentry_on`, `sentry_dsn` both have non-None defaults) — so the
`if sentry_config_opts:` branch is *always* taken, and the `else` branch (the only place
`before_send`/`transport` get resolved via `conf.getimport()`) is unreachable dead code.

As a result, `sentry_sdk.init(before_send="my.dotted.path", ...)` receives a plain string.
At event-capture time, `sentry_sdk`'s `Client._prepare_event` calls `before_send(event, hint)`,
raising `TypeError: 'str' object is not callable`. This is swallowed internally by
`capture_internal_exceptions()` (logged only via the internal `sentry_sdk.errors` logger) —
the event is silently dropped with no error surfaced and nothing delivered to Sentry.
Same issue applies to `transport`.

This is a regression from Airflow 2.x's `airflow/sentry.py`, where the equivalent
`conf.getimport(...)` calls ran unconditionally:

```python
sentry_config_opts["before_send"] = conf.getimport("sentry", "before_send", fallback=None)
sentry_config_opts["transport"] = conf.getimport("sentry", "transport", fallback=None)
```

(see 2.11.0's `airflow/sentry.py`). This logic appears to have been ported into the Task SDK
reimplementation in #57032, but the branching structure changed such that the resolution
path became unreachable.

**Steps to reproduce:**

```
AIRFLOW__SENTRY__SENTRY_ON=True
AIRFLOW__SENTRY__SENTRY_DSN=
AIRFLOW__SENTRY__BEFORE_SEND=path.to.a.real.before_send.function
```

```python
from airflow.sdk.execution_time.sentry.configured import ConfiguredSentry
cs = ConfiguredSentry()
cs.prepare_to_enrich_errors(executor_integration="")
import sentry_sdk
print(type(sentry_sdk.get_client().options.get("before_send")))
# prints instead of the resolved function
```

Any subsequent `sentry_sdk.capture_exception()` / `capture_message()` call will silently
fail to deliver its event.

### What you think should happen instead?

`before_send`/`transport` dotted paths configured via `[sentry]` should always be resolved
into real callables via `conf.getimport()`, regardless of what else is set in the `[sentry]`
section — matching 2.x behavior, e.g.:

```python
sentry_config_opts: dict[str, Any] = conf.getsection("sentry") or {}
sentry_config_opts.pop("sentry_on", None)
old_way_dsn = sentry_config_opts.pop("sentry_dsn", None)
new_way_dsn = sentry_config_opts.pop("dsn", None)
dsn = old_way_dsn or new_way_dsn
if before_send := conf.getimport("sentry", "before_send", fallback=None):
sentry_config_opts["before_send"] = before_send
if transport := conf.getimport("sentry", "transport", fallback=None):
sentry_config_opts["transport"] = transport
```

### Operating System

N/A — reproduced via the official `apache/airflow:3.2.2-python3.12` and
`apache/airflow:3.3.0rc2-python3.12` images.

### Deployment

Other Docker-based deployment

### Apache Airflow Provider(s)

_No response_

### Versions of Apache Airflow Providers

_No response_

### Official Helm Chart version

Not Applicable

### Kubernetes Version

Not Applicable

### Helm Chart configuration

Not Applicable

### Docker Image customizations

Not Applicable — reproduced against the unmodified official images.

### Anything else?

This is separate from #52368 / #65136 / #65161 (uncaught task exceptions not captured by
Sentry), which is fixed in 3.3.0. This `before_send`/`transport` resolution bug is a
different issue, still present unchanged in 3.3.0rc2.

### Are you willing to submit PR?

- [ ] Yes I am willing to submit a PR!

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Contributor guide

Open the contributing guide

Research direction

Start in task-sdk/src/airflow/sdk/execution_time/sentry/configured.py, specifically ConfiguredSentry.prepare_to_enrich_errors(), and compare its [sentry] option handling with the 2.11.0 airflow/sentry.py behavior described in the issue. Verify the configured before_send and transport values are resolved through conf.getimport() before sentry_sdk.init() receives them. Done means dotted paths become callable options even when the Sentry section has schema defaults, without dropping events.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
observability-sre
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.