From 3bc035fd9868bdf3699e3d9ed287931622cdb9cf Mon Sep 17 00:00:00 2001 From: Hannah Stepanek Date: Tue, 9 Dec 2025 15:36:26 -0800 Subject: [PATCH 1/5] Fixup: duplicated test names --- tests/agent_features/test_distributed_tracing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/agent_features/test_distributed_tracing.py b/tests/agent_features/test_distributed_tracing.py index a29a5d3b7..fee86cac6 100644 --- a/tests/agent_features/test_distributed_tracing.py +++ b/tests/agent_features/test_distributed_tracing.py @@ -1282,7 +1282,7 @@ def _test(): ), ), ) -def test_distributed_trace_uses_sampling_instance( +def test_distributed_trace_uses_adaptive_sampling_instance( dt_settings, dt_headers, expected_sampling_instance_called, @@ -1368,7 +1368,7 @@ def _test(): ), ), ) -def test_distributed_trace_uses_sampling_instance( +def test_distributed_trace_uses_ratio_sampling_instance( dt_settings, dt_headers, expected_sampling_instance_called, expected_ratio ): test_settings = _override_settings.copy() From 56237454c971e36de978358b25e47b64af4f0619 Mon Sep 17 00:00:00 2001 From: Hannah Stepanek Date: Tue, 2 Dec 2025 17:34:02 -0800 Subject: [PATCH 2/5] Prioritize full over partial granularity * priority +=2 when full granularity and sampled=true * priority +=1 when partial granularity and sampled=true * always_on sampler for full granularity sets priority=3 instead of 2 --- newrelic/api/transaction.py | 18 +++-- .../test_distributed_tracing.py | 75 ++++++++++++++++--- .../validate_transaction_object_attributes.py | 38 ++++++++++ 3 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 tests/testing_support/validators/validate_transaction_object_attributes.py diff --git a/newrelic/api/transaction.py b/newrelic/api/transaction.py index 3fcb0908e..36d0668c8 100644 --- a/newrelic/api/transaction.py +++ b/newrelic/api/transaction.py @@ -571,6 +571,12 @@ def __exit__(self, exc, value, tb): # Sampled and priority need to be computed at the end of the # transaction when distributed tracing or span events are enabled. self._make_sampling_decision() + else: + # If dt is disabled, set sampled=False and priority random number between 0 and 1. + # The priority of the transaction is used for other data like transaction + # events even when span events are not sent. + self._priority = float(f"{random.random():.6f}") # noqa: S311 + self._sampled = False self._cached_path._name = self.path agent_attributes = self.agent_attributes @@ -1017,8 +1023,9 @@ def sampling_algo_compute_sampled_and_priority(self, priority, sampled, sampler_ if sampled is None: _logger.debug("No trusted account id found. Sampling decision will be made by adaptive sampling algorithm.") sampled = self._application.compute_sampled(**sampler_kwargs) + # Increment the priority + 2 for full and + 1 for partial granularity. if sampled: - priority += 1 + priority += 1 + int(sampler_kwargs.get("full_granularity")) return priority, sampled def _compute_sampled_and_priority( @@ -1063,7 +1070,8 @@ def _compute_sampled_and_priority( ) if config == "always_on": sampled = True - priority = 2.0 + # priority=3 for full granularity and priority=2 for partial granularity. + priority = 2.0 + int(full_granularity) elif config == "always_off": sampled = False priority = 0 @@ -1139,9 +1147,9 @@ def _make_sampling_decision(self): return # This is only reachable if both full and partial granularity tracing are off. - # Set priority=0 and do not sample. This enables DT headers to still be sent - # even if the trace is never sampled. - self._priority = 0 + # Set priority to random number between 0 and 1 and do not sample. This enables + # DT headers to still be sent even if the trace is never sampled. + self._priority = float(f"{random.random():.6f}") # noqa: S311 self._sampled = False def _freeze_path(self): diff --git a/tests/agent_features/test_distributed_tracing.py b/tests/agent_features/test_distributed_tracing.py index fee86cac6..00cd6c80a 100644 --- a/tests/agent_features/test_distributed_tracing.py +++ b/tests/agent_features/test_distributed_tracing.py @@ -16,6 +16,7 @@ import copy import json import time +import random import pytest import webtest @@ -25,6 +26,7 @@ from testing_support.validators.validate_function_not_called import validate_function_not_called from testing_support.validators.validate_transaction_event_attributes import validate_transaction_event_attributes from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics +from testing_support.validators.validate_transaction_object_attributes import validate_transaction_object_attributes from newrelic.api.application import application_instance from newrelic.api.function_trace import function_trace @@ -542,17 +544,17 @@ def _test_inbound_dt_payload_acceptance(): "newrelic_header,traceparent_sampled,newrelic_sampled,root_setting,remote_parent_sampled_setting,remote_parent_not_sampled_setting,expected_sampled,expected_priority,expected_adaptive_sampling_algo_called", ( (False, None, None, "default", "default", "default", None, None, True), # Uses adaptive sampling algo. - (False, None, None, "always_on", "default", "default", True, 2, False), # Always sampled. + (False, None, None, "always_on", "default", "default", True, 3, False), # Always sampled. (False, None, None, "always_off", "default", "default", False, 0, False), # Never sampled. (True, True, None, "default", "default", "default", None, None, True), # Uses adaptive sampling algo. - (True, True, None, "default", "always_on", "default", True, 2, False), # Always sampled. + (True, True, None, "default", "always_on", "default", True, 3, False), # Always sampled. (True, True, None, "default", "always_off", "default", False, 0, False), # Never sampled. (True, False, None, "default", "default", "default", None, None, True), # Uses adaptive sampling algo. (True, False, None, "default", "always_on", "default", None, None, True), # Uses adaptive sampling alog. (True, False, None, "default", "always_off", "default", None, None, True), # Uses adaptive sampling algo. (True, True, None, "default", "default", "always_on", None, None, True), # Uses adaptive sampling algo. (True, True, None, "default", "default", "always_off", None, None, True), # Uses adaptive sampling algo. - (True, False, None, "default", "default", "always_on", True, 2, False), # Always sampled. + (True, False, None, "default", "default", "always_on", True, 3, False), # Always sampled. (True, False, None, "default", "default", "always_off", False, 0, False), # Never sampled. ( True, @@ -587,9 +589,9 @@ def _test_inbound_dt_payload_acceptance(): 1.23456, False, ), # Uses sampling decision in W3C TraceState header. - (True, True, False, "default", "always_on", "default", True, 2, False), # Always sampled. + (True, True, False, "default", "always_on", "default", True, 3, False), # Always sampled. (True, True, True, "default", "always_off", "default", False, 0, False), # Never sampled. - (True, False, False, "default", "default", "always_on", True, 2, False), # Always sampled. + (True, False, False, "default", "default", "always_on", True, 3, False), # Always sampled. (True, False, True, "default", "default", "always_off", False, 0, False), # Never sampled. ( True, @@ -602,7 +604,7 @@ def _test_inbound_dt_payload_acceptance(): 0.1234, False, ), # Uses sampling and priority from newrelic header. - (True, None, True, "default", "always_on", "default", True, 2, False), # Always sampled. + (True, None, True, "default", "always_on", "default", True, 3, False), # Always sampled. (True, None, True, "default", "always_off", "default", False, 0, False), # Never sampled. ( True, @@ -637,7 +639,7 @@ def _test_inbound_dt_payload_acceptance(): 0.1234, False, ), # Uses sampling and priority from newrelic header. - (True, None, False, "default", "default", "always_on", True, 2, False), # Always sampled. + (True, None, False, "default", "default", "always_on", True, 3, False), # Always sampled. (True, None, False, "default", "default", "always_off", False, 0, False), # Never sampled. (True, None, None, "default", "default", "default", None, None, True), # Uses adaptive sampling algo. ), @@ -878,8 +880,7 @@ def _test(): "full_granularity_enabled,full_granularity_remote_parent_sampled_setting,partial_granularity_enabled,partial_granularity_remote_parent_sampled_setting,expected_sampled,expected_priority,expected_adaptive_sampling_algo_called", ( (True, "always_off", True, "adaptive", None, None, True), # Uses adaptive sampling algo. - (True, "always_on", True, "adaptive", True, 2, False), # Uses adaptive sampling algo. - (False, "always_on", False, "adaptive", False, 0, False), # Uses adaptive sampling algo. + (True, "always_on", True, "adaptive", True, 3, False), # Always samples. ), ) def test_distributed_trace_remote_parent_sampling_decision_between_full_and_partial_granularity( @@ -1394,3 +1395,59 @@ def _test(): assert application.sampler._samplers[expected_sampling_instance_called].ratio == expected_ratio _test() + + +@pytest.mark.parametrize( + "dt_settings,expected_priority,expected_sampled", + ( + ( # When dt is enabled but full and partial are disabled. + { + "distributed_tracing.sampler.full_granularity.enabled": False, + "distributed_tracing.sampler.partial_granularity.enabled": False, + }, + 0.123, # random + False, + ), + ( # When dt is disabled. + { + "distributed_tracing.enabled": False, + }, + 0.123, # random + False, + ), + ( # Verify when full granularity sampled +2 is added to the priority. + { + "distributed_tracing.sampler.full_granularity.root.trace_id_ratio_based.ratio": 1, + }, + 2.123, # random + 2 + True, + ), + ( # Verify when partial granularity sampled +1 is added to the priority. + { + "distributed_tracing.sampler.full_granularity.enabled": False, + "distributed_tracing.sampler.partial_granularity.enabled": True, + "distributed_tracing.sampler.partial_granularity.root.trace_id_ratio_based.ratio": 1, + }, + 1.123, # random + 1 + True, + ), + ), +) +def test_distributed_trace_enabled_settings_set_correct_sampled_priority( + dt_settings, + expected_priority, + expected_sampled, + monkeypatch, +): + monkeypatch.setattr(random, 'random', lambda *args, **kwargs: 0.123) + + test_settings = _override_settings.copy() + test_settings.update(dt_settings) + + @override_application_settings(test_settings) + @validate_transaction_object_attributes({"sampled": expected_sampled, "priority": expected_priority}) + @background_task(name="test_distributed_trace_attributes") + def _test(): + pass + + _test() diff --git a/tests/testing_support/validators/validate_transaction_object_attributes.py b/tests/testing_support/validators/validate_transaction_object_attributes.py new file mode 100644 index 000000000..3a2a96191 --- /dev/null +++ b/tests/testing_support/validators/validate_transaction_object_attributes.py @@ -0,0 +1,38 @@ +# Copyright 2010 New Relic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from newrelic.common.object_wrapper import transient_function_wrapper + + +def validate_transaction_object_attributes(attributes): + # This validates attributes on the transaction object that is passed to + # `StatsEngine.record_transaction`. + # + # Args: + # + # attributes: a dictionary of expected attributes (key) and values (value) + + @transient_function_wrapper("newrelic.core.stats_engine", "StatsEngine.record_transaction") + def _validate_attributes(wrapped, instance, args, kwargs): + def _bind_params(transaction, *args, **kwargs): + return transaction + + transaction = _bind_params(*args, **kwargs) + + for attr, value in attributes.items(): + assert getattr(transaction, attr) == value + + return wrapped(*args, **kwargs) + + return _validate_attributes From 1d455c976e6807907011ca28cd4eced729b220e8 Mon Sep 17 00:00:00 2001 From: Hannah Stepanek Date: Thu, 11 Dec 2025 15:00:50 -0800 Subject: [PATCH 3/5] Increment priority when only sampled is sent in headers --- newrelic/api/transaction.py | 5 +++-- .../agent_features/test_distributed_tracing.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/newrelic/api/transaction.py b/newrelic/api/transaction.py index 36d0668c8..a0082aa3e 100644 --- a/newrelic/api/transaction.py +++ b/newrelic/api/transaction.py @@ -1017,15 +1017,16 @@ def sampling_algo_compute_sampled_and_priority(self, priority, sampled, sampler_ # or newrelic DT headers and may be overridden in _make_sampling_decision # based on the configuration. The only time they are set in here is when the # sampling decision must be made by the adaptive sampling algorithm. + adjust_priority = priority is None or sampled is None if priority is None: # Truncate priority field to 6 digits past the decimal. priority = float(f"{random.random():.6f}") # noqa: S311 if sampled is None: _logger.debug("No trusted account id found. Sampling decision will be made by adaptive sampling algorithm.") sampled = self._application.compute_sampled(**sampler_kwargs) + if adjust_priority and sampled: # Increment the priority + 2 for full and + 1 for partial granularity. - if sampled: - priority += 1 + int(sampler_kwargs.get("full_granularity")) + priority += 1 + int(sampler_kwargs.get("full_granularity")) return priority, sampled def _compute_sampled_and_priority( diff --git a/tests/agent_features/test_distributed_tracing.py b/tests/agent_features/test_distributed_tracing.py index 00cd6c80a..b7f2df726 100644 --- a/tests/agent_features/test_distributed_tracing.py +++ b/tests/agent_features/test_distributed_tracing.py @@ -1451,3 +1451,21 @@ def _test(): pass _test() + + +def test_distributed_trace_priority_set_when_only_sampled_set_in_headers( + monkeypatch, +): + monkeypatch.setattr(random, 'random', lambda *args, **kwargs: 0.123) + + @override_application_settings(_override_settings) + @validate_transaction_object_attributes({"sampled": True, "priority": 2.123}) + @background_task() + def _test(): + headers = { + "traceparent": f"00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01", + "tracestate": f"1@nr=0-0-1-2827902-0af7651916cd43dd-00f067aa0ba902b7-1--1518469636035" + } + accept_distributed_trace_headers(headers) + + _test() From 6956500224c219dc7fcac05bb79eb3f916edd15f Mon Sep 17 00:00:00 2001 From: hmstepanek <30059933+hmstepanek@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:02:50 +0000 Subject: [PATCH 4/5] [MegaLinter] Apply linters fixes --- .../test_distributed_tracing.py | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/tests/agent_features/test_distributed_tracing.py b/tests/agent_features/test_distributed_tracing.py index b7f2df726..8f278b022 100644 --- a/tests/agent_features/test_distributed_tracing.py +++ b/tests/agent_features/test_distributed_tracing.py @@ -15,8 +15,8 @@ import asyncio import copy import json -import time import random +import time import pytest import webtest @@ -1405,21 +1405,17 @@ def _test(): "distributed_tracing.sampler.full_granularity.enabled": False, "distributed_tracing.sampler.partial_granularity.enabled": False, }, - 0.123, # random + 0.123, # random False, ), ( # When dt is disabled. - { - "distributed_tracing.enabled": False, - }, - 0.123, # random + {"distributed_tracing.enabled": False}, + 0.123, # random False, ), ( # Verify when full granularity sampled +2 is added to the priority. - { - "distributed_tracing.sampler.full_granularity.root.trace_id_ratio_based.ratio": 1, - }, - 2.123, # random + 2 + {"distributed_tracing.sampler.full_granularity.root.trace_id_ratio_based.ratio": 1}, + 2.123, # random + 2 True, ), ( # Verify when partial granularity sampled +1 is added to the priority. @@ -1428,18 +1424,15 @@ def _test(): "distributed_tracing.sampler.partial_granularity.enabled": True, "distributed_tracing.sampler.partial_granularity.root.trace_id_ratio_based.ratio": 1, }, - 1.123, # random + 1 + 1.123, # random + 1 True, ), ), ) def test_distributed_trace_enabled_settings_set_correct_sampled_priority( - dt_settings, - expected_priority, - expected_sampled, - monkeypatch, + dt_settings, expected_priority, expected_sampled, monkeypatch ): - monkeypatch.setattr(random, 'random', lambda *args, **kwargs: 0.123) + monkeypatch.setattr(random, "random", lambda *args, **kwargs: 0.123) test_settings = _override_settings.copy() test_settings.update(dt_settings) @@ -1453,18 +1446,16 @@ def _test(): _test() -def test_distributed_trace_priority_set_when_only_sampled_set_in_headers( - monkeypatch, -): - monkeypatch.setattr(random, 'random', lambda *args, **kwargs: 0.123) +def test_distributed_trace_priority_set_when_only_sampled_set_in_headers(monkeypatch): + monkeypatch.setattr(random, "random", lambda *args, **kwargs: 0.123) @override_application_settings(_override_settings) @validate_transaction_object_attributes({"sampled": True, "priority": 2.123}) @background_task() def _test(): headers = { - "traceparent": f"00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01", - "tracestate": f"1@nr=0-0-1-2827902-0af7651916cd43dd-00f067aa0ba902b7-1--1518469636035" + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01", + "tracestate": "1@nr=0-0-1-2827902-0af7651916cd43dd-00f067aa0ba902b7-1--1518469636035", } accept_distributed_trace_headers(headers) From 858287b257eb1ad60b8e839bcb9732aa3c9d7b64 Mon Sep 17 00:00:00 2001 From: Hannah Stepanek Date: Thu, 11 Dec 2025 15:08:51 -0800 Subject: [PATCH 5/5] Fixup: test name --- tests/agent_features/test_distributed_tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/agent_features/test_distributed_tracing.py b/tests/agent_features/test_distributed_tracing.py index 8f278b022..46f441cfb 100644 --- a/tests/agent_features/test_distributed_tracing.py +++ b/tests/agent_features/test_distributed_tracing.py @@ -1446,7 +1446,7 @@ def _test(): _test() -def test_distributed_trace_priority_set_when_only_sampled_set_in_headers(monkeypatch): +def test_distributed_trace_priority_set_when_only_sampled_set_in_tracestate_header(monkeypatch): monkeypatch.setattr(random, "random", lambda *args, **kwargs: 0.123) @override_application_settings(_override_settings)