From 6bf2461c97586aa5e50877a4b499d8cb38514d7b Mon Sep 17 00:00:00 2001 From: Dibbu-cell Date: Mon, 13 Oct 2025 19:12:35 +0530 Subject: [PATCH 1/3] feat(perf): add allow_n_plus_one context manager to mark transactions/spans --- sentry_sdk/__init__.py | 4 ++++ sentry_sdk/performance.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 sentry_sdk/performance.py diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 1939be0510..84ed79abdc 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -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 diff --git a/sentry_sdk/performance.py b/sentry_sdk/performance.py new file mode 100644 index 0000000000..12731c6f4a --- /dev/null +++ b/sentry_sdk/performance.py @@ -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 + + try: + yield + finally: + # leave the tag in place so the transaction contains it when sent + pass From 220604ed58c002fac1a6699f6ba90fcc540b2bb0 Mon Sep 17 00:00:00 2001 From: Dibbu-cell Date: Mon, 13 Oct 2025 19:16:13 +0530 Subject: [PATCH 2/3] test(docs): add test and docs for allow_n_plus_one helper --- docs/performance/allow_n_plus_one.rst | 22 ++++++++++++++++++++++ tests/tracing/test_allow_n_plus_one.py | 16 ++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 docs/performance/allow_n_plus_one.rst create mode 100644 tests/tracing/test_allow_n_plus_one.py diff --git a/docs/performance/allow_n_plus_one.rst b/docs/performance/allow_n_plus_one.rst new file mode 100644 index 0000000000..da4f60775b --- /dev/null +++ b/docs/performance/allow_n_plus_one.rst @@ -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. diff --git a/tests/tracing/test_allow_n_plus_one.py b/tests/tracing/test_allow_n_plus_one.py new file mode 100644 index 0000000000..13cd05248e --- /dev/null +++ b/tests/tracing/test_allow_n_plus_one.py @@ -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" From e23817daef47a72b2f83b1b2dadf855189f9298b Mon Sep 17 00:00:00 2001 From: Dibbu-cell Date: Mon, 13 Oct 2025 19:35:42 +0530 Subject: [PATCH 3/3] change5 --- sentry_sdk/performance.py | 44 +++++++++++++++++++------- tests/tracing/test_allow_n_plus_one.py | 11 ++++++- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/performance.py b/sentry_sdk/performance.py index 12731c6f4a..bee52e9807 100644 --- a/sentry_sdk/performance.py +++ b/sentry_sdk/performance.py @@ -1,33 +1,53 @@ from contextlib import contextmanager -import sentry_sdk + +# 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 transaction/spans as allowed N+1. + """Context manager to mark the current span and its root transaction as + intentionally 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. + 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: ... """ - tx = sentry_sdk.get_current_span() - if tx is not None: + span = get_current_span() + if span is not None: try: - tx.set_tag("sentry.n_plus_one.ignore", True) + # Tag the active span + span.set_tag("sentry.n_plus_one.ignore", True) if reason: - tx.set_tag("sentry.n_plus_one.reason", 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 + # best-effort: silence any unexpected errors pass try: yield finally: - # leave the tag in place so the transaction contains it when sent + # keep tags; no cleanup required pass diff --git a/tests/tracing/test_allow_n_plus_one.py b/tests/tracing/test_allow_n_plus_one.py index 13cd05248e..1198952aad 100644 --- a/tests/tracing/test_allow_n_plus_one.py +++ b/tests/tracing/test_allow_n_plus_one.py @@ -11,6 +11,15 @@ def test_allow_n_plus_one_sets_tag(sentry_init): # no-op loop simulated pass - # The tag should be set on the transaction + # 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"