Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
33 changes: 33 additions & 0 deletions sentry_sdk/performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from contextlib import contextmanager
import sentry_sdk


@contextmanager
def allow_n_plus_one(reason=None):
"""Context manager to mark the current transaction/spans as allowed N+1.

This sets a tag on the current transaction and current span so the server
side N+1 detector can (optionally) ignore this transaction. The server
change is required for ignoring to take effect; this helper simply
attaches metadata to the event.

Usage:
with allow_n_plus_one("expected loop"):
for x in queryset:
...
"""
tx = sentry_sdk.get_current_span()
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
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

Choose a reason for hiding this comment

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

Potential bug: The allow_n_plus_one() helper only sets tags on the current span, not the parent transaction, when called within a nested span.
  • Description: The allow_n_plus_one() function sets tags only on the span returned by get_current_span(). When this function is called within a nested span context, get_current_span() returns the child span, not the transaction. The tags are therefore not propagated to the containing transaction. Since the server-side N+1 detector checks for these tags at the transaction level, the feature will fail to ignore N+1 patterns in the common use case where database operations are wrapped in their own child spans.

  • Suggested fix: In allow_n_plus_one(), after setting the tag on the current span, check if span.containing_transaction exists. If it does, set the same tag on the transaction object to ensure the tag is propagated correctly for server-side detection.
    severity: 0.7, confidence: 0.95

Did we get this right? 👍 / 👎 to inform future reviews.


try:
yield
finally:
# leave the tag in place so the transaction contains it when sent
pass
16 changes: 16 additions & 0 deletions tests/tracing/test_allow_n_plus_one.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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
assert tx._tags.get("sentry.n_plus_one.ignore") is True
assert tx._tags.get("sentry.n_plus_one.reason") == "expected"