Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
from typing import Any
from typing import Sequence
from typing import Tuple
from typing import AbstractSet
from typing_extensions import Literal
from typing_extensions import TypedDict

Expand DownExpand Up@@ -919,6 +920,7 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
enable_logs=False, # type: bool
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
Comment thread
alexander-alderman-webb marked this conversation as resolved.
):
# type: (...) -> None
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
Expand DownExpand Up@@ -1307,6 +1309,14 @@ def __init__(
function will be retained. If the function returns None, the log will
not be sent to Sentry.

:param trace_ignore_status_codes: An optional property that disables tracing for
HTTP requests with certain status codes.

Requests are not traced if the status code is contained in the provided set.

If `trace_ignore_status_codes` is not provided, requests with any status code
may be traced.

:param _experiments:
"""
pass
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set

from typing_extensions import TypedDict, Unpack

Expand DownExpand Up@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(

return scope_or_hub

def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)

def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
Expand DownExpand Up@@ -1039,6 +1046,32 @@ def finish(

super().finish(scope, end_timestamp)

status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)

num_spans = len(self._span_recorder.spans) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in CursorFix in Web

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)

self.sampled = False
Comment thread
alexander-alderman-webb marked this conversation as resolved.

Comment thread
alexander-alderman-webb marked this conversation as resolved.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
Expand DownExpand Up@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
"""
client = sentry_sdk.get_client()

transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()

# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import sentry_sdk
from sentry_sdk import start_transaction, start_span

import pytest

from collections import Counter


def test_no_ignored_codes(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 404])
def test_single_code_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if status_code == 404:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
def test_range_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(
305,
400,
),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if 305 <= status_code <= 399:
assert not events
else:
assert len(events) == 1


@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
def test_variety_ignored(sentry_init, capture_events, status_code):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
301,
302,
303,
*range(
305,
400,
),
*range(
401,
405,
),
},
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", status_code)

if (
301 <= status_code <= 303
or 305 <= status_code <= 399
or 401 <= status_code <= 404
):
assert not events
else:
assert len(events) == 1


def test_transaction_not_ignored_when_status_code_has_invalid_type(
sentry_init, capture_events
):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes=set(
range(401, 404),
),
)
events = capture_events()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", "404")

assert len(events) == 1


def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
sentry_init(
traces_sample_rate=1.0,
trace_ignore_status_codes={
404,
},
)
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(op="http", name="GET /"):
span_or_tx = sentry_sdk.get_current_span()
span_or_tx.set_data("http.response.status_code", 404)

with start_span(op="child-span"):
with start_span(op="child-child-span"):
pass

assert Counter(record_lost_event_calls) == Counter(
[
("event_processor", "transaction", None, 1),
("event_processor", "span", None, 3),
]
)