diff --git a/newrelic/api/cat_header_mixin.py b/newrelic/api/cat_header_mixin.py index fe5c0a71ff..b8251fdca1 100644 --- a/newrelic/api/cat_header_mixin.py +++ b/newrelic/api/cat_header_mixin.py @@ -22,6 +22,7 @@ class CatHeaderMixin(object): cat_transaction_key = 'X-NewRelic-Transaction' cat_appdata_key = 'X-NewRelic-App-Data' cat_synthetics_key = 'X-NewRelic-Synthetics' + cat_synthetics_info_key = 'X-NewRelic-Synthetics-Info' cat_metadata_key = 'x-newrelic-trace' cat_distributed_trace_key = 'newrelic' settings = None @@ -105,8 +106,9 @@ def generate_request_headers(cls, transaction): (cls.cat_transaction_key, encoded_transaction)) if transaction.synthetics_header: - nr_headers.append( - (cls.cat_synthetics_key, transaction.synthetics_header)) + nr_headers.append((cls.cat_synthetics_key, transaction.synthetics_header)) + if transaction.synthetics_info_header: + nr_headers.append((cls.cat_synthetics_info_key, transaction.synthetics_info_header)) return nr_headers diff --git a/newrelic/api/message_trace.py b/newrelic/api/message_trace.py index f564c41cb4..e0fa5956d0 100644 --- a/newrelic/api/message_trace.py +++ b/newrelic/api/message_trace.py @@ -27,6 +27,7 @@ class MessageTrace(CatHeaderMixin, TimeTrace): cat_transaction_key = "NewRelicTransaction" cat_appdata_key = "NewRelicAppData" cat_synthetics_key = "NewRelicSynthetics" + cat_synthetics_info_key = "NewRelicSyntheticsInfo" def __init__(self, library, operation, destination_type, destination_name, params=None, terminal=True, **kwargs): parent = kwargs.pop("parent", None) diff --git a/newrelic/api/transaction.py b/newrelic/api/transaction.py index 988b56be6e..bbe8f38522 100644 --- a/newrelic/api/transaction.py +++ b/newrelic/api/transaction.py @@ -44,6 +44,7 @@ json_decode, json_encode, obfuscate, + snake_case, ) from newrelic.core.attribute import ( MAX_ATTRIBUTE_LENGTH, @@ -303,10 +304,17 @@ def __init__(self, application, enabled=None, source=None): self._alternate_path_hashes = {} self.is_part_of_cat = False + # Synthetics Header self.synthetics_resource_id = None self.synthetics_job_id = None self.synthetics_monitor_id = None self.synthetics_header = None + + # Synthetics Info Header + self.synthetics_type = None + self.synthetics_initiator = None + self.synthetics_attributes = None + self.synthetics_info_header = None self._custom_metrics = CustomMetrics() self._dimensional_metrics = DimensionalMetrics() @@ -603,6 +611,10 @@ def __exit__(self, exc, value, tb): synthetics_job_id=self.synthetics_job_id, synthetics_monitor_id=self.synthetics_monitor_id, synthetics_header=self.synthetics_header, + synthetics_type=self.synthetics_type, + synthetics_initiator=self.synthetics_initiator, + synthetics_attributes=self.synthetics_attributes, + synthetics_info_header=self.synthetics_info_header, is_part_of_cat=self.is_part_of_cat, trip_id=self.trip_id, path_hash=self.path_hash, @@ -840,6 +852,16 @@ def trace_intrinsics(self): i_attrs["synthetics_job_id"] = self.synthetics_job_id if self.synthetics_monitor_id: i_attrs["synthetics_monitor_id"] = self.synthetics_monitor_id + if self.synthetics_type: + i_attrs["synthetics_type"] = self.synthetics_type + if self.synthetics_initiator: + i_attrs["synthetics_initiator"] = self.synthetics_initiator + if self.synthetics_attributes: + # Add all synthetics attributes + for k, v in self.synthetics_attributes.items(): + if k: + i_attrs["synthetics_%s" % snake_case(k)] = v + if self.total_time: i_attrs["totalTime"] = self.total_time if self._loop_time: diff --git a/newrelic/api/web_transaction.py b/newrelic/api/web_transaction.py index 9749e26194..47155425e1 100644 --- a/newrelic/api/web_transaction.py +++ b/newrelic/api/web_transaction.py @@ -125,6 +125,27 @@ def _parse_synthetics_header(header): return synthetics +def _parse_synthetics_info_header(header): + # Return a dictionary of values from SyntheticsInfo header + # Returns empty dict, if version is not supported. + + synthetics_info = {} + version = None + + try: + version = int(header.get("version")) + + if version == 1: + synthetics_info['version'] = version + synthetics_info['type'] = header.get("type") + synthetics_info['initiator'] = header.get("initiator") + synthetics_info['attributes'] = header.get("attributes") + except Exception: + return + + return synthetics_info + + def _remove_query_string(url): url = ensure_str(url) out = urlparse.urlsplit(url) @@ -231,6 +252,7 @@ def _process_synthetics_header(self): settings.trusted_account_ids and \ settings.encoding_key: + # Synthetics Header encoded_header = self._request_headers.get('x-newrelic-synthetics') encoded_header = encoded_header and ensure_str(encoded_header) if not encoded_header: @@ -241,11 +263,20 @@ def _process_synthetics_header(self): settings.encoding_key) synthetics = _parse_synthetics_header(decoded_header) + # Synthetics Info Header + encoded_info_header = self._request_headers.get('x-newrelic-synthetics-info') + encoded_info_header = encoded_info_header and ensure_str(encoded_info_header) + + decoded_info_header = decode_newrelic_header( + encoded_info_header, + settings.encoding_key) + synthetics_info = _parse_synthetics_info_header(decoded_info_header) + if synthetics and \ synthetics['account_id'] in \ settings.trusted_account_ids: - # Save obfuscated header, because we will pass it along + # Save obfuscated headers, because we will pass them along # unchanged in all external requests. self.synthetics_header = encoded_header @@ -253,6 +284,12 @@ def _process_synthetics_header(self): self.synthetics_job_id = synthetics['job_id'] self.synthetics_monitor_id = synthetics['monitor_id'] + if synthetics_info: + self.synthetics_info_header = encoded_info_header + self.synthetics_type = synthetics_info['type'] + self.synthetics_initiator = synthetics_info['initiator'] + self.synthetics_attributes = synthetics_info['attributes'] + def _process_context_headers(self): # Process the New Relic cross process ID header and extract # the relevant details. diff --git a/newrelic/common/encoding_utils.py b/newrelic/common/encoding_utils.py index ef8624240f..bee53df1a9 100644 --- a/newrelic/common/encoding_utils.py +++ b/newrelic/common/encoding_utils.py @@ -571,3 +571,46 @@ def decode(cls, payload, tk): data['pr'] = None return data + + +def capitalize(string): + """Capitalize the first letter of a string.""" + if not string: + return string + elif len(string) == 1: + return string.capitalize() + else: + return "".join((string[0].upper(), string[1:])) + + +def camel_case(string, upper=False): + """ + Convert a string of snake case to camel case. + + Setting upper=True will capitalize the first letter. Defaults to False, where no change is made to the first letter. + """ + string = ensure_str(string) + split_string = list(string.split("_")) + + if len(split_string) < 2: + if upper: + return capitalize(string) + else: + return string + else: + if upper: + camel_cased_string = "".join([capitalize(substr) for substr in split_string]) + else: + camel_cased_string = split_string[0] + "".join([capitalize(substr) for substr in split_string[1:]]) + + return camel_cased_string + + +_snake_case_re = re.compile(r"([A-Z]+[a-z]*)") +def snake_case(string): + """Convert a string of camel case to snake case. Assumes no repeated runs of capital letters.""" + string = ensure_str(string) + if "_" in string: + return string # Don't touch strings that are already snake cased + + return "_".join([s for s in _snake_case_re.split(string) if s]).lower() diff --git a/newrelic/core/transaction_node.py b/newrelic/core/transaction_node.py index d63d7f9b65..74216f7df2 100644 --- a/newrelic/core/transaction_node.py +++ b/newrelic/core/transaction_node.py @@ -22,6 +22,7 @@ import newrelic.core.error_collector import newrelic.core.trace_node +from newrelic.common.encoding_utils import camel_case from newrelic.common.streaming_utils import SpanProtoAttrs from newrelic.core.attribute import create_agent_attributes, create_user_attributes from newrelic.core.attribute_filter import ( @@ -76,6 +77,10 @@ "synthetics_job_id", "synthetics_monitor_id", "synthetics_header", + "synthetics_type", + "synthetics_initiator", + "synthetics_attributes", + "synthetics_info_header", "is_part_of_cat", "trip_id", "path_hash", @@ -586,6 +591,15 @@ def _event_intrinsics(self, stats_table): intrinsics["nr.syntheticsJobId"] = self.synthetics_job_id intrinsics["nr.syntheticsMonitorId"] = self.synthetics_monitor_id + if self.synthetics_type: + intrinsics["nr.syntheticsType"] = self.synthetics_type + intrinsics["nr.syntheticsInitiator"] = self.synthetics_initiator + if self.synthetics_attributes: + # Add all synthetics attributes + for k, v in self.synthetics_attributes.items(): + if k: + intrinsics["nr.synthetics%s" % camel_case(k, upper=True)] = v + def _add_call_time(source, target): # include time for keys previously added to stats table via # stats_engine.record_transaction diff --git a/tests/agent_features/test_error_events.py b/tests/agent_features/test_error_events.py index 72bdb14f7c..2e648271d0 100644 --- a/tests/agent_features/test_error_events.py +++ b/tests/agent_features/test_error_events.py @@ -20,7 +20,7 @@ from testing_support.fixtures import ( cat_enabled, make_cross_agent_headers, - make_synthetics_header, + make_synthetics_headers, override_application_settings, reset_core_stats_engine, validate_error_event_sample_data, @@ -43,6 +43,9 @@ SYNTHETICS_RESOURCE_ID = "09845779-16ef-4fa7-b7f2-44da8e62931c" SYNTHETICS_JOB_ID = "8c7dd3ba-4933-4cbb-b1ed-b62f511782f4" SYNTHETICS_MONITOR_ID = "dc452ae9-1a93-4ab5-8a33-600521e9cd00" +SYNTHETICS_TYPE = "scheduled" +SYNTHETICS_INITIATOR = "graphql" +SYNTHETICS_ATTRIBUTES = {"exampleAttribute": "1"} ERR_MESSAGE = "Transaction had bad value" ERROR = ValueError(ERR_MESSAGE) @@ -135,6 +138,9 @@ def test_transaction_error_cross_agent(): "nr.syntheticsResourceId": SYNTHETICS_RESOURCE_ID, "nr.syntheticsJobId": SYNTHETICS_JOB_ID, "nr.syntheticsMonitorId": SYNTHETICS_MONITOR_ID, + "nr.syntheticsType": SYNTHETICS_TYPE, + "nr.syntheticsInitiator": SYNTHETICS_INITIATOR, + "nr.syntheticsExampleAttribute": "1", } @@ -144,12 +150,15 @@ def test_transaction_error_with_synthetics(): "err_message": ERR_MESSAGE, } settings = application_settings() - headers = make_synthetics_header( + headers = make_synthetics_headers( + settings.encoding_key, settings.trusted_account_ids[0], SYNTHETICS_RESOURCE_ID, SYNTHETICS_JOB_ID, SYNTHETICS_MONITOR_ID, - settings.encoding_key, + SYNTHETICS_TYPE, + SYNTHETICS_INITIATOR, + SYNTHETICS_ATTRIBUTES, ) response = fully_featured_application.get("/", headers=headers, extra_environ=test_environ) diff --git a/tests/agent_features/test_synthetics.py b/tests/agent_features/test_synthetics.py index 2e08144cc7..350cab03f0 100644 --- a/tests/agent_features/test_synthetics.py +++ b/tests/agent_features/test_synthetics.py @@ -17,7 +17,7 @@ from testing_support.external_fixtures import validate_synthetics_external_trace_header from testing_support.fixtures import ( cat_enabled, - make_synthetics_header, + make_synthetics_headers, override_application_settings, ) from testing_support.validators.validate_synthetics_event import ( @@ -37,6 +37,9 @@ SYNTHETICS_RESOURCE_ID = "09845779-16ef-4fa7-b7f2-44da8e62931c" SYNTHETICS_JOB_ID = "8c7dd3ba-4933-4cbb-b1ed-b62f511782f4" SYNTHETICS_MONITOR_ID = "dc452ae9-1a93-4ab5-8a33-600521e9cd00" +SYNTHETICS_TYPE = "scheduled" +SYNTHETICS_INITIATOR = "graphql" +SYNTHETICS_ATTRIBUTES = {"exampleAttribute": "1"} _override_settings = { "encoding_key": ENCODING_KEY, @@ -45,15 +48,19 @@ } -def _make_synthetics_header( +def _make_synthetics_headers( version="1", account_id=ACCOUNT_ID, resource_id=SYNTHETICS_RESOURCE_ID, job_id=SYNTHETICS_JOB_ID, monitor_id=SYNTHETICS_MONITOR_ID, encoding_key=ENCODING_KEY, + info_version="1", + type_=SYNTHETICS_TYPE, + initiator=SYNTHETICS_INITIATOR, + attributes=SYNTHETICS_ATTRIBUTES, ): - return make_synthetics_header(account_id, resource_id, job_id, monitor_id, encoding_key, version) + return make_synthetics_headers(encoding_key, account_id, resource_id, job_id, monitor_id, type_, initiator, attributes, synthetics_version=version, synthetics_info_version=info_version) def decode_header(header, encoding_key=ENCODING_KEY): @@ -80,6 +87,9 @@ def target_wsgi_application(environ, start_response): ("nr.syntheticsResourceId", SYNTHETICS_RESOURCE_ID), ("nr.syntheticsJobId", SYNTHETICS_JOB_ID), ("nr.syntheticsMonitorId", SYNTHETICS_MONITOR_ID), + ("nr.syntheticsType", SYNTHETICS_TYPE), + ("nr.syntheticsInitiator", SYNTHETICS_INITIATOR), + ("nr.syntheticsExampleAttribute", "1"), ] _test_valid_synthetics_event_forgone = [] @@ -89,21 +99,51 @@ def target_wsgi_application(environ, start_response): ) @override_application_settings(_override_settings) def test_valid_synthetics_event(): - headers = _make_synthetics_header() + headers = _make_synthetics_headers() + response = target_application.get("/", headers=headers) + + +_test_valid_synthetics_event_without_info_required = [ + ("nr.syntheticsResourceId", SYNTHETICS_RESOURCE_ID), + ("nr.syntheticsJobId", SYNTHETICS_JOB_ID), + ("nr.syntheticsMonitorId", SYNTHETICS_MONITOR_ID), +] +_test_valid_synthetics_event_without_info_forgone = [ + "nr.syntheticsType", + "nr.syntheticsInitiator", + "nr.syntheticsExampleAttribute", +] + + +@validate_synthetics_event( + _test_valid_synthetics_event_without_info_required, _test_valid_synthetics_event_without_info_forgone, should_exist=True +) +@override_application_settings(_override_settings) +def test_valid_synthetics_event_without_info(): + headers = _make_synthetics_headers(type_=None, initiator=None, attributes=None) response = target_application.get("/", headers=headers) @validate_synthetics_event([], [], should_exist=False) @override_application_settings(_override_settings) def test_no_synthetics_event_unsupported_version(): - headers = _make_synthetics_header(version="0") + headers = _make_synthetics_headers(version="0") + response = target_application.get("/", headers=headers) + + +@validate_synthetics_event( + _test_valid_synthetics_event_without_info_required, _test_valid_synthetics_event_without_info_forgone, should_exist=True +) +@override_application_settings(_override_settings) +def test_synthetics_event_unsupported_info_version(): + headers = _make_synthetics_headers(info_version="0") response = target_application.get("/", headers=headers) @validate_synthetics_event([], [], should_exist=False) @override_application_settings(_override_settings) def test_no_synthetics_event_untrusted_account(): - headers = _make_synthetics_header(account_id="999") + headers = _make_synthetics_headers(account_id="999") response = target_application.get("/", headers=headers) @@ -111,7 +151,20 @@ def test_no_synthetics_event_untrusted_account(): @override_application_settings(_override_settings) def test_no_synthetics_event_mismatched_encoding_key(): encoding_key = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" - headers = _make_synthetics_header(encoding_key=encoding_key) + headers = _make_synthetics_headers(encoding_key=encoding_key) + response = target_application.get("/", headers=headers) + + +@validate_synthetics_event( + _test_valid_synthetics_event_without_info_required, _test_valid_synthetics_event_without_info_forgone, should_exist=True +) +@override_application_settings(_override_settings) +def test_synthetics_event_mismatched_info_encoding_key(): + encoding_key = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + headers = { + "X-NewRelic-Synthetics": _make_synthetics_headers(type_=None)["X-NewRelic-Synthetics"], + "X-NewRelic-Synthetics-Info": _make_synthetics_headers(encoding_key=encoding_key)["X-NewRelic-Synthetics-Info"], + } response = target_application.get("/", headers=headers) @@ -119,6 +172,9 @@ def test_no_synthetics_event_mismatched_encoding_key(): "synthetics_resource_id": SYNTHETICS_RESOURCE_ID, "synthetics_job_id": SYNTHETICS_JOB_ID, "synthetics_monitor_id": SYNTHETICS_MONITOR_ID, + "synthetics_type": SYNTHETICS_TYPE, + "synthetics_initiator": SYNTHETICS_INITIATOR, + "synthetics_example_attribute": "1", } @@ -126,7 +182,7 @@ def test_no_synthetics_event_mismatched_encoding_key(): @validate_synthetics_transaction_trace(_test_valid_synthetics_tt_required) @override_application_settings(_override_settings) def test_valid_synthetics_in_transaction_trace(): - headers = _make_synthetics_header() + headers = _make_synthetics_headers() response = target_application.get("/", headers=headers) @@ -146,26 +202,36 @@ def test_no_synthetics_in_transaction_trace(): @validate_synthetics_event([], [], should_exist=False) @override_application_settings(_disabled_settings) def test_synthetics_disabled(): - headers = _make_synthetics_header() + headers = _make_synthetics_headers() response = target_application.get("/", headers=headers) -_external_synthetics_header = ("X-NewRelic-Synthetics", _make_synthetics_header()["X-NewRelic-Synthetics"]) +_external_synthetics_headers = _make_synthetics_headers() +_external_synthetics_header = _external_synthetics_headers["X-NewRelic-Synthetics"] +_external_synthetics_info_header = _external_synthetics_headers["X-NewRelic-Synthetics-Info"] @cat_enabled -@validate_synthetics_external_trace_header(required_header=_external_synthetics_header, should_exist=True) +@validate_synthetics_external_trace_header(_external_synthetics_header, _external_synthetics_info_header) @override_application_settings(_override_settings) def test_valid_synthetics_external_trace_header(): - headers = _make_synthetics_header() + headers = _make_synthetics_headers() + response = target_application.get("/", headers=headers) + + +@cat_enabled +@validate_synthetics_external_trace_header(_external_synthetics_header, None) +@override_application_settings(_override_settings) +def test_valid_synthetics_external_trace_header_without_info(): + headers = _make_synthetics_headers(type_=None) response = target_application.get("/", headers=headers) @cat_enabled -@validate_synthetics_external_trace_header(required_header=_external_synthetics_header, should_exist=True) +@validate_synthetics_external_trace_header(_external_synthetics_header, _external_synthetics_info_header) @override_application_settings(_override_settings) def test_valid_external_trace_header_with_byte_inbound_header(): - headers = _make_synthetics_header() + headers = _make_synthetics_headers() headers = {k.encode("utf-8"): v.encode("utf-8") for k, v in headers.items()} @web_transaction( @@ -178,7 +244,7 @@ def webapp(): webapp() -@validate_synthetics_external_trace_header(should_exist=False) +@validate_synthetics_external_trace_header(None, None) @override_application_settings(_override_settings) def test_no_synthetics_external_trace_header(): response = target_application.get("/") @@ -194,7 +260,7 @@ def _synthetics_limit_test(num_requests, num_events, num_transactions): # Send requests - headers = _make_synthetics_header() + headers = _make_synthetics_headers() for i in range(num_requests): response = target_application.get("/", headers=headers) diff --git a/tests/agent_unittests/test_encoding_utils.py b/tests/agent_unittests/test_encoding_utils.py new file mode 100644 index 0000000000..397f2fa2ef --- /dev/null +++ b/tests/agent_unittests/test_encoding_utils.py @@ -0,0 +1,52 @@ +# 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. + +import pytest + +from newrelic.common.encoding_utils import camel_case, snake_case + + +@pytest.mark.parametrize("input_,expected,upper", [ + ("", "", False), + ("", "", True), + ("my_string", "myString", False), + ("my_string", "MyString", True), + ("LeaveCase", "LeaveCase", False), + ("correctCase", "CorrectCase", True), + ("UPPERcaseLETTERS", "UPPERcaseLETTERS", False), + ("UPPERcaseLETTERS", "UPPERcaseLETTERS", True), + ("lowerCASEletters", "lowerCASEletters", False), + ("lowerCASEletters", "LowerCASEletters", True), + ("very_long_snake_string", "VeryLongSnakeString", True), + ("kebab-case", "kebab-case", False), +]) +def test_camel_case(input_, expected, upper): + output = camel_case(input_, upper=upper) + assert output == expected + + +@pytest.mark.parametrize("input_,expected", [ + ("", ""), + ("", ""), + ("my_string", "my_string"), + ("myString", "my_string"), + ("MyString", "my_string"), + ("UPPERcaseLETTERS", "uppercase_letters"), + ("lowerCASEletters", "lower_caseletters"), + ("VeryLongCamelString", "very_long_camel_string"), + ("kebab-case", "kebab-case"), +]) +def test_snake_case(input_, expected): + output = snake_case(input_) + assert output == expected diff --git a/tests/agent_unittests/test_harvest_loop.py b/tests/agent_unittests/test_harvest_loop.py index 15b67a81e1..a3eaf7b5ff 100644 --- a/tests/agent_unittests/test_harvest_loop.py +++ b/tests/agent_unittests/test_harvest_loop.py @@ -143,6 +143,10 @@ def transaction_node(request): synthetics_job_id=None, synthetics_monitor_id=None, synthetics_header=None, + synthetics_type=None, + synthetics_initiator=None, + synthetics_attributes=None, + synthetics_info_header=None, is_part_of_cat=False, trip_id="4485b89db608aece", path_hash=None, diff --git a/tests/testing_support/external_fixtures.py b/tests/testing_support/external_fixtures.py index a968fe2d1e..de746c38be 100644 --- a/tests/testing_support/external_fixtures.py +++ b/tests/testing_support/external_fixtures.py @@ -51,8 +51,10 @@ def create_incoming_headers(transaction): return headers -def validate_synthetics_external_trace_header(required_header=(), - should_exist=True): +def validate_synthetics_external_trace_header( + synthetics_header, + synthetics_info_header, + ): @transient_function_wrapper('newrelic.core.stats_engine', 'StatsEngine.record_transaction') def _validate_synthetics_external_trace_header(wrapped, instance, @@ -67,34 +69,46 @@ def _bind_params(transaction, *args, **kwargs): except: raise else: - if should_exist: - # XXX This validation routine is technically - # broken as the argument to record_transaction() - # is not actually an instance of the Transaction - # object. Instead it is a TransactionNode object. - # The static method generate_request_headers() is - # expecting a Transaction object and not - # TransactionNode. The latter provides attributes - # which are not updatable by the static method - # generate_request_headers(), which it wants to - # update, so would fail. For now what we do is use - # a little proxy wrapper so that updates do not - # fail. The use of this wrapper needs to be - # reviewed and a better way of achieving what is - # required found. - - class _Transaction(object): - def __init__(self, wrapped): - self.__wrapped__ = wrapped - - def __getattr__(self, name): - return getattr(self.__wrapped__, name) - - external_headers = ExternalTrace.generate_request_headers( - _Transaction(transaction)) - assert required_header in external_headers, ( - 'required_header=%r, ''external_headers=%r' % ( - required_header, external_headers)) + # XXX This validation routine is technically + # broken as the argument to record_transaction() + # is not actually an instance of the Transaction + # object. Instead it is a TransactionNode object. + # The static method generate_request_headers() is + # expecting a Transaction object and not + # TransactionNode. The latter provides attributes + # which are not updatable by the static method + # generate_request_headers(), which it wants to + # update, so would fail. For now what we do is use + # a little proxy wrapper so that updates do not + # fail. The use of this wrapper needs to be + # reviewed and a better way of achieving what is + # required found. + + class _Transaction(object): + def __init__(self, wrapped): + self.__wrapped__ = wrapped + + def __getattr__(self, name): + return getattr(self.__wrapped__, name, lambda *args, **kwargs: None) + + external_headers = ExternalTrace.generate_request_headers( + _Transaction(transaction)) + external_headers = {header[0]: header[1] for header in external_headers} + + if synthetics_header: + assert synthetics_header == external_headers["X-NewRelic-Synthetics"], ( + 'synthetics_header=%r, external_headers=%r' % ( + synthetics_header, external_headers)) + else: + assert "X-NewRelic-Synthetics" not in external_headers + + if synthetics_info_header: + assert synthetics_info_header == external_headers["X-NewRelic-Synthetics-Info"], ( + 'synthetics_info_header=%r, external_headers=%r' % ( + synthetics_info_header, external_headers)) + else: + assert "X-NewRelic-Synthetics-Info" not in external_headers + return result diff --git a/tests/testing_support/fixtures.py b/tests/testing_support/fixtures.py index 883c3ec595..fab0150d52 100644 --- a/tests/testing_support/fixtures.py +++ b/tests/testing_support/fixtures.py @@ -363,12 +363,26 @@ def make_cross_agent_headers(payload, encoding_key, cat_id): return {"X-NewRelic-Transaction": value, "X-NewRelic-ID": id_value} +def make_synthetics_headers(encoding_key, account_id, resource_id, job_id, monitor_id, type_, initiator, attributes, synthetics_version=1, synthetics_info_version=1): + headers = {} + headers.update(make_synthetics_header(account_id, resource_id, job_id, monitor_id, encoding_key, synthetics_version)) + if type_: + headers.update(make_synthetics_info_header(type_, initiator, attributes, encoding_key, synthetics_info_version)) + return headers + + def make_synthetics_header(account_id, resource_id, job_id, monitor_id, encoding_key, version=1): value = [version, account_id, resource_id, job_id, monitor_id] value = obfuscate(json_encode(value), encoding_key) return {"X-NewRelic-Synthetics": value} +def make_synthetics_info_header(type_, initiator, attributes, encoding_key, version=1): + value = {"version": version, "type": type_, "initiator": initiator, "attributes": attributes} + value = obfuscate(json_encode(value), encoding_key) + return {"X-NewRelic-Synthetics-Info": value} + + def capture_transaction_metrics(metrics_list, full_metrics=None): @transient_function_wrapper("newrelic.core.stats_engine", "StatsEngine.record_transaction") @catch_background_exceptions @@ -744,6 +758,9 @@ def _bind_params(transaction, *args, **kwargs): return _validate_error_event_sample_data +SYNTHETICS_INTRINSIC_ATTR_NAMES = set(["nr.syntheticsResourceId", "nr.syntheticsJobId", "nr.syntheticsMonitorId", "nr.syntheticsType", "nr.syntheticsInitiator"]) + + def _validate_event_attributes(intrinsics, user_attributes, required_intrinsics, required_user): now = time.time() assert isinstance(intrinsics["timestamp"], int) @@ -793,6 +810,16 @@ def _validate_event_attributes(intrinsics, user_attributes, required_intrinsics, assert intrinsics["nr.syntheticsResourceId"] == res_id assert intrinsics["nr.syntheticsJobId"] == job_id assert intrinsics["nr.syntheticsMonitorId"] == monitor_id + + if "nr.syntheticsType" in required_intrinsics: + type_ = required_intrinsics["nr.syntheticsType"] + initiator = required_intrinsics["nr.syntheticsInitiator"] + assert intrinsics["nr.syntheticsType"] == type_ + assert intrinsics["nr.syntheticsInitiator"] == initiator + + for k, v in required_intrinsics.items(): + if k.startswith("nr.synthetics") and k not in SYNTHETICS_INTRINSIC_ATTR_NAMES: + assert v == intrinsics[k] if "port" in required_intrinsics: assert intrinsics["port"] == required_intrinsics["port"] diff --git a/tests/testing_support/validators/validate_synthetics_event.py b/tests/testing_support/validators/validate_synthetics_event.py index 221cf7e6ef..bab176138d 100644 --- a/tests/testing_support/validators/validate_synthetics_event.py +++ b/tests/testing_support/validators/validate_synthetics_event.py @@ -51,8 +51,8 @@ def _flatten(event): assert name in flat_event, "name=%r, event=%r" % (name, flat_event) assert flat_event[name] == value, "name=%r, value=%r, event=%r" % (name, value, flat_event) - for name, value in forgone_attrs: - assert name not in flat_event, "name=%r, value=%r, event=%r" % (name, value, flat_event) + for name in forgone_attrs: + assert name not in flat_event, "name=%r, event=%r" % (name, flat_event) except Exception as e: failed.append(e)