Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions docs/performance/allow_n_plus_one.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Mark expected N+1 loops
======================

The SDK provides a small helper to mark transactions or spans where an N+1 loop is
expected and acceptable.

.. code-block:: python

from sentry_sdk.performance import allow_n_plus_one

with sentry_sdk.start_transaction(name="process_items"):
with allow_n_plus_one("expected batch processing"):
for item in items:
process(item)

Notes
-----

- This helper sets the tag ``sentry.n_plus_one.ignore`` (and optional
``sentry.n_plus_one.reason``) on the current transaction and current span.
- Server-side support is required for the N+1 detector to actually ignore
transactions with this tag. The SDK only attaches the metadata.
4 changes: 4 additions & 0 deletions sentry_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"update_current_span",
]

# Public convenience submodule for performance helpers
from sentry_sdk import performance as performance
__all__.append("performance")

# Initialize the debug support after everything is loaded
from sentry_sdk.debug import init_debug_support

Expand Down
53 changes: 53 additions & 0 deletions sentry_sdk/performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from contextlib import contextmanager

# Import the helper that returns the current active span/transaction without
# importing the top-level package to avoid circular imports.
from sentry_sdk.tracing_utils import get_current_span


@contextmanager
def allow_n_plus_one(reason=None):
"""Context manager to mark the current span and its root transaction as
intentionally allowed N+1.

This sets tags on the active span and its containing transaction so that
server-side N+1 detectors (if updated to honor these tags) can ignore the
transaction. This helper is best-effort and will not raise if there is no
active span/transaction.

Usage:
with allow_n_plus_one("expected loop"):
for x in queryset:
...
"""
span = get_current_span()
if span is not None:
try:
# Tag the active span
span.set_tag("sentry.n_plus_one.ignore", True)
if reason:
span.set_tag("sentry.n_plus_one.reason", reason)

# Also tag the containing transaction if available
try:
tx = span.containing_transaction
except Exception:
tx = None

if tx is not None:
try:
tx.set_tag("sentry.n_plus_one.ignore", True)
if reason:
tx.set_tag("sentry.n_plus_one.reason", reason)
except Exception:
# best-effort: do not fail if transaction tagging fails
pass
except Exception:
# best-effort: silence any unexpected errors
pass
Copy link

Choose a reason for hiding this comment

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

Bug: Circular Import and Incorrect Documentation

A circular import exists between sentry_sdk/performance.py and the main sentry_sdk package. Additionally, allow_n_plus_one's docstring claims it tags both the transaction and current span, but the code only tags the active span. This means the parent transaction might miss the N+1 ignore tag, potentially bypassing the N+1 detector.

Fix in Cursor Fix in Web


try:
yield
finally:
# keep tags; no cleanup required
pass
25 changes: 25 additions & 0 deletions tests/tracing/test_allow_n_plus_one.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import sentry_sdk
from sentry_sdk.performance import allow_n_plus_one


def test_allow_n_plus_one_sets_tag(sentry_init):
# Initialize SDK with test fixture
sentry_init()

with sentry_sdk.start_transaction(name="tx") as tx:
with allow_n_plus_one("expected"):
# no-op loop simulated
pass

# The tag should be set on the transaction and the active span
assert tx._tags.get("sentry.n_plus_one.ignore") is True
assert tx._tags.get("sentry.n_plus_one.reason") == "expected"

# if a span was active, it should have been tagged as well; start a span
# to verify tagging of the active span
with sentry_sdk.start_span(op="db", name="q") as span:
with allow_n_plus_one("inner"):
pass

assert span._tags.get("sentry.n_plus_one.ignore") is True
assert span._tags.get("sentry.n_plus_one.reason") == "inner"