diff --git a/.github/workflows/deploy-python.yml b/.github/workflows/deploy-python.yml index a579703034..fe16ee4854 100644 --- a/.github/workflows/deploy-python.yml +++ b/.github/workflows/deploy-python.yml @@ -20,125 +20,17 @@ on: - published jobs: - build-linux-py3: + deploy-linux: runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - wheel: - - cp37-manylinux - - cp37-musllinux - - cp38-manylinux - - cp38-musllinux - - cp39-manylinux - - cp39-musllinux - - cp310-manylinux - - cp310-musllinux - - cp311-manylinux - - cp311-musllinux - - cp312-manylinux - - cp312-musllinux steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: persist-credentials: false fetch-depth: 0 - name: Setup QEMU - uses: docker/setup-qemu-action@v3 - - - name: Build Wheels - uses: pypa/cibuildwheel@v2.16.2 - env: - CIBW_PLATFORM: linux - CIBW_BUILD: "${{ matrix.wheel }}*" - CIBW_ARCHS_LINUX: x86_64 aarch64 - CIBW_ENVIRONMENT: "LD_LIBRARY_PATH=/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst:/usr/local/lib64:/usr/local/lib" - CIBW_TEST_REQUIRES: pytest - CIBW_TEST_COMMAND: "PYTHONPATH={project}/tests pytest {project}/tests/agent_unittests -vx" - - - name: Upload Artifacts - uses: actions/upload-artifact@v4.0.0 - with: - name: ${{ github.job }}-${{ matrix.wheel }} - path: ./wheelhouse/*.whl - retention-days: 1 - - build-linux-py2: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 0 - - - name: Setup QEMU - uses: docker/setup-qemu-action@v3 - - - name: Build Wheels - uses: pypa/cibuildwheel@v1.12.0 - env: - CIBW_PLATFORM: linux - CIBW_BUILD: cp27-manylinux_x86_64 - CIBW_ARCHS_LINUX: x86_64 - CIBW_ENVIRONMENT: "LD_LIBRARY_PATH=/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst:/usr/local/lib64:/usr/local/lib" - CIBW_TEST_REQUIRES: pytest==4.6.11 - CIBW_TEST_COMMAND: "PYTHONPATH={project}/tests pytest {project}/tests/agent_unittests -vx" - - - name: Upload Artifacts - uses: actions/upload-artifact@v4.0.0 - with: - name: ${{ github.job }} - path: ./wheelhouse/*.whl - retention-days: 1 - - build-sdist: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 0 - - - name: Install Dependencies - run: | - pip install -U pip - pip install -U setuptools - - - name: Build Source Package - run: | - python setup.py sdist - - - name: Prepare MD5 Hash File - run: | - tarball="$(python setup.py --fullname).tar.gz" - md5_file=${tarball}.md5 - openssl md5 -binary dist/${tarball} | xxd -p | tr -d '\n' > dist/${md5_file} - - - name: Upload Artifacts - uses: actions/upload-artifact@v4.0.0 - with: - name: ${{ github.job }}-sdist - path: | - ./dist/*.tar.gz - ./dist/*.tar.gz.md5 - retention-days: 1 - - deploy: - runs-on: ubuntu-latest - - needs: - - build-linux-py3 - - build-linux-py2 - - build-sdist - - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 0 + uses: docker/setup-qemu-action@v1 - uses: actions/setup-python@v2 with: @@ -150,22 +42,32 @@ jobs: pip install -U pip pip install -U wheel setuptools twine - - name: Download Artifacts - uses: actions/download-artifact@v4.1.0 - with: - path: ./artifacts/ + - name: Build Source Package + run: python setup.py sdist - - name: Unpack Artifacts - run: | - mkdir -p dist/ - mv artifacts/**/*{.whl,.tar.gz,.tar.gz.md5} dist/ + - name: Build Manylinux Wheels (Python 2) + uses: pypa/cibuildwheel@v1.12.0 + env: + CIBW_PLATFORM: linux + CIBW_BUILD: cp27-manylinux_x86_64 + CIBW_ARCHS: x86_64 + CIBW_ENVIRONMENT: "LD_LIBRARY_PATH=/opt/rh/=vtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst:/usr/local/lib64:/usr/local/lib" + + - name: Build Manylinux Wheels (Python 3) + uses: pypa/cibuildwheel@v2.11.1 + env: + CIBW_PLATFORM: linux + CIBW_BUILD: cp37-manylinux* cp38-manylinux* cp39-manylinux* cp310-manylinux* cp311-manylinux* + CIBW_ARCHS: x86_64 aarch64 + CIBW_ENVIRONMENT: "LD_LIBRARY_PATH=/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst:/usr/local/lib64:/usr/local/lib" - name: Upload Package to S3 run: | tarball="$(python setup.py --fullname).tar.gz" - md5_file=${tarball}.md5 - aws s3 cp dist/${md5_file} $S3_DST/${md5_file} - aws s3 cp dist/${tarball} $S3_DST/${tarball} + md5_file=$(mktemp) + openssl md5 -binary dist/$tarball | xxd -p | tr -d '\n' > $md5_file + aws s3 cp $md5_file $S3_DST/${tarball}.md5 + aws s3 cp dist/$tarball $S3_DST/$tarball env: S3_DST: s3://nr-downloads-main/python_agent/release AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -174,7 +76,7 @@ jobs: - name: Upload Package to PyPI run: | - twine upload --non-interactive dist/*.tar.gz dist/*.whl + twine upload --non-interactive dist/*.tar.gz wheelhouse/*-manylinux*.whl env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d525b7df4d..12081d1ee7 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -228,3 +228,14 @@ entering the directory of the tests you want to run. Then, run the following command: ``tox -c tox.ini -e [test environment]`` + +******* + Slack +******* + +We host a public Slack with a dedicated channel for contributors and +maintainers of open source projects hosted by New Relic. If you are +contributing to this project, you're welcome to request access to the +#oss-contributors channel in the newrelicusers.slack.com workspace. To +request access, please use this `link +`__. diff --git a/newrelic/agent.py b/newrelic/agent.py index 9665cb9d22..2c7f0fb858 100644 --- a/newrelic/agent.py +++ b/newrelic/agent.py @@ -15,7 +15,7 @@ from newrelic.api.application import application_instance as __application from newrelic.api.application import application_settings as __application_settings from newrelic.api.application import register_application as __register_application -from newrelic.api.log import NewRelicContextFormatter as __NewRelicContextFormatter +from newrelic.api.log import NewRelicContextFormatter # noqa from newrelic.api.time_trace import ( add_custom_span_attribute as __add_custom_span_attribute, ) @@ -177,7 +177,6 @@ def __asgi_application(*args, **kwargs): from newrelic.common.object_wrapper import FunctionWrapper as __FunctionWrapper from newrelic.common.object_wrapper import InFunctionWrapper as __InFunctionWrapper from newrelic.common.object_wrapper import ObjectProxy as __ObjectProxy -from newrelic.common.object_wrapper import CallableObjectProxy as __CallableObjectProxy from newrelic.common.object_wrapper import ObjectWrapper as __ObjectWrapper from newrelic.common.object_wrapper import OutFunctionWrapper as __OutFunctionWrapper from newrelic.common.object_wrapper import PostFunctionWrapper as __PostFunctionWrapper @@ -277,7 +276,6 @@ def __asgi_application(*args, **kwargs): wrap_background_task = __wrap_api_call(__wrap_background_task, "wrap_background_task") LambdaHandlerWrapper = __wrap_api_call(__LambdaHandlerWrapper, "LambdaHandlerWrapper") lambda_handler = __wrap_api_call(__lambda_handler, "lambda_handler") -NewRelicContextFormatter = __wrap_api_call(__NewRelicContextFormatter, "NewRelicContextFormatter") transaction_name = __wrap_api_call(__transaction_name, "transaction_name") TransactionNameWrapper = __wrap_api_call(__TransactionNameWrapper, "TransactionNameWrapper") wrap_transaction_name = __wrap_api_call(__wrap_transaction_name, "wrap_transaction_name") @@ -318,7 +316,6 @@ def __asgi_application(*args, **kwargs): wrap_message_transaction = __wrap_api_call(__wrap_message_transaction, "wrap_message_transaction") callable_name = __wrap_api_call(__callable_name, "callable_name") ObjectProxy = __wrap_api_call(__ObjectProxy, "ObjectProxy") -CallableObjectProxy = __wrap_api_call(__CallableObjectProxy, "CallableObjectProxy") wrap_object = __wrap_api_call(__wrap_object, "wrap_object") wrap_object_attribute = __wrap_api_call(__wrap_object_attribute, "wrap_object_attribute") resolve_path = __wrap_api_call(__resolve_path, "resolve_path") diff --git a/newrelic/api/asgi_application.py b/newrelic/api/asgi_application.py index 475faa7cbb..201e8643e6 100644 --- a/newrelic/api/asgi_application.py +++ b/newrelic/api/asgi_application.py @@ -157,9 +157,16 @@ async def send_inject_browser_agent(self, message): # if there's a valid body string, attempt to insert the HTML if verify_body_exists(self.body): - body = insert_html_snippet( - self.body, lambda: six.b(self.transaction.browser_timing_header()), self.search_maximum - ) + header = self.transaction.browser_timing_header() + if not header: + # If there's no header, abort browser monitoring injection + await self.send_buffered() + return + + footer = self.transaction.browser_timing_footer() + browser_agent_data = six.b(header) + six.b(footer) + + body = insert_html_snippet(self.body, lambda: browser_agent_data, self.search_maximum) # If we have inserted the browser agent if len(body) != len(self.body): diff --git a/newrelic/api/solr_trace.py b/newrelic/api/solr_trace.py index 6907f20f8b..e482158ee9 100644 --- a/newrelic/api/solr_trace.py +++ b/newrelic/api/solr_trace.py @@ -14,7 +14,6 @@ import newrelic.api.object_wrapper import newrelic.api.time_trace -import newrelic.common.object_wrapper import newrelic.core.solr_node @@ -112,4 +111,4 @@ def decorator(wrapped): def wrap_solr_trace(module, object_path, library, command): - newrelic.common.object_wrapper.wrap_object(module, object_path, SolrTraceWrapper, (library, command)) + newrelic.api.object_wrapper.wrap_object(module, object_path, SolrTraceWrapper, (library, command)) diff --git a/newrelic/api/transaction.py b/newrelic/api/transaction.py index f581d1a519..b003312abe 100644 --- a/newrelic/api/transaction.py +++ b/newrelic/api/transaction.py @@ -1957,10 +1957,9 @@ def get_browser_timing_header(nonce=None): def get_browser_timing_footer(nonce=None): - warnings.warn( - "The get_browser_timing_footer function is deprecated. Please migrate to only using the get_browser_timing_header API instead.", - DeprecationWarning, - ) + transaction = current_transaction() + if transaction and hasattr(transaction, "browser_timing_footer"): + return transaction.browser_timing_footer(nonce) return "" diff --git a/newrelic/api/web_transaction.py b/newrelic/api/web_transaction.py index 1dfa390b6c..e87ffe9689 100644 --- a/newrelic/api/web_transaction.py +++ b/newrelic/api/web_transaction.py @@ -13,8 +13,8 @@ # limitations under the License. import functools -import logging import time +import logging import warnings try: @@ -24,22 +24,23 @@ from newrelic.api.application import Application, application_instance from newrelic.api.transaction import Transaction, current_transaction -from newrelic.common.async_proxy import TransactionContext, async_proxy -from newrelic.common.encoding_utils import ( - decode_newrelic_header, - ensure_str, - json_encode, - obfuscate, -) -from newrelic.common.object_names import callable_name -from newrelic.common.object_wrapper import FunctionWrapper, wrap_object + +from newrelic.common.async_proxy import async_proxy, TransactionContext +from newrelic.common.encoding_utils import (obfuscate, json_encode, + decode_newrelic_header, ensure_str) + from newrelic.core.attribute import create_attributes, process_user_attribute from newrelic.core.attribute_filter import DST_BROWSER_MONITORING, DST_NONE + from newrelic.packages import six +from newrelic.common.object_names import callable_name +from newrelic.common.object_wrapper import FunctionWrapper, wrap_object + _logger = logging.getLogger(__name__) -_js_agent_header_fragment = '' +_js_agent_header_fragment = '' +_js_agent_footer_fragment = '' # Seconds since epoch for Jan 1 2000 JAN_1_2000 = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, 0)) @@ -79,8 +80,8 @@ def _parse_time_stamp(time_stamp): return converted_time -TRUE_VALUES = {"on", "true", "1"} -FALSE_VALUES = {"off", "false", "0"} +TRUE_VALUES = {'on', 'true', '1'} +FALSE_VALUES = {'off', 'false', '0'} def _lookup_environ_setting(environ, name, default=False): @@ -112,11 +113,11 @@ def _parse_synthetics_header(header): version = int(header[0]) if version == 1: - synthetics["version"] = version - synthetics["account_id"] = int(header[1]) - synthetics["resource_id"] = header[2] - synthetics["job_id"] = header[3] - synthetics["monitor_id"] = header[4] + synthetics['version'] = version + synthetics['account_id'] = int(header[1]) + synthetics['resource_id'] = header[2] + synthetics['job_id'] = header[3] + synthetics['monitor_id'] = header[4] except Exception: return @@ -134,10 +135,10 @@ def _parse_synthetics_info_header(header): 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") + 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 @@ -147,11 +148,11 @@ def _parse_synthetics_info_header(header): def _remove_query_string(url): url = ensure_str(url) out = urlparse.urlsplit(url) - return urlparse.urlunsplit((out.scheme, out.netloc, out.path, "", "")) + return urlparse.urlunsplit((out.scheme, out.netloc, out.path, '', '')) def _is_websocket(environ): - return environ.get("HTTP_UPGRADE", "").lower() == "websocket" + return environ.get('HTTP_UPGRADE', '').lower() == 'websocket' def _encode_nonce(nonce): @@ -170,27 +171,20 @@ def _encode_nonce(nonce): class WebTransaction(Transaction): unicode_error_reported = False - QUEUE_TIME_HEADERS = ("x-request-start", "x-queue-start") - - def __init__( - self, - application, - name, - group=None, - scheme=None, - host=None, - port=None, - request_method=None, - request_path=None, - query_string=None, - headers=None, - enabled=None, - source=None, - ): + QUEUE_TIME_HEADERS = ('x-request-start', 'x-queue-start') + + def __init__(self, application, name, group=None, + scheme=None, host=None, port=None, request_method=None, + request_path=None, query_string=None, headers=None, + enabled=None, source=None): + super(WebTransaction, self).__init__(application, enabled, source=source) - # Flag for tracking whether RUM header has been generated. + # Flags for tracking whether RUM header and footer have been + # generated. + self.rum_header_generated = False + self.rum_footer_generated = False if not self.enabled: return @@ -228,7 +222,9 @@ def __init__( if query_string and not self._settings.high_security: query_string = ensure_str(query_string) try: - params = urlparse.parse_qs(query_string, keep_blank_values=True) + params = urlparse.parse_qs( + query_string, + keep_blank_values=True) self._request_params.update(params) except Exception: pass @@ -240,7 +236,7 @@ def __init__( if name is not None: self.set_transaction_name(name, group, priority=1) elif request_path is not None: - self.set_transaction_name(request_path, "Uri", priority=1) + self.set_transaction_name(request_path, 'Uri', priority=1) def _process_queue_time(self): for queue_time_header in self.QUEUE_TIME_HEADERS: @@ -250,7 +246,7 @@ def _process_queue_time(self): value = ensure_str(value) try: - if value.startswith("t="): + if value.startswith('t='): self.queue_start = _parse_time_stamp(float(value[2:])) else: self.queue_start = _parse_time_stamp(float(value)) @@ -265,37 +261,47 @@ def _process_synthetics_header(self): settings = self._settings - if settings.synthetics.enabled and settings.trusted_account_ids and settings.encoding_key: + if settings.synthetics.enabled and \ + settings.trusted_account_ids and \ + settings.encoding_key: + # Synthetics Header - encoded_header = self._request_headers.get("x-newrelic-synthetics") + encoded_header = self._request_headers.get('x-newrelic-synthetics') encoded_header = encoded_header and ensure_str(encoded_header) if not encoded_header: return - decoded_header = decode_newrelic_header(encoded_header, settings.encoding_key) + decoded_header = decode_newrelic_header( + encoded_header, + 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 = 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) + 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: + if synthetics and \ + synthetics['account_id'] in \ + settings.trusted_account_ids: + # Save obfuscated headers, because we will pass them along # unchanged in all external requests. self.synthetics_header = encoded_header - self.synthetics_resource_id = synthetics["resource_id"] - self.synthetics_job_id = synthetics["job_id"] - self.synthetics_monitor_id = synthetics["monitor_id"] + self.synthetics_resource_id = synthetics['resource_id'] + 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"] + 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 @@ -303,9 +309,11 @@ def _process_context_headers(self): if self._settings.distributed_tracing.enabled: self.accept_distributed_trace_headers(self._request_headers) else: - client_cross_process_id = self._request_headers.get("x-newrelic-id") - txn_header = self._request_headers.get("x-newrelic-transaction") - self._process_incoming_cat_headers(client_cross_process_id, txn_header) + client_cross_process_id = \ + self._request_headers.get('x-newrelic-id') + txn_header = self._request_headers.get('x-newrelic-transaction') + self._process_incoming_cat_headers(client_cross_process_id, + txn_header) def process_response(self, status_code, response_headers): """Processes response status and headers, extracting any @@ -344,41 +352,50 @@ def process_response(self, status_code, response_headers): # Generate CAT response headers try: - read_length = int(self._request_headers.get("content-length")) + read_length = int(self._request_headers.get('content-length')) except Exception: read_length = -1 return self._generate_response_headers(read_length) def _update_agent_attributes(self): - if "accept" in self._request_headers: - self._add_agent_attribute("request.headers.accept", self._request_headers["accept"]) + if 'accept' in self._request_headers: + self._add_agent_attribute('request.headers.accept', + self._request_headers['accept']) try: - content_length = int(self._request_headers["content-length"]) - self._add_agent_attribute("request.headers.contentLength", content_length) + content_length = int(self._request_headers['content-length']) + self._add_agent_attribute('request.headers.contentLength', + content_length) except: pass - if "content-type" in self._request_headers: - self._add_agent_attribute("request.headers.contentType", self._request_headers["content-type"]) - if "host" in self._request_headers: - self._add_agent_attribute("request.headers.host", self._request_headers["host"]) - if "referer" in self._request_headers: - self._add_agent_attribute("request.headers.referer", _remove_query_string(self._request_headers["referer"])) - if "user-agent" in self._request_headers: - self._add_agent_attribute("request.headers.userAgent", self._request_headers["user-agent"]) + if 'content-type' in self._request_headers: + self._add_agent_attribute('request.headers.contentType', + self._request_headers['content-type']) + if 'host' in self._request_headers: + self._add_agent_attribute('request.headers.host', + self._request_headers['host']) + if 'referer' in self._request_headers: + self._add_agent_attribute('request.headers.referer', + _remove_query_string(self._request_headers['referer'])) + if 'user-agent' in self._request_headers: + self._add_agent_attribute('request.headers.userAgent', + self._request_headers['user-agent']) if self._request_method: - self._add_agent_attribute("request.method", self._request_method) + self._add_agent_attribute('request.method', self._request_method) if self._request_uri: - self._add_agent_attribute("request.uri", self._request_uri) + self._add_agent_attribute('request.uri', self._request_uri) try: - content_length = int(self._response_headers["content-length"]) - self._add_agent_attribute("response.headers.contentLength", content_length) + content_length = int(self._response_headers['content-length']) + self._add_agent_attribute('response.headers.contentLength', + content_length) except: pass - if "content-type" in self._response_headers: - self._add_agent_attribute("response.headers.contentType", self._response_headers["content-type"]) + if 'content-type' in self._response_headers: + self._add_agent_attribute('response.headers.contentType', + self._response_headers['content-type']) if self._response_code is not None: - self._add_agent_attribute("response.status", str(self._response_code)) + self._add_agent_attribute('response.status', + str(self._response_code)) return super(WebTransaction, self)._update_agent_attributes() @@ -392,39 +409,39 @@ def browser_timing_header(self, nonce=None): """ if not self.enabled: - return "" + return '' if self._state != self.STATE_RUNNING: - return "" + return '' if self.background_task: - return "" + return '' if self.ignore_transaction: - return "" + return '' if not self._settings: - return "" + return '' if not self._settings.browser_monitoring.enabled: - return "" + return '' if not self._settings.license_key: - return "" + return '' # Don't return the header a second time if it has already # been generated. if self.rum_header_generated: - return "" + return '' # Requirement is that the first 13 characters of the account # license key is used as the key when obfuscating values for - # the RUM configuration. Will not be able to perform the obfuscation + # the RUM footer. Will not be able to perform the obfuscation # if license key isn't that long for some reason. if len(self._settings.license_key) < 13: - return "" + return '' # Return the RUM header only if the agent received a valid value # for js_agent_loader from the data collector. The data @@ -433,48 +450,7 @@ def browser_timing_header(self, nonce=None): # 'none'. if self._settings.js_agent_loader: - # Make sure we freeze the path. - - self._freeze_path() - - # When obfuscating values for the browser agent configuration, we only use the - # first 13 characters of the account license key. - - obfuscation_key = self._settings.license_key[:13] - - attributes = {} - - user_attributes = {} - for attr in self.user_attributes: - if attr.destinations & DST_BROWSER_MONITORING: - user_attributes[attr.name] = attr.value - - if user_attributes: - attributes["u"] = user_attributes - - request_parameters = self.request_parameters - request_parameter_attributes = self.filter_request_parameters(request_parameters) - agent_attributes = {} - for attr in request_parameter_attributes: - if attr.destinations & DST_BROWSER_MONITORING: - agent_attributes[attr.name] = attr.value - - if agent_attributes: - attributes["a"] = agent_attributes - - # create the data structure that pull all our data in - - broswer_agent_configuration = self.browser_monitoring_intrinsics(obfuscation_key) - - if attributes: - attributes = obfuscate(json_encode(attributes), obfuscation_key) - broswer_agent_configuration["atts"] = attributes - - header = _js_agent_header_fragment % ( - _encode_nonce(nonce), - json_encode(broswer_agent_configuration), - self._settings.js_agent_loader, - ) + header = _js_agent_header_fragment % (_encode_nonce(nonce), self._settings.js_agent_loader) # To avoid any issues with browser encodings, we will make sure # that the javascript we inject for the browser agent is ASCII @@ -488,22 +464,25 @@ def browser_timing_header(self, nonce=None): try: if six.PY2: - header = header.encode("ascii") + header = header.encode('ascii') else: - header.encode("ascii") + header.encode('ascii') except UnicodeError: if not WebTransaction.unicode_error_reported: - _logger.error("ASCII encoding of js-agent-header failed.", header) + _logger.error('ASCII encoding of js-agent-header failed.', + header) WebTransaction.unicode_error_reported = True - header = "" + header = '' else: - header = "" + header = '' # We remember if we have returned a non empty string value and - # if called a second time we will not return it again. + # if called a second time we will not return it again. The flag + # will also be used to check whether the footer should be + # generated. if header: self.rum_header_generated = True @@ -511,12 +490,102 @@ def browser_timing_header(self, nonce=None): return header def browser_timing_footer(self, nonce=None): - """Deprecated API that has been replaced entirely by browser_timing_header().""" - warnings.warn( - "The browser_timing_footer function is deprecated. Please migrate to only using the browser_timing_header api instead.", - DeprecationWarning, - ) - return "" + """Returns the JavaScript footer to be included in any HTML + response to perform real user monitoring. This function returns + the footer as a native Python string. In Python 2 native strings + are stored as bytes. In Python 3 native strings are stored as + unicode. + + """ + + if not self.enabled: + return '' + + if self._state != self.STATE_RUNNING: + return '' + + if self.ignore_transaction: + return '' + + # Only generate a footer if the header had already been + # generated and we haven't already generated the footer. + + if not self.rum_header_generated: + return '' + + if self.rum_footer_generated: + return '' + + # Make sure we freeze the path. + + self._freeze_path() + + # When obfuscating values for the footer, we only use the + # first 13 characters of the account license key. + + obfuscation_key = self._settings.license_key[:13] + + attributes = {} + + user_attributes = {} + for attr in self.user_attributes: + if attr.destinations & DST_BROWSER_MONITORING: + user_attributes[attr.name] = attr.value + + if user_attributes: + attributes['u'] = user_attributes + + request_parameters = self.request_parameters + request_parameter_attributes = self.filter_request_parameters( + request_parameters) + agent_attributes = {} + for attr in request_parameter_attributes: + if attr.destinations & DST_BROWSER_MONITORING: + agent_attributes[attr.name] = attr.value + + if agent_attributes: + attributes['a'] = agent_attributes + + # create the data structure that pull all our data in + + footer_data = self.browser_monitoring_intrinsics(obfuscation_key) + + if attributes: + attributes = obfuscate(json_encode(attributes), obfuscation_key) + footer_data['atts'] = attributes + + footer = _js_agent_footer_fragment % (_encode_nonce(nonce), json_encode(footer_data)) + + # To avoid any issues with browser encodings, we will make sure that + # the javascript we inject for the browser agent is ASCII encodable. + # Since we obfuscate all agent and user attributes, and the transaction + # name with base 64 encoding, this will preserve those strings, if + # they have values outside of the ASCII character set. + # In the case of Python 2, we actually then use the encoded value + # as we need a native string, which for Python 2 is a byte string. + # If encoding as ASCII fails we will return an empty string. + + try: + if six.PY2: + footer = footer.encode('ascii') + else: + footer.encode('ascii') + + except UnicodeError: + if not WebTransaction.unicode_error_reported: + _logger.error('ASCII encoding of js-agent-footer failed.', + footer) + WebTransaction.unicode_error_reported = True + + footer = '' + + # We remember if we have returned a non empty string value and + # if called a second time we will not return it again. + + if footer: + self.rum_footer_generated = True + + return footer def browser_monitoring_intrinsics(self, obfuscation_key): txn_name = obfuscate(self.path, obfuscation_key) @@ -541,7 +610,7 @@ def browser_monitoring_intrinsics(self, obfuscation_key): if self._settings.browser_monitoring.ssl_for_http is not None: ssl_for_http = self._settings.browser_monitoring.ssl_for_http - intrinsics["sslForHttp"] = ssl_for_http + intrinsics['sslForHttp'] = ssl_for_http return intrinsics @@ -554,16 +623,16 @@ def __init__(self, environ): @staticmethod def _to_wsgi(key): key = key.upper() - if key == "CONTENT-LENGTH": - return "CONTENT_LENGTH" - elif key == "CONTENT-TYPE": - return "CONTENT_TYPE" - return "HTTP_" + key.replace("-", "_") + if key == 'CONTENT-LENGTH': + return 'CONTENT_LENGTH' + elif key == 'CONTENT-TYPE': + return 'CONTENT_TYPE' + return 'HTTP_' + key.replace('-', '_') @staticmethod def _from_wsgi(key): key = key.lower() - return key[5:].replace("_", "-") + return key[5:].replace('_', '-') def __getitem__(self, key): wsgi_key = self._to_wsgi(key) @@ -571,14 +640,14 @@ def __getitem__(self, key): def __iter__(self): for key in self.environ: - if key == "CONTENT_LENGTH": - yield "content-length", self.environ["CONTENT_LENGTH"] - elif key == "CONTENT_TYPE": - yield "content-type", self.environ["CONTENT_TYPE"] - elif key == "HTTP_CONTENT_LENGTH" or key == "HTTP_CONTENT_TYPE": + if key == 'CONTENT_LENGTH': + yield 'content-length', self.environ['CONTENT_LENGTH'] + elif key == 'CONTENT_TYPE': + yield 'content-type', self.environ['CONTENT_TYPE'] + elif key == 'HTTP_CONTENT_LENGTH' or key == 'HTTP_CONTENT_TYPE': # These keys are illegal and should be ignored continue - elif key.startswith("HTTP_"): + elif key.startswith('HTTP_'): yield self._from_wsgi(key), self.environ[key] def __len__(self): @@ -588,9 +657,11 @@ def __len__(self): class WSGIWebTransaction(WebTransaction): - MOD_WSGI_HEADERS = ("mod_wsgi.request_start", "mod_wsgi.queue_start") + + MOD_WSGI_HEADERS = ('mod_wsgi.request_start', 'mod_wsgi.queue_start') def __init__(self, application, environ, source=None): + # The web transaction can be enabled/disabled by # the value of the variable "newrelic.enabled" # in the WSGI environ dictionary. We need to check @@ -600,20 +671,17 @@ def __init__(self, application, environ, source=None): # base class making the decision based on whether # application or agent as a whole are enabled. - enabled = _lookup_environ_setting(environ, "newrelic.enabled", None) + enabled = _lookup_environ_setting(environ, + 'newrelic.enabled', None) # Initialise the common transaction base class. super(WSGIWebTransaction, self).__init__( - application, - name=None, - port=environ.get("SERVER_PORT"), - request_method=environ.get("REQUEST_METHOD"), - query_string=environ.get("QUERY_STRING"), + application, name=None, port=environ.get('SERVER_PORT'), + request_method=environ.get('REQUEST_METHOD'), + query_string=environ.get('QUERY_STRING'), headers=iter(WSGIHeaderProxy(environ)), - enabled=enabled, - source=source, - ) + enabled=enabled, source=source) # Disable transactions for websocket connections. # Also disable autorum if this is a websocket. This is a good idea for @@ -638,17 +706,21 @@ def __init__(self, application, environ, source=None): # Check for override settings from WSGI environ. - self.background_task = _lookup_environ_setting(environ, "newrelic.set_background_task", False) - - self.ignore_transaction = _lookup_environ_setting(environ, "newrelic.ignore_transaction", False) - self.suppress_apdex = _lookup_environ_setting(environ, "newrelic.suppress_apdex_metric", False) - self.suppress_transaction_trace = _lookup_environ_setting(environ, "newrelic.suppress_transaction_trace", False) - self.capture_params = _lookup_environ_setting( - environ, "newrelic.capture_request_params", settings.capture_params - ) - self.autorum_disabled = _lookup_environ_setting( - environ, "newrelic.disable_browser_autorum", not settings.browser_monitoring.auto_instrument - ) + self.background_task = _lookup_environ_setting(environ, + 'newrelic.set_background_task', False) + + self.ignore_transaction = _lookup_environ_setting(environ, + 'newrelic.ignore_transaction', False) + self.suppress_apdex = _lookup_environ_setting(environ, + 'newrelic.suppress_apdex_metric', False) + self.suppress_transaction_trace = _lookup_environ_setting(environ, + 'newrelic.suppress_transaction_trace', False) + self.capture_params = _lookup_environ_setting(environ, + 'newrelic.capture_request_params', + settings.capture_params) + self.autorum_disabled = _lookup_environ_setting(environ, + 'newrelic.disable_browser_autorum', + not settings.browser_monitoring.auto_instrument) # Make sure that if high security mode is enabled that # capture of request params is still being disabled. @@ -675,17 +747,17 @@ def __init__(self, application, environ, source=None): # due to use of REST style URL concepts or # otherwise. - request_uri = environ.get("REQUEST_URI", None) + request_uri = environ.get('REQUEST_URI', None) if request_uri is None: # The gunicorn WSGI server uses RAW_URI instead # of the more typical REQUEST_URI used by Apache # and other web servers. - request_uri = environ.get("RAW_URI", None) + request_uri = environ.get('RAW_URI', None) - script_name = environ.get("SCRIPT_NAME", None) - path_info = environ.get("PATH_INFO", None) + script_name = environ.get('SCRIPT_NAME', None) + path_info = environ.get('PATH_INFO', None) self._request_uri = request_uri @@ -706,13 +778,13 @@ def __init__(self, application, environ, source=None): else: path = script_name + path_info - self.set_transaction_name(path, "Uri", priority=1) + self.set_transaction_name(path, 'Uri', priority=1) if self._request_uri is None: self._request_uri = path else: if self._request_uri is not None: - self.set_transaction_name(self._request_uri, "Uri", priority=1) + self.set_transaction_name(self._request_uri, 'Uri', priority=1) # mod_wsgi sets its own distinct variables for queue time # automatically. Initially it set mod_wsgi.queue_start, @@ -736,7 +808,7 @@ def __init__(self, application, environ, source=None): continue try: - if value.startswith("t="): + if value.startswith('t='): try: self.queue_start = _parse_time_stamp(float(value[2:])) except Exception: @@ -751,40 +823,58 @@ def __init__(self, application, environ, source=None): pass def __exit__(self, exc, value, tb): - self.record_custom_metric("Python/WSGI/Input/Bytes", self._bytes_read) - self.record_custom_metric("Python/WSGI/Input/Time", self.read_duration) - self.record_custom_metric("Python/WSGI/Input/Calls/read", self._calls_read) - self.record_custom_metric("Python/WSGI/Input/Calls/readline", self._calls_readline) - self.record_custom_metric("Python/WSGI/Input/Calls/readlines", self._calls_readlines) - - self.record_custom_metric("Python/WSGI/Output/Bytes", self._bytes_sent) - self.record_custom_metric("Python/WSGI/Output/Time", self.sent_duration) - self.record_custom_metric("Python/WSGI/Output/Calls/yield", self._calls_yield) - self.record_custom_metric("Python/WSGI/Output/Calls/write", self._calls_write) + self.record_custom_metric('Python/WSGI/Input/Bytes', + self._bytes_read) + self.record_custom_metric('Python/WSGI/Input/Time', + self.read_duration) + self.record_custom_metric('Python/WSGI/Input/Calls/read', + self._calls_read) + self.record_custom_metric('Python/WSGI/Input/Calls/readline', + self._calls_readline) + self.record_custom_metric('Python/WSGI/Input/Calls/readlines', + self._calls_readlines) + + self.record_custom_metric('Python/WSGI/Output/Bytes', + self._bytes_sent) + self.record_custom_metric('Python/WSGI/Output/Time', + self.sent_duration) + self.record_custom_metric('Python/WSGI/Output/Calls/yield', + self._calls_yield) + self.record_custom_metric('Python/WSGI/Output/Calls/write', + self._calls_write) return super(WSGIWebTransaction, self).__exit__(exc, value, tb) def _update_agent_attributes(self): # Add WSGI agent attributes if self.read_duration != 0: - self._add_agent_attribute("wsgi.input.seconds", self.read_duration) + self._add_agent_attribute('wsgi.input.seconds', + self.read_duration) if self._bytes_read != 0: - self._add_agent_attribute("wsgi.input.bytes", self._bytes_read) + self._add_agent_attribute('wsgi.input.bytes', + self._bytes_read) if self._calls_read != 0: - self._add_agent_attribute("wsgi.input.calls.read", self._calls_read) + self._add_agent_attribute('wsgi.input.calls.read', + self._calls_read) if self._calls_readline != 0: - self._add_agent_attribute("wsgi.input.calls.readline", self._calls_readline) + self._add_agent_attribute('wsgi.input.calls.readline', + self._calls_readline) if self._calls_readlines != 0: - self._add_agent_attribute("wsgi.input.calls.readlines", self._calls_readlines) + self._add_agent_attribute('wsgi.input.calls.readlines', + self._calls_readlines) if self.sent_duration != 0: - self._add_agent_attribute("wsgi.output.seconds", self.sent_duration) + self._add_agent_attribute('wsgi.output.seconds', + self.sent_duration) if self._bytes_sent != 0: - self._add_agent_attribute("wsgi.output.bytes", self._bytes_sent) + self._add_agent_attribute('wsgi.output.bytes', + self._bytes_sent) if self._calls_write != 0: - self._add_agent_attribute("wsgi.output.calls.write", self._calls_write) + self._add_agent_attribute('wsgi.output.calls.write', + self._calls_write) if self._calls_yield != 0: - self._add_agent_attribute("wsgi.output.calls.yield", self._calls_yield) + self._add_agent_attribute('wsgi.output.calls.yield', + self._calls_yield) return super(WSGIWebTransaction, self)._update_agent_attributes() @@ -802,28 +892,20 @@ def process_response(self, status, response_headers, *args): # would raise as a 500 for WSGI applications). try: - status = status.split(" ", 1)[0] + status = status.split(' ', 1)[0] except Exception: status = None - return super(WSGIWebTransaction, self).process_response(status, response_headers) - - -def WebTransactionWrapper( - wrapped, - application=None, - name=None, - group=None, - scheme=None, - host=None, - port=None, - request_method=None, - request_path=None, - query_string=None, - headers=None, - source=None, -): + return super(WSGIWebTransaction, self).process_response( + status, response_headers) + + +def WebTransactionWrapper(wrapped, application=None, name=None, group=None, + scheme=None, host=None, port=None, request_method=None, + request_path=None, query_string=None, headers=None, source=None): + def wrapper(wrapped, instance, args, kwargs): + if type(application) != Application: _application = application_instance(application) else: @@ -903,6 +985,7 @@ def wrapper(wrapped, instance, args, kwargs): else: _headers = headers + proxy = async_proxy(wrapped) source_arg = source or wrapped @@ -910,37 +993,17 @@ def wrapper(wrapped, instance, args, kwargs): def create_transaction(transaction): if transaction: return None - return WebTransaction( - _application, - _name, - _group, - _scheme, - _host, - _port, - _request_method, - _request_path, - _query_string, - _headers, - source=source_arg, - ) + return WebTransaction( _application, _name, _group, + _scheme, _host, _port, _request_method, + _request_path, _query_string, _headers, source=source_arg) if proxy: context_manager = TransactionContext(create_transaction) return proxy(wrapped(*args, **kwargs), context_manager) transaction = WebTransaction( - _application, - _name, - _group, - _scheme, - _host, - _port, - _request_method, - _request_path, - _query_string, - _headers, - source=source_arg, - ) + _application, _name, _group, _scheme, _host, _port, + _request_method, _request_path, _query_string, _headers, source=source_arg) transaction = create_transaction(current_transaction(active_only=False)) @@ -953,50 +1016,22 @@ def create_transaction(transaction): return FunctionWrapper(wrapped, wrapper) -def web_transaction( - application=None, - name=None, - group=None, - scheme=None, - host=None, - port=None, - request_method=None, - request_path=None, - query_string=None, - headers=None, -): - return functools.partial( - WebTransactionWrapper, - application=application, - name=name, - group=group, - scheme=scheme, - host=host, - port=port, - request_method=request_method, - request_path=request_path, - query_string=query_string, - headers=headers, - ) - - -def wrap_web_transaction( - module, - object_path, - application=None, - name=None, - group=None, - scheme=None, - host=None, - port=None, - request_method=None, - request_path=None, - query_string=None, - headers=None, -): - return wrap_object( - module, - object_path, - WebTransactionWrapper, - (application, name, group, scheme, host, port, request_method, request_path, query_string, headers), - ) +def web_transaction(application=None, name=None, group=None, + scheme=None, host=None, port=None, request_method=None, + request_path=None, query_string=None, headers=None): + + return functools.partial(WebTransactionWrapper, + application=application, name=name, group=group, + scheme=scheme, host=host, port=port, request_method=request_method, + request_path=request_path, query_string=query_string, + headers=headers) + + +def wrap_web_transaction(module, object_path, + application=None, name=None, group=None, + scheme=None, host=None, port=None, request_method=None, + request_path=None, query_string=None, headers=None): + + return wrap_object(module, object_path, WebTransactionWrapper, + (application, name, group, scheme, host, port, request_method, + request_path, query_string, headers)) diff --git a/newrelic/api/wsgi_application.py b/newrelic/api/wsgi_application.py index 5d12e94f30..67338cbddd 100644 --- a/newrelic/api/wsgi_application.py +++ b/newrelic/api/wsgi_application.py @@ -78,6 +78,7 @@ def close(self): try: with FunctionTrace(name="Finalize", group="Python/WSGI"): + if isinstance(self.generator, _WSGIApplicationMiddleware): self.generator.close() @@ -152,6 +153,7 @@ def readlines(self, *args, **kwargs): class _WSGIApplicationMiddleware(object): + # This is a WSGI middleware for automatically inserting RUM into # HTML responses. It only works for where a WSGI application is # returning response content via a iterable/generator. It does not @@ -202,7 +204,16 @@ def process_data(self, data): # works then we are done, else we move to next phase of # buffering up content until we find the body element. - html_to_be_inserted = lambda: six.b(self.transaction.browser_timing_header()) + def html_to_be_inserted(): + header = self.transaction.browser_timing_header() + + if not header: + return b"" + + footer = self.transaction.browser_timing_footer() + + return six.b(header) + six.b(footer) + if not self.response_data: modified = insert_html_snippet(data, html_to_be_inserted) @@ -329,6 +340,7 @@ def start_response(self, status, response_headers, *args): # Also check whether RUM insertion has already occurred. if self.transaction.autorum_disabled or self.transaction.rum_header_generated: + self.flush_headers() self.pass_through = True @@ -348,7 +360,7 @@ def start_response(self, status, response_headers, *args): content_encoding = None content_disposition = None - for name, value in response_headers: + for (name, value) in response_headers: _name = name.lower() if _name == "content-length": @@ -496,6 +508,7 @@ def __iter__(self): def WSGIApplicationWrapper(wrapped, application=None, name=None, group=None, framework=None, dispatcher=None): + # Python 2 does not allow rebinding nonlocal variables, so to fix this # framework must be stored in list so it can be edited by closure. _framework = [framework] @@ -636,6 +649,7 @@ def _args(environ, start_response, *args, **kwargs): transaction.set_transaction_name(name, group, priority=1) def _start_response(status, response_headers, *args): + additional_headers = transaction.process_response(status, response_headers, *args) _write = start_response(status, response_headers + additional_headers, *args) diff --git a/newrelic/common/object_wrapper.py b/newrelic/common/object_wrapper.py index 09c737fd2b..c676966108 100644 --- a/newrelic/common/object_wrapper.py +++ b/newrelic/common/object_wrapper.py @@ -20,7 +20,6 @@ """ import inspect -import warnings from newrelic.packages.wrapt import BoundFunctionWrapper as _BoundFunctionWrapper from newrelic.packages.wrapt import CallableObjectProxy as _CallableObjectProxy @@ -32,6 +31,7 @@ wrap_object, wrap_object_attribute, ) +from newrelic.packages.wrapt.__wrapt__ import _FunctionWrapperBase # We previously had our own pure Python implementation of the generic # object wrapper but we now defer to using the wrapt module as its C @@ -122,13 +122,19 @@ class CallableObjectProxy(ObjectProxy, _CallableObjectProxy): # own code no longer uses it. It reaches down into what are wrapt internals # at present which shouldn't be doing. -class ObjectWrapper(FunctionWrapper): + +class ObjectWrapper(ObjectProxy, _FunctionWrapperBase): + __bound_function_wrapper__ = _NRBoundFunctionWrapper + def __init__(self, wrapped, instance, wrapper): - warnings.warn( - ("The ObjectWrapper API is deprecated. Please use one of ObjectProxy, FunctionWrapper, or CallableObjectProxy instead."), - DeprecationWarning, - ) - super(ObjectWrapper, self).__init__(wrapped, wrapper) + if isinstance(wrapped, classmethod): + binding = "classmethod" + elif isinstance(wrapped, staticmethod): + binding = "staticmethod" + else: + binding = "function" + + super(ObjectWrapper, self).__init__(wrapped, instance, wrapper, binding=binding) # Function for creating a decorator for applying to functions, as well as diff --git a/newrelic/config.py b/newrelic/config.py index 2528d84d35..6782a0396b 100644 --- a/newrelic/config.py +++ b/newrelic/config.py @@ -34,7 +34,7 @@ import newrelic.api.generator_trace import newrelic.api.import_hook import newrelic.api.memcache_trace -from newrelic.common.object_names import callable_name +import newrelic.api.object_wrapper import newrelic.api.profile_trace import newrelic.api.settings import newrelic.api.transaction_name @@ -1348,7 +1348,7 @@ def _process_background_task_configuration(): group = _config_object.get(section, "group") if name and name.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} name = eval(name, callable_vars) # nosec, pylint: disable=W0123 _logger.debug("register background-task %s", ((module, object_path, application, name, group),)) @@ -1398,7 +1398,7 @@ def _process_database_trace_configuration(): sql = _config_object.get(section, "sql") if sql.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} sql = eval(sql, callable_vars) # nosec, pylint: disable=W0123 _logger.debug("register database-trace %s", ((module, object_path, sql),)) @@ -1453,11 +1453,11 @@ def _process_external_trace_configuration(): method = _config_object.get(section, "method") if url.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} url = eval(url, callable_vars) # nosec, pylint: disable=W0123 if method and method.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} method = eval(method, callable_vars) # nosec, pylint: disable=W0123 _logger.debug("register external-trace %s", ((module, object_path, library, url, method),)) @@ -1525,7 +1525,7 @@ def _process_function_trace_configuration(): rollup = _config_object.get(section, "rollup") if name and name.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} name = eval(name, callable_vars) # nosec, pylint: disable=W0123 _logger.debug( @@ -1583,7 +1583,7 @@ def _process_generator_trace_configuration(): group = _config_object.get(section, "group") if name and name.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} name = eval(name, callable_vars) # nosec, pylint: disable=W0123 _logger.debug("register generator-trace %s", ((module, object_path, name, group),)) @@ -1642,7 +1642,7 @@ def _process_profile_trace_configuration(): depth = _config_object.get(section, "depth") if name and name.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} name = eval(name, callable_vars) # nosec, pylint: disable=W0123 _logger.debug("register profile-trace %s", ((module, object_path, name, group, depth),)) @@ -1692,7 +1692,7 @@ def _process_memcache_trace_configuration(): command = _config_object.get(section, "command") if command.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} command = eval(command, callable_vars) # nosec, pylint: disable=W0123 _logger.debug("register memcache-trace %s", (module, object_path, command)) @@ -1752,7 +1752,7 @@ def _process_transaction_name_configuration(): priority = _config_object.getint(section, "priority") if name and name.startswith("lambda "): - callable_vars = {"callable_name": callable_name} + callable_vars = {"callable_name": newrelic.api.object_wrapper.callable_name} name = eval(name, callable_vars) # nosec, pylint: disable=W0123 _logger.debug("register transaction-name %s", ((module, object_path, name, group, priority),)) diff --git a/newrelic/console.py b/newrelic/console.py index 31b664b55a..48cda6e7cc 100644 --- a/newrelic/console.py +++ b/newrelic/console.py @@ -72,7 +72,8 @@ def doc_signature(func): return formatargspec(args[1:], varargs, keywords, defaults) -from newrelic.common.object_wrapper import ObjectProxy +from newrelic.api.object_wrapper import ObjectWrapper +from newrelic.api.transaction import Transaction from newrelic.core.agent import agent_instance from newrelic.core.config import flatten_settings, global_settings from newrelic.core.trace_cache import trace_cache @@ -160,7 +161,7 @@ def __call__(self, code=None): __builtin__.exit = Quitter("exit") -class OutputWrapper(ObjectProxy): +class OutputWrapper(ObjectWrapper): def flush(self): try: shell = _consoles.active @@ -186,8 +187,8 @@ def writelines(self, data): def intercept_console(): setquit() - sys.stdout = OutputWrapper(sys.stdout) - sys.stderr = OutputWrapper(sys.stderr) + sys.stdout = OutputWrapper(sys.stdout, None, None) + sys.stderr = OutputWrapper(sys.stderr, None, None) class EmbeddedConsole(code.InteractiveConsole): diff --git a/newrelic/core/internal_metrics.py b/newrelic/core/internal_metrics.py index 090a658c73..87452fce4a 100644 --- a/newrelic/core/internal_metrics.py +++ b/newrelic/core/internal_metrics.py @@ -17,7 +17,6 @@ import types import time import threading -import newrelic.common.object_wrapper _context = threading.local() @@ -89,7 +88,7 @@ def decorator(wrapped): return decorator def wrap_internal_trace(module, object_path, name=None): - newrelic.common.object_wrapper.wrap_object(module, object_path, + newrelic.api.object_wrapper.wrap_object(module, object_path, InternalTraceWrapper, (name,)) def internal_metric(name, value): diff --git a/newrelic/hooks/application_celery.py b/newrelic/hooks/application_celery.py index ab7ca9e95c..12f41d8d0d 100644 --- a/newrelic/hooks/application_celery.py +++ b/newrelic/hooks/application_celery.py @@ -26,8 +26,7 @@ from newrelic.api.background_task import BackgroundTask from newrelic.api.function_trace import FunctionTrace from newrelic.api.pre_function import wrap_pre_function -from newrelic.common.object_names import callable_name -from newrelic.common.object_wrapper import FunctionWrapper +from newrelic.api.object_wrapper import callable_name, ObjectWrapper from newrelic.api.transaction import current_transaction from newrelic.core.agent import shutdown_agent @@ -99,6 +98,10 @@ def _application(): with BackgroundTask(_application(), _name, 'Celery', source=instance): return wrapped(*args, **kwargs) + # Start Hotfix v2.2.1. + # obj = ObjectWrapper(wrapped, None, wrapper) + # End Hotfix v2.2.1. + # Celery tasks that inherit from celery.app.task must implement a run() # method. # ref: (http://docs.celeryproject.org/en/2.5/reference/ @@ -107,11 +110,11 @@ def _application(): # task. But celery does a micro-optimization where if the __call__ method # was not overridden by an inherited task, then it will directly execute # the run() method without going through the __call__ method. Our - # instrumentation via FunctionWrapper() relies on __call__ being called which + # instrumentation via ObjectWrapper() relies on __call__ being called which # in turn executes the wrapper() function defined above. Since the micro # optimization bypasses __call__ method it breaks our instrumentation of # celery. To circumvent this problem, we added a run() attribute to our - # FunctionWrapper which points to our __call__ method. This causes Celery + # ObjectWrapper which points to our __call__ method. This causes Celery # to execute our __call__ method which in turn applies the wrapper # correctly before executing the task. # @@ -119,11 +122,17 @@ def _application(): # versions included a monkey-patching provision which did not perform this # optimization on functions that were monkey-patched. - class TaskWrapper(FunctionWrapper): + # Start Hotfix v2.2.1. + # obj.__dict__['run'] = obj.__call__ + + class _ObjectWrapper(ObjectWrapper): def run(self, *args, **kwargs): return self.__call__(*args, **kwargs) - return TaskWrapper(wrapped, wrapper) + obj = _ObjectWrapper(wrapped, None, wrapper) + # End Hotfix v2.2.1. + + return obj def instrument_celery_app_task(module): diff --git a/newrelic/hooks/component_piston.py b/newrelic/hooks/component_piston.py index 96204f404c..78b975ed53 100644 --- a/newrelic/hooks/component_piston.py +++ b/newrelic/hooks/component_piston.py @@ -16,15 +16,14 @@ import newrelic.api.transaction import newrelic.api.function_trace -import newrelic.common.object_wrapper -from newrelic.common.object_names import callable_name +import newrelic.api.object_wrapper import newrelic.api.in_function class MethodWrapper(object): def __init__(self, wrapped, priority=None): - self._nr_name = callable_name(wrapped) + self._nr_name = newrelic.api.object_wrapper.callable_name(wrapped) self._nr_wrapped = wrapped self._nr_priority = priority @@ -77,7 +76,7 @@ def __call__(self, *args, **kwargs): def instrument_piston_resource(module): - newrelic.common.object_wrapper.wrap_object(module, + newrelic.api.object_wrapper.wrap_object(module, 'Resource.__init__', ResourceInitWrapper) diff --git a/newrelic/hooks/component_tastypie.py b/newrelic/hooks/component_tastypie.py index da93efbfb3..8cc251916c 100644 --- a/newrelic/hooks/component_tastypie.py +++ b/newrelic/hooks/component_tastypie.py @@ -12,11 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys + from newrelic.api.function_trace import FunctionTraceWrapper -from newrelic.common.object_names import callable_name -from newrelic.common.object_wrapper import wrap_function_wrapper, function_wrapper +from newrelic.api.object_wrapper import ObjectWrapper, callable_name from newrelic.api.transaction import current_transaction from newrelic.api.time_trace import notice_error +from newrelic.common.object_wrapper import wrap_function_wrapper def _nr_wrap_handle_exception(wrapped, instance, args, kwargs): @@ -54,7 +56,6 @@ def outer_fn_wrapper(outer_fn, instance, args, kwargs): name = callable_name(callback) group = None - @function_wrapper def inner_fn_wrapper(inner_fn, instance, args, kwargs): transaction = current_transaction() @@ -68,14 +69,18 @@ def inner_fn_wrapper(inner_fn, instance, args, kwargs): result = outer_fn(*args, **kwargs) - return inner_fn_wrapper(result) + return ObjectWrapper(result, None, inner_fn_wrapper) def instrument_tastypie_resources(module): - wrap_function_wrapper(module, "Resource.wrap_view", outer_fn_wrapper) + _wrap_view = module.Resource.wrap_view + module.Resource.wrap_view = ObjectWrapper( + _wrap_view, None, outer_fn_wrapper) - wrap_function_wrapper(module, 'Resource._handle_500', _nr_wrap_handle_exception) + wrap_function_wrapper(module, 'Resource._handle_500', + _nr_wrap_handle_exception) def instrument_tastypie_api(module): - wrap_function_wrapper(module, "Api.wrap_view", outer_fn_wrapper) + _wrap_view = module.Api.wrap_view + module.Api.wrap_view = ObjectWrapper(_wrap_view, None, outer_fn_wrapper) diff --git a/newrelic/hooks/external_feedparser.py b/newrelic/hooks/external_feedparser.py index 1d2003eb21..13f9ebd63e 100644 --- a/newrelic/hooks/external_feedparser.py +++ b/newrelic/hooks/external_feedparser.py @@ -19,7 +19,6 @@ import newrelic.api.transaction import newrelic.api.object_wrapper -import newrelic.common.object_wrapper import newrelic.api.external_trace class capture_external_trace(object): @@ -71,5 +70,5 @@ def __getattr__(self, name): return getattr(self._nr_next_object, name) def instrument(module): - newrelic.common.object_wrapper.wrap_object( + newrelic.api.object_wrapper.wrap_object( module, 'parse', capture_external_trace) diff --git a/newrelic/hooks/external_httplib.py b/newrelic/hooks/external_httplib.py index ca8decb40c..7d322f7194 100644 --- a/newrelic/hooks/external_httplib.py +++ b/newrelic/hooks/external_httplib.py @@ -18,7 +18,7 @@ from newrelic.api.external_trace import ExternalTrace from newrelic.api.transaction import current_transaction -from newrelic.common.object_wrapper import wrap_function_wrapper +from newrelic.common.object_wrapper import ObjectWrapper def httplib_endheaders_wrapper(wrapped, instance, args, kwargs, @@ -125,7 +125,24 @@ def instrument(module): else: library = 'http' - wrap_function_wrapper(module, "HTTPConnection.endheaders", functools.partial(httplib_endheaders_wrapper, scheme='http', library=library)) - wrap_function_wrapper(module, "HTTPSConnection.endheaders", functools.partial(httplib_endheaders_wrapper, scheme='https', library=library)) - wrap_function_wrapper(module, "HTTPConnection.getresponse", httplib_getresponse_wrapper) - wrap_function_wrapper(module, "HTTPConnection.putheader", httplib_putheader_wrapper) + module.HTTPConnection.endheaders = ObjectWrapper( + module.HTTPConnection.endheaders, + None, + functools.partial(httplib_endheaders_wrapper, scheme='http', + library=library)) + + module.HTTPSConnection.endheaders = ObjectWrapper( + module.HTTPConnection.endheaders, + None, + functools.partial(httplib_endheaders_wrapper, scheme='https', + library=library)) + + module.HTTPConnection.getresponse = ObjectWrapper( + module.HTTPConnection.getresponse, + None, + httplib_getresponse_wrapper) + + module.HTTPConnection.putheader = ObjectWrapper( + module.HTTPConnection.putheader, + None, + httplib_putheader_wrapper) diff --git a/newrelic/hooks/framework_django.py b/newrelic/hooks/framework_django.py index 91d6fec200..3d9f448cc2 100644 --- a/newrelic/hooks/framework_django.py +++ b/newrelic/hooks/framework_django.py @@ -16,7 +16,6 @@ import logging import sys import threading -import warnings from newrelic.api.application import register_application from newrelic.api.background_task import BackgroundTaskWrapper @@ -92,6 +91,7 @@ def _setting_set(value): def should_add_browser_timing(response, transaction): + # Don't do anything if receive a streaming response which # was introduced in Django 1.5. Need to avoid this as there # will be no 'content' attribute. Alternatively there may be @@ -111,7 +111,7 @@ def should_add_browser_timing(response, transaction): if not transaction or not transaction.enabled: return False - # Only insert RUM JavaScript headers if enabled + # Only insert RUM JavaScript headers and footers if enabled # in configuration and not already likely inserted. if not transaction.settings.browser_monitoring.enabled: @@ -152,21 +152,38 @@ def should_add_browser_timing(response, transaction): return True -# Response middleware for automatically inserting RUM header into HTML response returned by application +# Response middleware for automatically inserting RUM header and +# footer into HTML response returned by application def browser_timing_insertion(response, transaction): - # No point continuing if header is empty. This can occur if RUM is not enabled within the UI. We don't want to - # generate the header just yet as we want to do that as late as possible so that application server time in header - # is as accurate as possible. In particular, if the response content is generated on demand then the flattening - # of the response could take some time and we want to track that. We thus generate header below at - # the point of insertion. - - # Make sure we flatten any content first as it could be stored as a list of strings in the response object. We - # assign it back to the response object to avoid having multiple copies of the string in memory at the same time + + # No point continuing if header is empty. This can occur if + # RUM is not enabled within the UI. It is assumed at this + # point that if header is not empty, then footer will not be + # empty. We don't want to generate the footer just yet as + # want to do that as late as possible so that application + # server time in footer is as accurate as possible. In + # particular, if the response content is generated on demand + # then the flattening of the response could take some time + # and we want to track that. We thus generate footer below + # at point of insertion. + + header = transaction.browser_timing_header() + + if not header: + return response + + def html_to_be_inserted(): + return six.b(header) + six.b(transaction.browser_timing_footer()) + + # Make sure we flatten any content first as it could be + # stored as a list of strings in the response object. We + # assign it back to the response object to avoid having + # multiple copies of the string in memory at the same time # as we progress through steps below. - result = insert_html_snippet(response.content, lambda: six.b(transaction.browser_timing_header())) + result = insert_html_snippet(response.content, html_to_be_inserted) if result is not None: if transaction.settings.debug.log_autorum_middleware: @@ -183,8 +200,10 @@ def browser_timing_insertion(response, transaction): return response -# Template tag functions for manually inserting RUM header into HTML response. A template tag library for 'newrelic' -# will be automatically inserted into set of tag libraries when performing step to instrument the middleware. +# Template tag functions for manually inserting RUM header and +# footer into HTML response. A template tag library for +# 'newrelic' will be automatically inserted into set of tag +# libraries when performing step to instrument the middleware. def newrelic_browser_timing_header(): @@ -195,11 +214,10 @@ def newrelic_browser_timing_header(): def newrelic_browser_timing_footer(): - warnings.warn( - "The newrelic_browser_timing_footer function is deprecated. Please migrate to only using the newrelic_browser_timing_header API instead.", - DeprecationWarning, - ) - return "" # nosec + from django.utils.safestring import mark_safe + + transaction = current_transaction() + return transaction and mark_safe(transaction.browser_timing_footer()) or "" # nosec # Addition of instrumentation for middleware. Can only do this @@ -210,6 +228,7 @@ def newrelic_browser_timing_footer(): def wrap_leading_middleware(middleware): + # Wrapper to be applied to middleware executed prior to the # view handler being executed. Records the time spent in the # middleware as separate function node and also attempts to @@ -257,6 +276,7 @@ def wrapper(wrapped, instance, args, kwargs): # functionality, so instead of removing this instrumentation, this # will be excluded from the coverage analysis. def wrap_view_middleware(middleware): # pragma: no cover + # This is no longer being used. The changes to strip the # wrapper from the view handler when passed into the function # urlresolvers.reverse() solves most of the problems. To back @@ -322,6 +342,7 @@ def _wrapped(request, view_func, view_args, view_kwargs): def wrap_trailing_middleware(middleware): + # Wrapper to be applied to trailing middleware executed # after the view handler. Records the time spent in the # middleware as separate function node. Transaction is never @@ -337,6 +358,7 @@ def wrap_trailing_middleware(middleware): def insert_and_wrap_middleware(handler, *args, **kwargs): + # Use lock to control access by single thread but also as # flag to indicate if done the initialisation. Lock will be # None if have already done this. @@ -361,6 +383,7 @@ def insert_and_wrap_middleware(handler, *args, **kwargs): middleware_instrumentation_lock = None try: + # Wrap the middleware to undertake timing and name # the web transaction. The naming is done as lower # priority than that for view handler so view handler @@ -388,6 +411,7 @@ def insert_and_wrap_middleware(handler, *args, **kwargs): def _nr_wrapper_GZipMiddleware_process_response_(wrapped, instance, args, kwargs): + transaction = current_transaction() if transaction is None: @@ -430,6 +454,7 @@ def _nr_wrapper_BaseHandler_get_response_(wrapped, instance, args, kwargs): def instrument_django_core_handlers_base(module): + # Attach a post function to load_middleware() method of # BaseHandler to trigger insertion of browser timing # middleware and wrapping of middleware for timing etc. @@ -443,10 +468,12 @@ def instrument_django_core_handlers_base(module): def instrument_django_gzip_middleware(module): + wrap_function_wrapper(module, "GZipMiddleware.process_response", _nr_wrapper_GZipMiddleware_process_response_) def wrap_handle_uncaught_exception(middleware): + # Wrapper to be applied to handler called when exceptions # propagate up to top level from middleware. Records the # time spent in the handler as separate function node. Names @@ -479,6 +506,7 @@ def _wrapped(request, resolver, exc_info): def instrument_django_core_handlers_wsgi(module): + # Wrap the WSGI application entry point. If this is also # wrapped from the WSGI script file or by the WSGI hosting # mechanism then those will take precedence. @@ -504,6 +532,7 @@ def instrument_django_core_handlers_wsgi(module): def wrap_view_handler(wrapped, priority=3): + # Ensure we don't wrap the view handler more than once. This # looks like it may occur in cases where the resolver is # called recursively. We flag that view handler was wrapped @@ -545,6 +574,7 @@ def wrapper(wrapped, instance, args, kwargs): def wrap_url_resolver(wrapped): + # Wrap URL resolver. If resolver returns valid result then # wrap the view handler returned. The type of the result # changes across Django versions so need to check and adapt @@ -594,6 +624,7 @@ def _wrapped(path): def wrap_url_resolver_nnn(wrapped, priority=1): + # Wrapper to be applied to the URL resolver for errors. name = callable_name(wrapped) @@ -616,6 +647,7 @@ def wrapper(wrapped, instance, args, kwargs): def wrap_url_reverse(wrapped): + # Wrap the URL resolver reverse lookup. Where the view # handler is passed in we need to strip any instrumentation # wrapper to ensure that it doesn't interfere with the @@ -635,6 +667,7 @@ def execute(viewname, *args, **kwargs): def instrument_django_core_urlresolvers(module): + # Wrap method which maps a string version of a function # name as used in urls.py pattern so can capture any # exception which is raised during that process. @@ -686,6 +719,7 @@ def instrument_django_core_urlresolvers(module): def instrument_django_urls_base(module): + # Wrap function for performing reverse URL lookup to strip any # instrumentation wrapper when view handler is passed in. @@ -694,6 +728,7 @@ def instrument_django_urls_base(module): def instrument_django_template(module): + # Wrap methods for rendering of Django templates. The name # of the method changed in between Django versions so need # to check for which one we have. The name of the function @@ -718,7 +753,8 @@ def template_name(template, *args): if not hasattr(module, "libraries"): return - # Register template tags used for manual insertion of RUM header. + # Register template tags used for manual insertion of RUM + # header and footer. # # TODO This can now be installed as a separate tag library # so should possibly look at deprecating this automatic @@ -739,6 +775,7 @@ def wrapper(wrapped, instance, args, kwargs): def instrument_django_template_loader_tags(module): + # Wrap template block node for timing, naming the node after # the block name as defined in the template rather than # function name. @@ -747,6 +784,7 @@ def instrument_django_template_loader_tags(module): def instrument_django_core_servers_basehttp(module): + # Allow 'runserver' to be used with Django <= 1.3. To do # this we wrap the WSGI application argument on the way in # so that the run() method gets the wrapped instance. @@ -781,6 +819,7 @@ def wrap_wsgi_application_entry_point(server, application, **kwargs): ) if not hasattr(module, "simple_server") and hasattr(module.ServerHandler, "run"): + # Patch the server to make it work properly. def run(self, application): @@ -830,6 +869,7 @@ def instrument_django_contrib_staticfiles_handlers(module): def instrument_django_views_debug(module): + # Wrap methods for handling errors when Django debug # enabled. For 404 we give this higher naming priority over # any prior middleware or view handler to give them @@ -856,6 +896,7 @@ def resolve_view_handler(view, request): def wrap_view_dispatch(wrapped): + # Wrapper to be applied to dispatcher for class based views. def wrapper(wrapped, instance, args, kwargs): @@ -955,6 +996,7 @@ def instrument_django_core_management_base(module): @function_wrapper def _nr_wrapper_django_inclusion_tag_wrapper_(wrapped, instance, args, kwargs): + name = hasattr(wrapped, "__name__") and wrapped.__name__ if name is None: @@ -983,11 +1025,13 @@ def _bind_params(func, *args, **kwargs): def _nr_wrapper_django_template_base_Library_inclusion_tag_(wrapped, instance, args, kwargs): + return _nr_wrapper_django_inclusion_tag_decorator_(wrapped(*args, **kwargs)) @function_wrapper def _nr_wrapper_django_template_base_InclusionNode_render_(wrapped, instance, args, kwargs): + if wrapped.__self__ is None: return wrapped(*args, **kwargs) @@ -1002,6 +1046,7 @@ def _nr_wrapper_django_template_base_InclusionNode_render_(wrapped, instance, ar def _nr_wrapper_django_template_base_generic_tag_compiler_(wrapped, instance, args, kwargs): + if wrapped.__code__.co_argcount > 6: # Django > 1.3. @@ -1038,6 +1083,7 @@ def _bind_params(name=None, compile_function=None, *args, **kwargs): return wrapped(*args, **kwargs) def _get_node_class(compile_function): + node_class = None # Django >= 1.4 uses functools.partial @@ -1053,6 +1099,7 @@ def _get_node_class(compile_function): and hasattr(compile_function, "__name__") and compile_function.__name__ == "_curried" ): + # compile_function here is generic_tag_compiler(), which has been # curried. To get node_class, we first get the function obj, args, # and kwargs of the curried function from the cells in @@ -1107,6 +1154,7 @@ def instrument_django_template_base(module): settings = global_settings() if "django.instrumentation.inclusion-tags.r1" in settings.feature_flag: + if hasattr(module, "generic_tag_compiler"): wrap_function_wrapper( module, "generic_tag_compiler", _nr_wrapper_django_template_base_generic_tag_compiler_ @@ -1149,6 +1197,7 @@ def _bind_params(original_middleware, *args, **kwargs): def instrument_django_core_handlers_exception(module): + if hasattr(module, "convert_exception_to_response"): wrap_function_wrapper(module, "convert_exception_to_response", _nr_wrapper_convert_exception_to_response_) diff --git a/newrelic/hooks/framework_pylons.py b/newrelic/hooks/framework_pylons.py index 2832261668..9c5c457cd7 100644 --- a/newrelic/hooks/framework_pylons.py +++ b/newrelic/hooks/framework_pylons.py @@ -16,15 +16,14 @@ import newrelic.api.transaction_name import newrelic.api.function_trace import newrelic.api.error_trace -import newrelic.common.object_wrapper -from newrelic.common.object_names import callable_name +import newrelic.api.object_wrapper import newrelic.api.import_hook from newrelic.api.time_trace import notice_error def name_controller(self, environ, start_response): action = environ['pylons.routes_dict']['action'] - return "%s.%s" % (callable_name(self), action) + return "%s.%s" % (newrelic.api.object_wrapper.callable_name(self), action) class capture_error(object): def __init__(self, wrapped): @@ -70,12 +69,12 @@ def instrument(module): module, 'WSGIController.__call__') def name_WSGIController_perform_call(self, func, args): - return callable_name(func) + return newrelic.api.object_wrapper.callable_name(func) newrelic.api.function_trace.wrap_function_trace( module, 'WSGIController._perform_call', name_WSGIController_perform_call) - newrelic.common.object_wrapper.wrap_object( + newrelic.api.object_wrapper.wrap_object( module, 'WSGIController._perform_call', capture_error) elif module.__name__ == 'pylons.templating': diff --git a/newrelic/hooks/framework_web2py.py b/newrelic/hooks/framework_web2py.py index aeb22bd84a..e9785e02f5 100644 --- a/newrelic/hooks/framework_web2py.py +++ b/newrelic/hooks/framework_web2py.py @@ -22,7 +22,6 @@ import newrelic.api.function_trace import newrelic.api.transaction_name import newrelic.api.object_wrapper -import newrelic.common.object_wrapper import newrelic.api.pre_function from newrelic.api.time_trace import notice_error @@ -133,7 +132,7 @@ def __call__(self, request, response, session): def __getattr__(self, name): return getattr(self._nr_next_object, name) - newrelic.common.object_wrapper.wrap_object( + newrelic.api.object_wrapper.wrap_object( module, 'serve_controller', error_serve_controller) def instrument_gluon_template(module): diff --git a/newrelic/hooks/framework_webpy.py b/newrelic/hooks/framework_webpy.py index 3c15bd1a7d..c1785a89f3 100644 --- a/newrelic/hooks/framework_webpy.py +++ b/newrelic/hooks/framework_webpy.py @@ -21,7 +21,7 @@ import newrelic.api.in_function import newrelic.api.out_function import newrelic.api.pre_function -from newrelic.common.object_names import callable_name +from newrelic.api.object_wrapper import callable_name from newrelic.api.wsgi_application import WSGIApplicationWrapper from newrelic.api.time_trace import notice_error diff --git a/newrelic/hooks/middleware_flask_compress.py b/newrelic/hooks/middleware_flask_compress.py index 078cc3d989..09e35b3cd2 100644 --- a/newrelic/hooks/middleware_flask_compress.py +++ b/newrelic/hooks/middleware_flask_compress.py @@ -18,41 +18,35 @@ from newrelic.api.transaction import current_transaction from newrelic.common.object_wrapper import wrap_function_wrapper from newrelic.config import extra_settings + from newrelic.packages import six _logger = logging.getLogger(__name__) _boolean_states = { - "1": True, - "yes": True, - "true": True, - "on": True, - "0": False, - "no": False, - "false": False, - "off": False, + '1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False } def _setting_boolean(value): if value.lower() not in _boolean_states: - raise ValueError("Not a boolean: %s" % value) + raise ValueError('Not a boolean: %s' % value) return _boolean_states[value.lower()] _settings_types = { - "browser_monitoring.auto_instrument": _setting_boolean, - "browser_monitoring.auto_instrument_passthrough": _setting_boolean, + 'browser_monitoring.auto_instrument': _setting_boolean, + 'browser_monitoring.auto_instrument_passthrough': _setting_boolean, } _settings_defaults = { - "browser_monitoring.auto_instrument": True, - "browser_monitoring.auto_instrument_passthrough": True, + 'browser_monitoring.auto_instrument': True, + 'browser_monitoring.auto_instrument_passthrough': True, } -flask_compress_settings = extra_settings( - "import-hook:flask_compress", types=_settings_types, defaults=_settings_defaults -) +flask_compress_settings = extra_settings('import-hook:flask_compress', + types=_settings_types, defaults=_settings_defaults) def _nr_wrapper_Compress_after_request(wrapped, instance, args, kwargs): @@ -68,7 +62,7 @@ def _params(response, *args, **kwargs): if not transaction: return wrapped(*args, **kwargs) - # Only insert RUM JavaScript headers if enabled + # Only insert RUM JavaScript headers and footers if enabled # in configuration and not already likely inserted. if not transaction.settings.browser_monitoring.enabled: @@ -89,34 +83,45 @@ def _params(response, *args, **kwargs): # a user may want to also perform insertion for # 'application/xhtml+xml'. - ctype = (response.mimetype or "").lower() + ctype = (response.mimetype or '').lower() if ctype not in transaction.settings.browser_monitoring.content_type: return wrapped(*args, **kwargs) # Don't risk it if content encoding already set. - if "Content-Encoding" in response.headers: + if 'Content-Encoding' in response.headers: return wrapped(*args, **kwargs) # Don't risk it if content is actually within an attachment. - cdisposition = response.headers.get("Content-Disposition", "").lower() + cdisposition = response.headers.get('Content-Disposition', '').lower() - if cdisposition.split(";")[0].strip() == "attachment": + if cdisposition.split(';')[0].strip() == 'attachment': return wrapped(*args, **kwargs) - # No point continuing if header is empty. This can occur if RUM is not enabled within the UI. We don't want to - # generate the header just yet as we want to do that as late as possible so that application server time in header - # is as accurate as possible. In particular, if the response content is generated on demand then the flattening - # of the response could take some time and we want to track that. We thus generate header below at - # the point of insertion. + # No point continuing if header is empty. This can occur if + # RUM is not enabled within the UI. It is assumed at this + # point that if header is not empty, then footer will not be + # empty. We don't want to generate the footer just yet as + # want to do that as late as possible so that application + # server time in footer is as accurate as possible. In + # particular, if the response content is generated on demand + # then the flattening of the response could take some time + # and we want to track that. We thus generate footer below + # at point of insertion. + + header = transaction.browser_timing_header() + + if not header: + return wrapped(*args, **kwargs) # If the response has direct_passthrough flagged, then is # likely to be streaming a file or other large response. - direct_passthrough = getattr(response, "direct_passthrough", None) + direct_passthrough = getattr(response, 'direct_passthrough', None) if direct_passthrough: - if not (flask_compress_settings.browser_monitoring.auto_instrument_passthrough): + if not (flask_compress_settings. + browser_monitoring.auto_instrument_passthrough): return wrapped(*args, **kwargs) # In those cases, if the mimetype is still a supported browser @@ -126,31 +131,34 @@ def _params(response, *args, **kwargs): # # In order to do that, we have to disable direct_passthrough on the # response since we have to immediately read the contents of the file. - elif ctype == "text/html": + elif ctype == 'text/html': response.direct_passthrough = False else: return wrapped(*args, **kwargs) + def html_to_be_inserted(): + return six.b(header) + six.b(transaction.browser_timing_footer()) + # Make sure we flatten any content first as it could be # stored as a list of strings in the response object. We # assign it back to the response object to avoid having # multiple copies of the string in memory at the same time # as we progress through steps below. - result = insert_html_snippet(response.get_data(), lambda: six.b(transaction.browser_timing_header())) + result = insert_html_snippet(response.get_data(), html_to_be_inserted) if result is not None: if transaction.settings.debug.log_autorum_middleware: - _logger.debug( - "RUM insertion from flask_compress " "triggered. Bytes added was %r.", - len(result) - len(response.get_data()), - ) + _logger.debug('RUM insertion from flask_compress ' + 'triggered. Bytes added was %r.', + len(result) - len(response.get_data())) response.set_data(result) - response.headers["Content-Length"] = str(len(response.get_data())) + response.headers['Content-Length'] = str(len(response.get_data())) return wrapped(*args, **kwargs) def instrument_flask_compress(module): - wrap_function_wrapper(module, "Compress.after_request", _nr_wrapper_Compress_after_request) + wrap_function_wrapper(module, 'Compress.after_request', + _nr_wrapper_Compress_after_request) diff --git a/newrelic/hooks/template_genshi.py b/newrelic/hooks/template_genshi.py index db58237fdb..abea1e485a 100644 --- a/newrelic/hooks/template_genshi.py +++ b/newrelic/hooks/template_genshi.py @@ -15,7 +15,7 @@ import types import newrelic.api.transaction -import newrelic.common.object_wrapper +import newrelic.api.object_wrapper import newrelic.api.function_trace class stream_wrapper(object): @@ -69,5 +69,5 @@ def instrument(module): if module.__name__ == 'genshi.template.base': - newrelic.common.object_wrapper.wrap_object( + newrelic.api.object_wrapper.wrap_object( module, 'Template.generate', wrap_template) diff --git a/newrelic/hooks/template_mako.py b/newrelic/hooks/template_mako.py index 1cd5bab16f..2e20da7306 100644 --- a/newrelic/hooks/template_mako.py +++ b/newrelic/hooks/template_mako.py @@ -13,7 +13,7 @@ # limitations under the License. import newrelic.api.function_trace -import newrelic.common.object_wrapper +import newrelic.api.object_wrapper class TemplateRenderWrapper(object): @@ -42,7 +42,7 @@ def __call__(self, template, *args, **kwargs): def instrument_mako_runtime(module): - newrelic.common.object_wrapper.wrap_object(module, + newrelic.api.object_wrapper.wrap_object(module, '_render', TemplateRenderWrapper) def instrument_mako_template(module): diff --git a/tests/agent_features/test_asgi_browser.py b/tests/agent_features/test_asgi_browser.py index 4146d507b6..281d08b967 100644 --- a/tests/agent_features/test_asgi_browser.py +++ b/tests/agent_features/test_asgi_browser.py @@ -31,6 +31,7 @@ from newrelic.api.transaction import ( add_custom_attribute, disable_browser_autorum, + get_browser_timing_footer, get_browser_timing_header, ) from newrelic.common.encoding_utils import deobfuscate @@ -40,9 +41,9 @@ @asgi_application() async def target_asgi_application_manual_rum(scope, receive, send): - text = "%s

RESPONSE

" + text = "%s

RESPONSE

%s" - output = (text % get_browser_timing_header()).encode("UTF-8") + output = (text % (get_browser_timing_header(), get_browser_timing_footer())).encode("UTF-8") response_headers = [ (b"content-type", b"text/html; charset=utf-8"), @@ -55,15 +56,15 @@ async def target_asgi_application_manual_rum(scope, receive, send): target_application_manual_rum = AsgiTest(target_asgi_application_manual_rum) -_test_header_attributes = { +_test_footer_attributes = { "browser_monitoring.enabled": True, "browser_monitoring.auto_instrument": False, "js_agent_loader": "", } -@override_application_settings(_test_header_attributes) -def test_header_attributes(): +@override_application_settings(_test_footer_attributes) +def test_footer_attributes(): settings = application_settings() assert settings.browser_monitoring.enabled @@ -83,6 +84,7 @@ def test_header_attributes(): html = BeautifulSoup(response.body, "html.parser") header = html.html.head.script.string content = html.html.body.p.string + footer = html.html.body.script.string # Validate actual body content. @@ -92,10 +94,10 @@ def test_header_attributes(): assert header.find("NREUM HEADER") != -1 - # Now validate the various fields of the header. The fields are + # Now validate the various fields of the footer. The fields are # held by a JSON dictionary. - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + data = json.loads(footer.split("NREUM.info=")[1]) assert data["licenseKey"] == settings.browser_key assert data["applicationID"] == settings.application_id @@ -135,8 +137,8 @@ def test_ssl_for_http_is_none(): response = target_application_manual_rum.get("/") html = BeautifulSoup(response.body, "html.parser") - header = html.html.head.script.string - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + footer = html.html.body.script.string + data = json.loads(footer.split("NREUM.info=")[1]) assert "sslForHttp" not in data @@ -157,8 +159,8 @@ def test_ssl_for_http_is_true(): response = target_application_manual_rum.get("/") html = BeautifulSoup(response.body, "html.parser") - header = html.html.head.script.string - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + footer = html.html.body.script.string + data = json.loads(footer.split("NREUM.info=")[1]) assert data["sslForHttp"] is True @@ -179,8 +181,8 @@ def test_ssl_for_http_is_false(): response = target_application_manual_rum.get("/") html = BeautifulSoup(response.body, "html.parser") - header = html.html.head.script.string - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + footer = html.html.body.script.string + data = json.loads(footer.split("NREUM.info=")[1]) assert data["sslForHttp"] is False @@ -217,7 +219,7 @@ def test_html_insertion_yield_single_no_head(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" in response.body assert b"NREUM.info" in response.body @@ -257,7 +259,7 @@ def test_html_insertion_yield_multi_no_head(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" in response.body assert b"NREUM.info" in response.body @@ -297,7 +299,7 @@ def test_html_insertion_unnamed_attachment_header(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" not in response.body assert b"NREUM.info" not in response.body @@ -337,7 +339,7 @@ def test_html_insertion_named_attachment_header(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" not in response.body assert b"NREUM.info" not in response.body @@ -377,7 +379,7 @@ def test_html_insertion_inline_attachment_header(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" in response.body assert b"NREUM.info" in response.body @@ -412,7 +414,7 @@ def test_html_insertion_empty(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" not in response.body assert b"NREUM.info" not in response.body @@ -447,7 +449,7 @@ def test_html_insertion_single_empty_string(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" not in response.body assert b"NREUM.info" not in response.body @@ -483,7 +485,7 @@ def test_html_insertion_multiple_empty_string(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" not in response.body assert b"NREUM.info" not in response.body @@ -520,7 +522,7 @@ def test_html_insertion_single_large_prelude(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert "content-type" in response.headers assert "content-length" in response.headers @@ -564,7 +566,7 @@ def test_html_insertion_multi_large_prelude(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert "content-type" in response.headers assert "content-length" in response.headers @@ -882,7 +884,7 @@ def test_html_insertion_disable_autorum_via_api(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" not in response.body assert b"NREUM.info" not in response.body @@ -893,9 +895,13 @@ async def target_asgi_application_manual_rum_insertion(scope, receive, send): output = b"

RESPONSE

" header = get_browser_timing_header() + footer = get_browser_timing_footer() + header = get_browser_timing_header() + footer = get_browser_timing_footer() assert header == "" + assert footer == "" response_headers = [ (b"content-type", b"text/html; charset=utf-8"), @@ -925,7 +931,7 @@ def test_html_insertion_manual_rum_insertion(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert b"NREUM HEADER" not in response.body assert b"NREUM.info" not in response.body diff --git a/tests/agent_features/test_browser.py b/tests/agent_features/test_browser.py index 84ce795000..735cec5c12 100644 --- a/tests/agent_features/test_browser.py +++ b/tests/agent_features/test_browser.py @@ -13,7 +13,6 @@ # limitations under the License. import json -import re import sys import six @@ -30,6 +29,7 @@ from newrelic.api.transaction import ( add_custom_attribute, disable_browser_autorum, + get_browser_timing_footer, get_browser_timing_header, ) from newrelic.api.web_transaction import web_transaction @@ -43,9 +43,9 @@ def target_wsgi_application_manual_rum(environ, start_response): status = "200 OK" - text = "%s

RESPONSE

" + text = "%s

RESPONSE

%s" - output = (text % get_browser_timing_header()).encode("UTF-8") + output = (text % (get_browser_timing_header(), get_browser_timing_footer())).encode("UTF-8") response_headers = [("Content-Type", "text/html; charset=utf-8"), ("Content-Length", str(len(output)))] start_response(status, response_headers) @@ -55,15 +55,15 @@ def target_wsgi_application_manual_rum(environ, start_response): target_application_manual_rum = webtest.TestApp(target_wsgi_application_manual_rum) -_test_header_attributes = { +_test_footer_attributes = { "browser_monitoring.enabled": True, "browser_monitoring.auto_instrument": False, "js_agent_loader": "", } -@override_application_settings(_test_header_attributes) -def test_header_attributes(): +@override_application_settings(_test_footer_attributes) +def test_footer_attributes(): settings = application_settings() assert settings.browser_monitoring.enabled @@ -82,6 +82,7 @@ def test_header_attributes(): header = response.html.html.head.script.string content = response.html.html.body.p.string + footer = response.html.html.body.script.string # Validate actual body content. @@ -91,10 +92,10 @@ def test_header_attributes(): assert header.find("NREUM HEADER") != -1 - # Now validate the various fields of the header. The fields are + # Now validate the various fields of the footer. The fields are # held by a JSON dictionary. - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + data = json.loads(footer.split("NREUM.info=")[1]) assert data["licenseKey"] == settings.browser_key assert data["applicationID"] == settings.application_id @@ -133,8 +134,8 @@ def test_ssl_for_http_is_none(): assert settings.browser_monitoring.ssl_for_http is None response = target_application_manual_rum.get("/") - header = response.html.html.head.script.string - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + footer = response.html.html.body.script.string + data = json.loads(footer.split("NREUM.info=")[1]) assert "sslForHttp" not in data @@ -154,8 +155,8 @@ def test_ssl_for_http_is_true(): assert settings.browser_monitoring.ssl_for_http is True response = target_application_manual_rum.get("/") - header = response.html.html.head.script.string - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + footer = response.html.html.body.script.string + data = json.loads(footer.split("NREUM.info=")[1]) assert data["sslForHttp"] is True @@ -175,8 +176,8 @@ def test_ssl_for_http_is_false(): assert settings.browser_monitoring.ssl_for_http is False response = target_application_manual_rum.get("/") - header = response.html.html.head.script.string - data = json.loads(header.split("NREUM.info=")[1].split(";\n")[0]) + footer = response.html.html.body.script.string + data = json.loads(footer.split("NREUM.info=")[1]) assert data["sslForHttp"] is False @@ -211,7 +212,7 @@ def test_html_insertion_yield_single_no_head(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain("NREUM HEADER", "NREUM.info") @@ -247,7 +248,7 @@ def test_html_insertion_yield_multi_no_head(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain("NREUM HEADER", "NREUM.info") @@ -287,7 +288,7 @@ def test_html_insertion_unnamed_attachment_header(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) @@ -327,7 +328,7 @@ def test_html_insertion_named_attachment_header(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) @@ -367,7 +368,7 @@ def test_html_insertion_inline_attachment_header(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain("NREUM HEADER", "NREUM.info") @@ -400,7 +401,7 @@ def test_html_insertion_empty_list(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) @@ -435,7 +436,7 @@ def test_html_insertion_single_empty_string(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) @@ -470,7 +471,7 @@ def test_html_insertion_multiple_empty_string(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) @@ -504,7 +505,7 @@ def test_html_insertion_single_large_prelude(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert "Content-Type" in response.headers assert "Content-Length" in response.headers @@ -543,7 +544,7 @@ def test_html_insertion_multi_large_prelude(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert "Content-Type" in response.headers assert "Content-Length" in response.headers @@ -588,7 +589,7 @@ def test_html_insertion_yield_before_start(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain("NREUM HEADER", "NREUM.info") @@ -626,7 +627,7 @@ def test_html_insertion_start_yield_start(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. assert "Content-Type" in response.headers assert "Content-Length" in response.headers @@ -979,7 +980,7 @@ def test_html_insertion_disable_autorum_via_api(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) @@ -991,9 +992,13 @@ def target_wsgi_application_manual_rum_insertion(environ, start_response): output = b"

RESPONSE

" header = get_browser_timing_header() + footer = get_browser_timing_footer() + header = get_browser_timing_header() + footer = get_browser_timing_footer() assert header == "" + assert footer == "" response_headers = [("Content-Type", "text/html; charset=utf-8"), ("Content-Length", str(len(output)))] start_response(status, response_headers) @@ -1019,42 +1024,34 @@ def test_html_insertion_manual_rum_insertion(): # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) -_test_get_browser_timing_snippet_with_nonces = { +_test_get_browser_timing_nonces_settings = { "browser_monitoring.enabled": True, "browser_monitoring.auto_instrument": False, "js_agent_loader": "", } -_test_get_browser_timing_snippet_with_nonces_rum_info_re = re.compile(r"NREUM\.info={[^}]*}") - -@override_application_settings(_test_get_browser_timing_snippet_with_nonces) -@web_transaction( - scheme="http", host="127.0.0.1", port=80, request_method="GET", request_path="/", query_string=None, headers={} -) -def test_get_browser_timing_snippet_with_nonces(): +@override_application_settings(_test_get_browser_timing_nonces_settings) +@web_transaction(scheme="http", host="127.0.0.1", port=80, request_method="GET", + request_path="/", query_string=None, headers={}) +def test_get_browser_timing_nonces(): header = get_browser_timing_header("NONCE") + footer = get_browser_timing_footer("NONCE") - header = _test_get_browser_timing_snippet_with_nonces_rum_info_re.sub("NREUM.info={}", header) - assert ( - header - == '' - ) + assert header == '' + assert '' - ) + assert header == '' + assert ' + EXPECTED_RUM_FOOTER_LOCATION + diff --git a/tests/cross_agent/test_cat_map.py b/tests/cross_agent/test_cat_map.py index ea011990a8..6e7ac63d6d 100644 --- a/tests/cross_agent/test_cat_map.py +++ b/tests/cross_agent/test_cat_map.py @@ -43,6 +43,7 @@ from newrelic.api.external_trace import ExternalTrace from newrelic.api.transaction import ( current_transaction, + get_browser_timing_footer, get_browser_timing_header, set_background_task, set_transaction_name, @@ -133,9 +134,9 @@ def target_wsgi_application(environ, start_response): set_background_task(True) set_transaction_name(txn_name[2], group=txn_name[1]) - text = "%s

RESPONSE

" + text = "%s

RESPONSE

%s" - output = (text % get_browser_timing_header()).encode("UTF-8") + output = (text % (get_browser_timing_header(), get_browser_timing_footer())).encode("UTF-8") response_headers = [("Content-type", "text/html; charset=utf-8"), ("Content-Length", str(len(output)))] start_response(status, response_headers) @@ -192,6 +193,7 @@ def test_cat_map( @override_application_settings(_custom_settings) @override_application_name(appName) def run_cat_test(): + if six.PY2: txn_name = transactionName.encode("UTF-8") guid = transactionGuid.encode("UTF-8") diff --git a/tests/cross_agent/test_rum_client_config.py b/tests/cross_agent/test_rum_client_config.py new file mode 100644 index 0000000000..5b8da4b84c --- /dev/null +++ b/tests/cross_agent/test_rum_client_config.py @@ -0,0 +1,145 @@ +# 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 json +import os + +import pytest +import webtest +from testing_support.fixtures import override_application_settings + +from newrelic.api.transaction import ( + add_custom_attribute, + get_browser_timing_footer, + set_transaction_name, +) +from newrelic.api.wsgi_application import wsgi_application + +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) +FIXTURE = os.path.join(CURRENT_DIR, "fixtures", "rum_client_config.json") + +def _load_tests(): + with open(FIXTURE, "r") as fh: + js = fh.read() + return json.loads(js) + + +fields = [ + "testname", + "apptime_milliseconds", + "queuetime_milliseconds", + "browser_monitoring.attributes.enabled", + "transaction_name", + "license_key", + "connect_reply", + "user_attributes", + "expected", +] + +# Replace . as not a valid character in python argument names + +field_names = ",".join([f.replace(".", "_") for f in fields]) + + +def _parametrize_test(test): + return tuple([test.get(f, None) for f in fields]) + + +_rum_tests = [_parametrize_test(t) for t in _load_tests()] + + +@wsgi_application() +def target_wsgi_application(environ, start_response): + status = "200 OK" + + txn_name = environ.get("txn_name") + set_transaction_name(txn_name, group="") + + user_attrs = json.loads(environ.get("user_attrs")) + for key, value in user_attrs.items(): + add_custom_attribute(key, value) + + text = "%s

RESPONSE

" + + output = (text % get_browser_timing_footer()).encode("UTF-8") + + response_headers = [("Content-Type", "text/html; charset=utf-8"), ("Content-Length", str(len(output)))] + start_response(status, response_headers) + + return [output] + + +target_application = webtest.TestApp(target_wsgi_application) + + +@pytest.mark.parametrize(field_names, _rum_tests) +def test_browser_montioring( + testname, + apptime_milliseconds, + queuetime_milliseconds, + browser_monitoring_attributes_enabled, + transaction_name, + license_key, + connect_reply, + user_attributes, + expected, +): + + settings = { + "browser_monitoring.attributes.enabled": browser_monitoring_attributes_enabled, + "license_key": license_key, + "js_agent_loader": "", + } + settings.update(connect_reply) + + @override_application_settings(settings) + def run_browser_data_test(): + + response = target_application.get( + "/", extra_environ={"txn_name": str(transaction_name), "user_attrs": json.dumps(user_attributes)} + ) + + # We actually put the "footer" in the header, the first script is the + # agent "header", the second one is where the data lives, hence the [1]. + + footer = response.html.html.head.find_all("script")[1] + footer_data = json.loads(footer.string.split("NREUM.info=")[1]) + + # Not feasible to test the time metric values in testing + + expected.pop("queueTime") + expected.pop("applicationTime") + assert footer_data["applicationTime"] >= 0 + assert footer_data["queueTime"] >= 0 + + # Python always prepends stuff to the transaction name, so this + # doesn't match the obscured value. + + expected.pop("transactionName") + + # Check that all other values are correct + + for key, value in expected.items(): + + # If there are no attributes, the spec allows us to omit the + # 'atts' field altogether, so we do. But, the cross agent tests + # don't omit it, so we need to special case 'atts' when we compare + # to 'expected'. + + if key == "atts" and value == "": + assert key not in footer_data + else: + assert footer_data[key] == value + + run_browser_data_test() diff --git a/tests/framework_bottle/test_application.py b/tests/framework_bottle/test_application.py index cdcd90e3f3..28619d5eb5 100644 --- a/tests/framework_bottle/test_application.py +++ b/tests/framework_bottle/test_application.py @@ -12,238 +12,218 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest import base64 -import pytest -import webtest -from bottle import __version__ as version from testing_support.fixtures import ( - override_application_settings, override_ignore_status_codes, -) -from testing_support.validators.validate_code_level_metrics import ( - validate_code_level_metrics, -) -from testing_support.validators.validate_transaction_errors import ( - validate_transaction_errors, -) -from testing_support.validators.validate_transaction_metrics import ( - validate_transaction_metrics, -) - -from newrelic.common.package_version_utils import get_package_version_tuple + override_application_settings) +from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics from newrelic.packages import six +from testing_support.validators.validate_code_level_metrics import validate_code_level_metrics +from testing_support.validators.validate_transaction_errors import validate_transaction_errors + +import webtest -version = list(get_package_version_tuple("bottle")) +from bottle import __version__ as version + +version = [int(x) for x in version.split('-')[0].split('.')] if len(version) == 2: version.append(0) version = tuple(version) -assert version > (0, 1), "version information not found" -requires_auth_basic = pytest.mark.skipif(version < (0, 9, 0), reason="Bottle only added auth_basic in 0.9.0.") -requires_plugins = pytest.mark.skipif(version < (0, 9, 0), reason="Bottle only added auth_basic in 0.9.0.") +requires_auth_basic = pytest.mark.skipif(version < (0, 9, 0), + reason="Bottle only added auth_basic in 0.9.0.") +requires_plugins = pytest.mark.skipif(version < (0, 9, 0), + reason="Bottle only added auth_basic in 0.9.0.") _test_application_index_scoped_metrics = [ - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_target_application:index_page", 1), -] + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_target_application:index_page', 1)] if version >= (0, 9, 0): - _test_application_index_scoped_metrics.extend([("Function/bottle:Bottle.wsgi", 1)]) + _test_application_index_scoped_metrics.extend([ + ('Function/bottle:Bottle.wsgi', 1)]) else: - _test_application_index_scoped_metrics.extend([("Function/bottle:Bottle.__call__", 1)]) - -_test_application_index_custom_metrics = [("Python/Framework/Bottle/%s.%s.%s" % version, 1)] + _test_application_index_scoped_metrics.extend([ + ('Function/bottle:Bottle.__call__', 1)]) +_test_application_index_custom_metrics = [ + ('Python/Framework/Bottle/%s.%s.%s' % version, 1)] @validate_code_level_metrics("_target_application", "index_page") @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_target_application:index_page", - scoped_metrics=_test_application_index_scoped_metrics, - custom_metrics=_test_application_index_custom_metrics, -) +@validate_transaction_metrics('_target_application:index_page', + scoped_metrics=_test_application_index_scoped_metrics, + custom_metrics=_test_application_index_custom_metrics) def test_application_index(target_application): - response = target_application.get("/index") - response.mustcontain("INDEX RESPONSE") - + response = target_application.get('/index') + response.mustcontain('INDEX RESPONSE') _test_application_error_scoped_metrics = [ - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_target_application:error_page", 1), -] + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_target_application:error_page', 1)] if version >= (0, 9, 0): - _test_application_error_scoped_metrics.extend([("Function/bottle:Bottle.wsgi", 1)]) + _test_application_error_scoped_metrics.extend([ + ('Function/bottle:Bottle.wsgi', 1)]) else: - _test_application_error_scoped_metrics.extend([("Function/bottle:Bottle.__call__", 1)]) + _test_application_error_scoped_metrics.extend([ + ('Function/bottle:Bottle.__call__', 1)]) -_test_application_error_custom_metrics = [("Python/Framework/Bottle/%s.%s.%s" % version, 1)] +_test_application_error_custom_metrics = [ + ('Python/Framework/Bottle/%s.%s.%s' % version, 1)] if six.PY3: - _test_application_error_errors = ["builtins:RuntimeError"] + _test_application_error_errors = ['builtins:RuntimeError'] else: - _test_application_error_errors = ["exceptions:RuntimeError"] - + _test_application_error_errors = ['exceptions:RuntimeError'] @validate_code_level_metrics("_target_application", "error_page") @validate_transaction_errors(errors=_test_application_error_errors) -@validate_transaction_metrics( - "_target_application:error_page", - scoped_metrics=_test_application_error_scoped_metrics, - custom_metrics=_test_application_error_custom_metrics, -) +@validate_transaction_metrics('_target_application:error_page', + scoped_metrics=_test_application_error_scoped_metrics, + custom_metrics=_test_application_error_custom_metrics) def test_application_error(target_application): - response = target_application.get("/error", status=500, expect_errors=True) - + response = target_application.get('/error', status=500, expect_errors=True) _test_application_not_found_scoped_metrics = [ - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_target_application:error404_page", 1), -] + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_target_application:error404_page', 1)] if version >= (0, 9, 0): - _test_application_not_found_scoped_metrics.extend([("Function/bottle:Bottle.wsgi", 1)]) + _test_application_not_found_scoped_metrics.extend([ + ('Function/bottle:Bottle.wsgi', 1)]) else: - _test_application_not_found_scoped_metrics.extend([("Function/bottle:Bottle.__call__", 1)]) - -_test_application_not_found_custom_metrics = [("Python/Framework/Bottle/%s.%s.%s" % version, 1)] + _test_application_not_found_scoped_metrics.extend([ + ('Function/bottle:Bottle.__call__', 1)]) +_test_application_not_found_custom_metrics = [ + ('Python/Framework/Bottle/%s.%s.%s' % version, 1)] @validate_code_level_metrics("_target_application", "error404_page") @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_target_application:error404_page", - scoped_metrics=_test_application_not_found_scoped_metrics, - custom_metrics=_test_application_not_found_custom_metrics, -) +@validate_transaction_metrics('_target_application:error404_page', + scoped_metrics=_test_application_not_found_scoped_metrics, + custom_metrics=_test_application_not_found_custom_metrics) def test_application_not_found(target_application): - response = target_application.get("/missing", status=404) - response.mustcontain("NOT FOUND") - + response = target_application.get('/missing', status=404) + response.mustcontain('NOT FOUND') _test_application_auth_basic_fail_scoped_metrics = [ - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_target_application:auth_basic_page", 1), -] + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_target_application:auth_basic_page', 1)] if version >= (0, 9, 0): - _test_application_auth_basic_fail_scoped_metrics.extend([("Function/bottle:Bottle.wsgi", 1)]) + _test_application_auth_basic_fail_scoped_metrics.extend([ + ('Function/bottle:Bottle.wsgi', 1)]) else: - _test_application_auth_basic_fail_scoped_metrics.extend([("Function/bottle:Bottle.__call__", 1)]) - -_test_application_auth_basic_fail_custom_metrics = [("Python/Framework/Bottle/%s.%s.%s" % version, 1)] + _test_application_auth_basic_fail_scoped_metrics.extend([ + ('Function/bottle:Bottle.__call__', 1)]) +_test_application_auth_basic_fail_custom_metrics = [ + ('Python/Framework/Bottle/%s.%s.%s' % version, 1)] @requires_auth_basic @validate_code_level_metrics("_target_application", "auth_basic_page") @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_target_application:auth_basic_page", - scoped_metrics=_test_application_auth_basic_fail_scoped_metrics, - custom_metrics=_test_application_auth_basic_fail_custom_metrics, -) +@validate_transaction_metrics('_target_application:auth_basic_page', + scoped_metrics=_test_application_auth_basic_fail_scoped_metrics, + custom_metrics=_test_application_auth_basic_fail_custom_metrics) def test_application_auth_basic_fail(target_application): - response = target_application.get("/auth", status=401) - + response = target_application.get('/auth', status=401) _test_application_auth_basic_okay_scoped_metrics = [ - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_target_application:auth_basic_page", 1), -] + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_target_application:auth_basic_page', 1)] if version >= (0, 9, 0): - _test_application_auth_basic_okay_scoped_metrics.extend([("Function/bottle:Bottle.wsgi", 1)]) + _test_application_auth_basic_okay_scoped_metrics.extend([ + ('Function/bottle:Bottle.wsgi', 1)]) else: - _test_application_auth_basic_okay_scoped_metrics.extend([("Function/bottle:Bottle.__call__", 1)]) - -_test_application_auth_basic_okay_custom_metrics = [("Python/Framework/Bottle/%s.%s.%s" % version, 1)] + _test_application_auth_basic_okay_scoped_metrics.extend([ + ('Function/bottle:Bottle.__call__', 1)]) +_test_application_auth_basic_okay_custom_metrics = [ + ('Python/Framework/Bottle/%s.%s.%s' % version, 1)] @requires_auth_basic @validate_code_level_metrics("_target_application", "auth_basic_page") @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_target_application:auth_basic_page", - scoped_metrics=_test_application_auth_basic_okay_scoped_metrics, - custom_metrics=_test_application_auth_basic_okay_custom_metrics, -) +@validate_transaction_metrics('_target_application:auth_basic_page', + scoped_metrics=_test_application_auth_basic_okay_scoped_metrics, + custom_metrics=_test_application_auth_basic_okay_custom_metrics) def test_application_auth_basic_okay(target_application): - authorization_value = base64.b64encode(b"user:password") + authorization_value = base64.b64encode(b'user:password') if six.PY3: - authorization_value = authorization_value.decode("Latin-1") - environ = {"HTTP_AUTHORIZATION": "Basic " + authorization_value} - response = target_application.get("/auth", extra_environ=environ) - response.mustcontain("AUTH OKAY") - + authorization_value = authorization_value.decode('Latin-1') + environ = { 'HTTP_AUTHORIZATION': 'Basic ' + authorization_value } + response = target_application.get('/auth', extra_environ=environ) + response.mustcontain('AUTH OKAY') _test_application_plugin_error_scoped_metrics = [ - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_target_application:plugin_error_page", 1), -] + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_target_application:plugin_error_page', 1)] if version >= (0, 9, 0): - _test_application_plugin_error_scoped_metrics.extend([("Function/bottle:Bottle.wsgi", 1)]) + _test_application_plugin_error_scoped_metrics.extend([ + ('Function/bottle:Bottle.wsgi', 1)]) else: - _test_application_plugin_error_scoped_metrics.extend([("Function/bottle:Bottle.__call__", 1)]) - -_test_application_plugin_error_custom_metrics = [("Python/Framework/Bottle/%s.%s.%s" % version, 1)] + _test_application_plugin_error_scoped_metrics.extend([ + ('Function/bottle:Bottle.__call__', 1)]) +_test_application_plugin_error_custom_metrics = [ + ('Python/Framework/Bottle/%s.%s.%s' % version, 1)] @requires_plugins @validate_code_level_metrics("_target_application", "plugin_error_page") @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_target_application:plugin_error_page", - scoped_metrics=_test_application_plugin_error_scoped_metrics, - custom_metrics=_test_application_plugin_error_custom_metrics, -) +@validate_transaction_metrics('_target_application:plugin_error_page', + scoped_metrics=_test_application_plugin_error_scoped_metrics, + custom_metrics=_test_application_plugin_error_custom_metrics) @override_ignore_status_codes([403]) def test_application_plugin_error_ignore(target_application): - response = target_application.get("/plugin_error", status=403, expect_errors=True) - + response = target_application.get('/plugin_error', status=403, + expect_errors=True) @requires_plugins @validate_code_level_metrics("_target_application", "plugin_error_page") -@validate_transaction_errors(errors=["bottle:HTTPError"]) -@validate_transaction_metrics( - "_target_application:plugin_error_page", - scoped_metrics=_test_application_plugin_error_scoped_metrics, - custom_metrics=_test_application_plugin_error_custom_metrics, -) +@validate_transaction_errors(errors=['bottle:HTTPError']) +@validate_transaction_metrics('_target_application:plugin_error_page', + scoped_metrics=_test_application_plugin_error_scoped_metrics, + custom_metrics=_test_application_plugin_error_custom_metrics) def test_application_plugin_error_capture(target_application): import newrelic.agent - - response = target_application.get("/plugin_error", status=403, expect_errors=True) - + response = target_application.get('/plugin_error', status=403, + expect_errors=True) _test_html_insertion_settings = { - "browser_monitoring.enabled": True, - "browser_monitoring.auto_instrument": True, - "js_agent_loader": "", + 'browser_monitoring.enabled': True, + 'browser_monitoring.auto_instrument': True, + 'js_agent_loader': u'', } - @override_application_settings(_test_html_insertion_settings) def test_html_insertion(target_application): - response = target_application.get("/html_insertion") + response = target_application.get('/html_insertion') # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. + + response.mustcontain('NREUM HEADER', 'NREUM.info') - response.mustcontain("NREUM HEADER", "NREUM.info") diff --git a/tests/framework_cherrypy/test_application.py b/tests/framework_cherrypy/test_application.py index dd4595c0b8..39f8b5c16d 100644 --- a/tests/framework_cherrypy/test_application.py +++ b/tests/framework_cherrypy/test_application.py @@ -12,33 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cherrypy import pytest import webtest -from testing_support.fixtures import ( - override_application_settings, - override_ignore_status_codes, -) -from testing_support.validators.validate_code_level_metrics import ( - validate_code_level_metrics, -) -from testing_support.validators.validate_transaction_errors import ( - validate_transaction_errors, -) from newrelic.packages import six -CHERRYPY_VERSION = tuple(int(v) for v in cherrypy.__version__.split(".")) +from testing_support.fixtures import ( + override_application_settings, + override_ignore_status_codes) +from testing_support.validators.validate_code_level_metrics import validate_code_level_metrics +from testing_support.validators.validate_transaction_errors import validate_transaction_errors + +import cherrypy + +CHERRYPY_VERSION = tuple(int(v) for v in cherrypy.__version__.split('.')) class Application(object): + @cherrypy.expose def index(self): - return "INDEX RESPONSE" + return 'INDEX RESPONSE' @cherrypy.expose def error(self): - raise RuntimeError("error") + raise RuntimeError('error') @cherrypy.expose def not_found(self): @@ -50,37 +48,35 @@ def not_found_as_http_error(self): @cherrypy.expose def not_found_as_str_http_error(self): - raise cherrypy.HTTPError("404 Not Found") + raise cherrypy.HTTPError('404 Not Found') @cherrypy.expose def bad_http_error(self): # this will raise HTTPError with status code 500 because 10 is not a # valid status code - raise cherrypy.HTTPError("10 Invalid status code") + raise cherrypy.HTTPError('10 Invalid status code') @cherrypy.expose def internal_redirect(self): - raise cherrypy.InternalRedirect("/") + raise cherrypy.InternalRedirect('/') @cherrypy.expose def external_redirect(self): - raise cherrypy.HTTPRedirect("/") + raise cherrypy.HTTPRedirect('/') @cherrypy.expose def upload_files(self, files): - return "UPLOAD FILES RESPONSE" + return 'UPLOAD FILES RESPONSE' @cherrypy.expose def encode_multipart(self, field, files): - return "ENCODE MULTIPART RESPONSE" + return 'ENCODE MULTIPART RESPONSE' @cherrypy.expose def html_insertion(self): - return ( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) + return ('Some header' + '

My First Heading

My first paragraph.

' + '') application = cherrypy.Application(Application()) @@ -90,97 +86,99 @@ def html_insertion(self): @validate_code_level_metrics("test_application.Application", "index") @validate_transaction_errors(errors=[]) def test_application_index(): - response = test_application.get("") - response.mustcontain("INDEX RESPONSE") + response = test_application.get('') + response.mustcontain('INDEX RESPONSE') @validate_transaction_errors(errors=[]) def test_application_index_agent_disabled(): - environ = {"newrelic.enabled": False} - response = test_application.get("", extra_environ=environ) - response.mustcontain("INDEX RESPONSE") + environ = {'newrelic.enabled': False} + response = test_application.get('', extra_environ=environ) + response.mustcontain('INDEX RESPONSE') @validate_transaction_errors(errors=[]) def test_application_missing(): - test_application.get("/missing", status=404) + test_application.get('/missing', status=404) if six.PY3: - _test_application_unexpected_exception_errors = ["builtins:RuntimeError"] + _test_application_unexpected_exception_errors = ['builtins:RuntimeError'] else: - _test_application_unexpected_exception_errors = ["exceptions:RuntimeError"] + _test_application_unexpected_exception_errors = ['exceptions:RuntimeError'] -@validate_transaction_errors(errors=_test_application_unexpected_exception_errors) +@validate_transaction_errors( + errors=_test_application_unexpected_exception_errors) def test_application_unexpected_exception(): - test_application.get("/error", status=500) + test_application.get('/error', status=500) @validate_transaction_errors(errors=[]) def test_application_not_found(): - test_application.get("/not_found", status=404) + test_application.get('/not_found', status=404) @validate_transaction_errors(errors=[]) def test_application_not_found_as_http_error(): - test_application.get("/not_found_as_http_error", status=404) + test_application.get('/not_found_as_http_error', status=404) @validate_transaction_errors(errors=[]) def test_application_internal_redirect(): - response = test_application.get("/internal_redirect") - response.mustcontain("INDEX RESPONSE") + response = test_application.get('/internal_redirect') + response.mustcontain('INDEX RESPONSE') @validate_transaction_errors(errors=[]) def test_application_external_redirect(): - test_application.get("/external_redirect", status=302) + test_application.get('/external_redirect', status=302) @validate_transaction_errors(errors=[]) def test_application_upload_files(): - test_application.post("/upload_files", upload_files=[("files", __file__)]) + test_application.post('/upload_files', upload_files=[('files', __file__)]) @validate_transaction_errors(errors=[]) def test_application_encode_multipart(): - content_type, body = test_application.encode_multipart(params=[("field", "value")], files=[("files", __file__)]) - test_application.request("/encode_multipart", method="POST", content_type=content_type, body=body) + content_type, body = test_application.encode_multipart( + params=[('field', 'value')], files=[('files', __file__)]) + test_application.request('/encode_multipart', method='POST', + content_type=content_type, body=body) _test_html_insertion_settings = { - "browser_monitoring.enabled": True, - "browser_monitoring.auto_instrument": True, - "js_agent_loader": "", + 'browser_monitoring.enabled': True, + 'browser_monitoring.auto_instrument': True, + 'js_agent_loader': u'', } @override_application_settings(_test_html_insertion_settings) def test_html_insertion(): - response = test_application.get("/html_insertion") + response = test_application.get('/html_insertion') # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. - response.mustcontain("NREUM HEADER", "NREUM.info") + response.mustcontain('NREUM HEADER', 'NREUM.info') -_error_endpoints = ["/not_found_as_http_error"] +_error_endpoints = ['/not_found_as_http_error'] if CHERRYPY_VERSION >= (3, 2): - _error_endpoints.extend(["/not_found_as_str_http_error", "/bad_http_error"]) + _error_endpoints.extend(['/not_found_as_str_http_error', + '/bad_http_error']) -@pytest.mark.parametrize("endpoint", _error_endpoints) -@pytest.mark.parametrize( - "ignore_overrides,expected_errors", - [ - ([], ["cherrypy._cperror:HTTPError"]), - ([404, 500], []), - ], -) +@pytest.mark.parametrize('endpoint', _error_endpoints) +@pytest.mark.parametrize('ignore_overrides,expected_errors', [ + ([], ['cherrypy._cperror:HTTPError']), + ([404, 500], []), +]) def test_ignore_status_code(endpoint, ignore_overrides, expected_errors): + @validate_transaction_errors(errors=expected_errors) @override_ignore_status_codes(ignore_overrides) def _test(): @@ -191,5 +189,5 @@ def _test(): @validate_transaction_errors(errors=[]) def test_ignore_status_unexpected_param(): - response = test_application.get("/?arg=1", status=404) - response.mustcontain(no=["INDEX RESPONSE"]) + response = test_application.get('/?arg=1', status=404) + response.mustcontain(no=['INDEX RESPONSE']) diff --git a/tests/framework_django/templates/main.html b/tests/framework_django/templates/main.html index 5de5a534a3..bcf5afda39 100644 --- a/tests/framework_django/templates/main.html +++ b/tests/framework_django/templates/main.html @@ -26,5 +26,6 @@

My First Heading

My first paragraph.

{% show_results %} + {% newrelic_browser_timing_footer %} diff --git a/tests/framework_django/test_application.py b/tests/framework_django/test_application.py index 82501707b2..1f2616b0fa 100644 --- a/tests/framework_django/test_application.py +++ b/tests/framework_django/test_application.py @@ -12,33 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - -import django from testing_support.fixtures import ( override_application_settings, - override_generic_settings, - override_ignore_status_codes, -) -from testing_support.validators.validate_code_level_metrics import ( - validate_code_level_metrics, -) -from testing_support.validators.validate_transaction_errors import ( - validate_transaction_errors, -) -from testing_support.validators.validate_transaction_metrics import ( - validate_transaction_metrics, -) - + override_generic_settings, override_ignore_status_codes) +from testing_support.validators.validate_code_level_metrics import validate_code_level_metrics from newrelic.hooks.framework_django import django_settings +from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics +from testing_support.validators.validate_transaction_errors import validate_transaction_errors + +import os + +import django -DJANGO_VERSION = tuple(map(int, django.get_version().split(".")[:2])) -DJANGO_SETTINGS_MODULE = os.environ.get("DJANGO_SETTINGS_MODULE", None) +DJANGO_VERSION = tuple(map(int, django.get_version().split('.')[:2])) +DJANGO_SETTINGS_MODULE = os.environ.get('DJANGO_SETTINGS_MODULE', None) def target_application(): from _target_application import _target_application - return _target_application @@ -46,233 +37,272 @@ def target_application(): # MIDDLEWARE defined in the version-specific Django settings.py file. _test_django_pre_1_10_middleware_scoped_metrics = [ - (("Function/django.middleware.common:" "CommonMiddleware.process_request"), 1), - (("Function/django.contrib.sessions.middleware:" "SessionMiddleware.process_request"), 1), - (("Function/django.contrib.auth.middleware:" "AuthenticationMiddleware.process_request"), 1), - (("Function/django.contrib.messages.middleware:" "MessageMiddleware.process_request"), 1), - (("Function/django.middleware.csrf:" "CsrfViewMiddleware.process_view"), 1), - (("Function/django.contrib.messages.middleware:" "MessageMiddleware.process_response"), 1), - (("Function/django.middleware.csrf:" "CsrfViewMiddleware.process_response"), 1), - (("Function/django.contrib.sessions.middleware:" "SessionMiddleware.process_response"), 1), - (("Function/django.middleware.common:" "CommonMiddleware.process_response"), 1), - (("Function/django.middleware.gzip:" "GZipMiddleware.process_response"), 1), - (("Function/newrelic.hooks.framework_django:" "browser_timing_insertion"), 1), + (('Function/django.middleware.common:' + 'CommonMiddleware.process_request'), 1), + (('Function/django.contrib.sessions.middleware:' + 'SessionMiddleware.process_request'), 1), + (('Function/django.contrib.auth.middleware:' + 'AuthenticationMiddleware.process_request'), 1), + (('Function/django.contrib.messages.middleware:' + 'MessageMiddleware.process_request'), 1), + (('Function/django.middleware.csrf:' + 'CsrfViewMiddleware.process_view'), 1), + (('Function/django.contrib.messages.middleware:' + 'MessageMiddleware.process_response'), 1), + (('Function/django.middleware.csrf:' + 'CsrfViewMiddleware.process_response'), 1), + (('Function/django.contrib.sessions.middleware:' + 'SessionMiddleware.process_response'), 1), + (('Function/django.middleware.common:' + 'CommonMiddleware.process_response'), 1), + (('Function/django.middleware.gzip:' + 'GZipMiddleware.process_response'), 1), + (('Function/newrelic.hooks.framework_django:' + 'browser_timing_insertion'), 1), ] _test_django_post_1_10_middleware_scoped_metrics = [ - ("Function/django.middleware.security:SecurityMiddleware", 1), - ("Function/django.contrib.sessions.middleware:SessionMiddleware", 1), - ("Function/django.middleware.common:CommonMiddleware", 1), - ("Function/django.middleware.csrf:CsrfViewMiddleware", 1), - ("Function/django.contrib.auth.middleware:AuthenticationMiddleware", 1), - ("Function/django.contrib.messages.middleware:MessageMiddleware", 1), - ("Function/django.middleware.clickjacking:XFrameOptionsMiddleware", 1), - ("Function/django.middleware.gzip:GZipMiddleware", 1), + ('Function/django.middleware.security:SecurityMiddleware', 1), + ('Function/django.contrib.sessions.middleware:SessionMiddleware', 1), + ('Function/django.middleware.common:CommonMiddleware', 1), + ('Function/django.middleware.csrf:CsrfViewMiddleware', 1), + ('Function/django.contrib.auth.middleware:AuthenticationMiddleware', 1), + ('Function/django.contrib.messages.middleware:MessageMiddleware', 1), + ('Function/django.middleware.clickjacking:XFrameOptionsMiddleware', 1), + ('Function/django.middleware.gzip:GZipMiddleware', 1), ] _test_django_pre_1_10_url_resolver_scoped_metrics = [ - ("Function/django.core.urlresolvers:RegexURLResolver.resolve", "present"), + ('Function/django.core.urlresolvers:RegexURLResolver.resolve', 'present'), ] _test_django_post_1_10_url_resolver_scoped_metrics = [ - ("Function/django.urls.resolvers:RegexURLResolver.resolve", "present"), + ('Function/django.urls.resolvers:RegexURLResolver.resolve', 'present'), ] _test_django_post_2_0_url_resolver_scoped_metrics = [ - ("Function/django.urls.resolvers:URLResolver.resolve", "present"), + ('Function/django.urls.resolvers:URLResolver.resolve', 'present'), ] _test_application_index_scoped_metrics = [ - ("Function/django.core.handlers.wsgi:WSGIHandler.__call__", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/views:index", 1), + ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/views:index', 1), ] if DJANGO_VERSION >= (1, 5): - _test_application_index_scoped_metrics.extend([("Function/django.http.response:HttpResponse.close", 1)]) + _test_application_index_scoped_metrics.extend([ + ('Function/django.http.response:HttpResponse.close', 1)]) if DJANGO_VERSION < (1, 10): - _test_application_index_scoped_metrics.extend(_test_django_pre_1_10_url_resolver_scoped_metrics) + _test_application_index_scoped_metrics.extend( + _test_django_pre_1_10_url_resolver_scoped_metrics) elif DJANGO_VERSION >= (2, 0): - _test_application_index_scoped_metrics.extend(_test_django_post_2_0_url_resolver_scoped_metrics) + _test_application_index_scoped_metrics.extend( + _test_django_post_2_0_url_resolver_scoped_metrics) else: - _test_application_index_scoped_metrics.extend(_test_django_post_1_10_url_resolver_scoped_metrics) - -if DJANGO_SETTINGS_MODULE == "settings_0110_old": - _test_application_index_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) -elif DJANGO_SETTINGS_MODULE == "settings_0110_new": - _test_application_index_scoped_metrics.extend(_test_django_post_1_10_middleware_scoped_metrics) + _test_application_index_scoped_metrics.extend( + _test_django_post_1_10_url_resolver_scoped_metrics) + +if DJANGO_SETTINGS_MODULE == 'settings_0110_old': + _test_application_index_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) +elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': + _test_application_index_scoped_metrics.extend( + _test_django_post_1_10_middleware_scoped_metrics) elif DJANGO_VERSION < (1, 10): - _test_application_index_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) + _test_application_index_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("views:index", scoped_metrics=_test_application_index_scoped_metrics) +@validate_transaction_metrics('views:index', + scoped_metrics=_test_application_index_scoped_metrics) @validate_code_level_metrics("views", "index") def test_application_index(): test_application = target_application() - response = test_application.get("") - response.mustcontain("INDEX RESPONSE") + response = test_application.get('') + response.mustcontain('INDEX RESPONSE') -@validate_transaction_metrics("views:exception") +@validate_transaction_metrics('views:exception') @validate_code_level_metrics("views", "exception") def test_application_exception(): test_application = target_application() - test_application.get("/exception", status=500) + test_application.get('/exception', status=500) _test_application_not_found_scoped_metrics = [ - ("Function/django.core.handlers.wsgi:WSGIHandler.__call__", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), + ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), ] if DJANGO_VERSION >= (1, 5): - _test_application_not_found_scoped_metrics.extend([("Function/django.http.response:HttpResponseNotFound.close", 1)]) + _test_application_not_found_scoped_metrics.extend([ + ('Function/django.http.response:HttpResponseNotFound.close', 1)]) if DJANGO_VERSION < (1, 10): - _test_application_not_found_scoped_metrics.extend(_test_django_pre_1_10_url_resolver_scoped_metrics) + _test_application_not_found_scoped_metrics.extend( + _test_django_pre_1_10_url_resolver_scoped_metrics) elif DJANGO_VERSION >= (2, 0): - _test_application_not_found_scoped_metrics.extend(_test_django_post_2_0_url_resolver_scoped_metrics) + _test_application_not_found_scoped_metrics.extend( + _test_django_post_2_0_url_resolver_scoped_metrics) else: - _test_application_not_found_scoped_metrics.extend(_test_django_post_1_10_url_resolver_scoped_metrics) + _test_application_not_found_scoped_metrics.extend( + _test_django_post_1_10_url_resolver_scoped_metrics) -if DJANGO_SETTINGS_MODULE == "settings_0110_old": - _test_application_not_found_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) +if DJANGO_SETTINGS_MODULE == 'settings_0110_old': + _test_application_not_found_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) # The `CsrfViewMiddleware.process_view` isn't called for 404 Not Found. _test_application_not_found_scoped_metrics.remove( - ("Function/django.middleware.csrf:CsrfViewMiddleware.process_view", 1) - ) -elif DJANGO_SETTINGS_MODULE == "settings_0110_new": - _test_application_not_found_scoped_metrics.extend(_test_django_post_1_10_middleware_scoped_metrics) + ('Function/django.middleware.csrf:CsrfViewMiddleware.process_view', 1)) +elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': + _test_application_not_found_scoped_metrics.extend( + _test_django_post_1_10_middleware_scoped_metrics) elif DJANGO_VERSION < (1, 10): - _test_application_not_found_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) + _test_application_not_found_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) # The `CsrfViewMiddleware.process_view` isn't called for 404 Not Found. _test_application_not_found_scoped_metrics.remove( - ("Function/django.middleware.csrf:CsrfViewMiddleware.process_view", 1) - ) + ('Function/django.middleware.csrf:CsrfViewMiddleware.process_view', 1)) @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "django.views.debug:technical_404_response", scoped_metrics=_test_application_not_found_scoped_metrics -) +@validate_transaction_metrics('django.views.debug:technical_404_response', + scoped_metrics=_test_application_not_found_scoped_metrics) def test_application_not_found(): test_application = target_application() - test_application.get("/not_found", status=404) + test_application.get('/not_found', status=404) @override_ignore_status_codes([403]) @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("views:permission_denied") +@validate_transaction_metrics('views:permission_denied') @validate_code_level_metrics("views", "permission_denied") def test_ignored_status_code(): test_application = target_application() - test_application.get("/permission_denied", status=403) + test_application.get('/permission_denied', status=403) @override_ignore_status_codes([410]) @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("views:middleware_410") +@validate_transaction_metrics('views:middleware_410') @validate_code_level_metrics("views", "middleware_410") def test_middleware_ignore_status_codes(): test_application = target_application() - test_application.get("/middleware_410", status=410) + test_application.get('/middleware_410', status=410) _test_application_cbv_scoped_metrics = [ - ("Function/django.core.handlers.wsgi:WSGIHandler.__call__", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/views:MyView", 1), - ("Function/views:MyView.get", 1), + ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/views:MyView', 1), + ('Function/views:MyView.get', 1), ] if DJANGO_VERSION >= (1, 5): - _test_application_cbv_scoped_metrics.extend([("Function/django.http.response:HttpResponse.close", 1)]) + _test_application_cbv_scoped_metrics.extend([ + ('Function/django.http.response:HttpResponse.close', 1)]) if DJANGO_VERSION < (1, 10): - _test_application_cbv_scoped_metrics.extend(_test_django_pre_1_10_url_resolver_scoped_metrics) + _test_application_cbv_scoped_metrics.extend( + _test_django_pre_1_10_url_resolver_scoped_metrics) elif DJANGO_VERSION >= (2, 0): - _test_application_cbv_scoped_metrics.extend(_test_django_post_2_0_url_resolver_scoped_metrics) + _test_application_cbv_scoped_metrics.extend( + _test_django_post_2_0_url_resolver_scoped_metrics) else: - _test_application_cbv_scoped_metrics.extend(_test_django_post_1_10_url_resolver_scoped_metrics) - -if DJANGO_SETTINGS_MODULE == "settings_0110_old": - _test_application_cbv_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) -elif DJANGO_SETTINGS_MODULE == "settings_0110_new": - _test_application_cbv_scoped_metrics.extend(_test_django_post_1_10_middleware_scoped_metrics) + _test_application_cbv_scoped_metrics.extend( + _test_django_post_1_10_url_resolver_scoped_metrics) + +if DJANGO_SETTINGS_MODULE == 'settings_0110_old': + _test_application_cbv_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) +elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': + _test_application_cbv_scoped_metrics.extend( + _test_django_post_1_10_middleware_scoped_metrics) elif DJANGO_VERSION < (1, 10): - _test_application_cbv_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) + _test_application_cbv_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("views:MyView.get", scoped_metrics=_test_application_cbv_scoped_metrics) +@validate_transaction_metrics('views:MyView.get', + scoped_metrics=_test_application_cbv_scoped_metrics) @validate_code_level_metrics("views.MyView", "get") def test_application_cbv(): test_application = target_application() - response = test_application.get("/cbv") - response.mustcontain("CBV RESPONSE") + response = test_application.get('/cbv') + response.mustcontain('CBV RESPONSE') _test_application_deferred_cbv_scoped_metrics = [ - ("Function/django.core.handlers.wsgi:WSGIHandler.__call__", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/views:deferred_cbv", 1), - ("Function/views:MyView.get", 1), + ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/views:deferred_cbv', 1), + ('Function/views:MyView.get', 1), ] if DJANGO_VERSION >= (1, 5): - _test_application_deferred_cbv_scoped_metrics.extend([("Function/django.http.response:HttpResponse.close", 1)]) + _test_application_deferred_cbv_scoped_metrics.extend([ + ('Function/django.http.response:HttpResponse.close', 1)]) if DJANGO_VERSION < (1, 10): - _test_application_deferred_cbv_scoped_metrics.extend(_test_django_pre_1_10_url_resolver_scoped_metrics) + _test_application_deferred_cbv_scoped_metrics.extend( + _test_django_pre_1_10_url_resolver_scoped_metrics) elif DJANGO_VERSION >= (2, 0): - _test_application_deferred_cbv_scoped_metrics.extend(_test_django_post_2_0_url_resolver_scoped_metrics) + _test_application_deferred_cbv_scoped_metrics.extend( + _test_django_post_2_0_url_resolver_scoped_metrics) else: - _test_application_deferred_cbv_scoped_metrics.extend(_test_django_post_1_10_url_resolver_scoped_metrics) - -if DJANGO_SETTINGS_MODULE == "settings_0110_old": - _test_application_deferred_cbv_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) -elif DJANGO_SETTINGS_MODULE == "settings_0110_new": - _test_application_deferred_cbv_scoped_metrics.extend(_test_django_post_1_10_middleware_scoped_metrics) + _test_application_deferred_cbv_scoped_metrics.extend( + _test_django_post_1_10_url_resolver_scoped_metrics) + +if DJANGO_SETTINGS_MODULE == 'settings_0110_old': + _test_application_deferred_cbv_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) +elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': + _test_application_deferred_cbv_scoped_metrics.extend( + _test_django_post_1_10_middleware_scoped_metrics) elif DJANGO_VERSION < (1, 10): - _test_application_deferred_cbv_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) + _test_application_deferred_cbv_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("views:deferred_cbv", scoped_metrics=_test_application_deferred_cbv_scoped_metrics) +@validate_transaction_metrics('views:deferred_cbv', + scoped_metrics=_test_application_deferred_cbv_scoped_metrics) @validate_code_level_metrics("views", "deferred_cbv") def test_application_deferred_cbv(): test_application = target_application() - response = test_application.get("/deferred_cbv") - response.mustcontain("CBV RESPONSE") + response = test_application.get('/deferred_cbv') + response.mustcontain('CBV RESPONSE') _test_html_insertion_settings = { - "browser_monitoring.enabled": True, - "browser_monitoring.auto_instrument": True, - "js_agent_loader": "", + 'browser_monitoring.enabled': True, + 'browser_monitoring.auto_instrument': True, + 'js_agent_loader': u'', } @override_application_settings(_test_html_insertion_settings) def test_html_insertion_django_middleware(): test_application = target_application() - response = test_application.get("/html_insertion", status=200) + response = test_application.get('/html_insertion', status=200) # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. - response.mustcontain("NREUM HEADER", "NREUM.info") + response.mustcontain('NREUM HEADER', 'NREUM.info') @override_application_settings(_test_html_insertion_settings) @@ -281,22 +311,23 @@ def test_html_insertion_django_gzip_middleware_enabled(): # GZipMiddleware only fires if given the following header. - gzip_header = {"Accept-Encoding": "gzip"} - response = test_application.get("/gzip_html_insertion", status=200, headers=gzip_header) + gzip_header = {'Accept-Encoding': 'gzip'} + response = test_application.get('/gzip_html_insertion', status=200, + headers=gzip_header) # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. # The response.text will already be gunzipped - response.mustcontain("NREUM HEADER", "NREUM.info") + response.mustcontain('NREUM HEADER', 'NREUM.info') _test_html_insertion_settings_disabled = { - "browser_monitoring.enabled": False, - "browser_monitoring.auto_instrument": False, - "js_agent_loader": "", + 'browser_monitoring.enabled': False, + 'browser_monitoring.auto_instrument': False, + 'js_agent_loader': u'', } @@ -306,238 +337,264 @@ def test_html_insertion_django_gzip_middleware_disabled(): # GZipMiddleware only fires if given the following header. - gzip_header = {"Accept-Encoding": "gzip"} - response = test_application.get("/gzip_html_insertion", status=200, headers=gzip_header) + gzip_header = {'Accept-Encoding': 'gzip'} + response = test_application.get('/gzip_html_insertion', status=200, + headers=gzip_header) # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. # The response.text will already be gunzipped - response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) + response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) _test_html_insertion_manual_settings = { - "browser_monitoring.enabled": True, - "browser_monitoring.auto_instrument": True, - "js_agent_loader": "", + 'browser_monitoring.enabled': True, + 'browser_monitoring.auto_instrument': True, + 'js_agent_loader': u'', } @override_application_settings(_test_html_insertion_manual_settings) def test_html_insertion_manual_django_middleware(): test_application = target_application() - response = test_application.get("/html_insertion_manual", status=200) + response = test_application.get('/html_insertion_manual', status=200) # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. - response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) + response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) @override_application_settings(_test_html_insertion_settings) def test_html_insertion_unnamed_attachment_header_django_middleware(): test_application = target_application() - response = test_application.get("/html_insertion_unnamed_attachment_header", status=200) + response = test_application.get( + '/html_insertion_unnamed_attachment_header', status=200) # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. - response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) + response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) @override_application_settings(_test_html_insertion_settings) def test_html_insertion_named_attachment_header_django_middleware(): test_application = target_application() - response = test_application.get("/html_insertion_named_attachment_header", status=200) + response = test_application.get( + '/html_insertion_named_attachment_header', status=200) # The 'NREUM HEADER' value comes from our override for the header. # The 'NREUM.info' value comes from the programmatically generated - # header added by the agent. + # footer added by the agent. - response.mustcontain(no=["NREUM HEADER", "NREUM.info"]) + response.mustcontain(no=['NREUM HEADER', 'NREUM.info']) _test_html_insertion_settings = { - "browser_monitoring.enabled": True, - "browser_monitoring.auto_instrument": False, - "js_agent_loader": "", + 'browser_monitoring.enabled': True, + 'browser_monitoring.auto_instrument': False, + 'js_agent_loader': u'', } @override_application_settings(_test_html_insertion_settings) def test_html_insertion_manual_tag_instrumentation(): test_application = target_application() - response = test_application.get("/template_tags") + response = test_application.get('/template_tags') # Assert that the instrumentation is not inappropriately escaped - response.mustcontain("", no=["<!-- NREUM HEADER -->"]) + response.mustcontain('', + no=['<!-- NREUM HEADER -->']) _test_application_inclusion_tag_scoped_metrics = [ - ("Function/django.core.handlers.wsgi:WSGIHandler.__call__", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/views:inclusion_tag", 1), - ("Template/Render/main.html", 1), + ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/views:inclusion_tag', 1), + ('Template/Render/main.html', 1), ] if DJANGO_VERSION < (1, 9): - _test_application_inclusion_tag_scoped_metrics.extend([("Template/Include/results.html", 1)]) + _test_application_inclusion_tag_scoped_metrics.extend([ + ('Template/Include/results.html', 1)]) if DJANGO_VERSION < (1, 10): - _test_application_inclusion_tag_scoped_metrics.extend(_test_django_pre_1_10_url_resolver_scoped_metrics) + _test_application_inclusion_tag_scoped_metrics.extend( + _test_django_pre_1_10_url_resolver_scoped_metrics) elif DJANGO_VERSION >= (2, 0): - _test_application_inclusion_tag_scoped_metrics.extend(_test_django_post_2_0_url_resolver_scoped_metrics) + _test_application_inclusion_tag_scoped_metrics.extend( + _test_django_post_2_0_url_resolver_scoped_metrics) else: - _test_application_inclusion_tag_scoped_metrics.extend(_test_django_post_1_10_url_resolver_scoped_metrics) - -if DJANGO_SETTINGS_MODULE == "settings_0110_old": - _test_application_inclusion_tag_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) -elif DJANGO_SETTINGS_MODULE == "settings_0110_new": - _test_application_inclusion_tag_scoped_metrics.extend(_test_django_post_1_10_middleware_scoped_metrics) + _test_application_inclusion_tag_scoped_metrics.extend( + _test_django_post_1_10_url_resolver_scoped_metrics) + +if DJANGO_SETTINGS_MODULE == 'settings_0110_old': + _test_application_inclusion_tag_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) +elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': + _test_application_inclusion_tag_scoped_metrics.extend( + _test_django_post_1_10_middleware_scoped_metrics) elif DJANGO_VERSION < (1, 10): - _test_application_inclusion_tag_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) + _test_application_inclusion_tag_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) try: _test_application_inclusion_tag_scoped_metrics.remove( - (("Function/newrelic.hooks.framework_django:" "browser_timing_insertion"), 1) + (('Function/newrelic.hooks.framework_django:' + 'browser_timing_insertion'), 1) ) except ValueError: pass @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("views:inclusion_tag", scoped_metrics=_test_application_inclusion_tag_scoped_metrics) +@validate_transaction_metrics('views:inclusion_tag', + scoped_metrics=_test_application_inclusion_tag_scoped_metrics) @validate_code_level_metrics("views", "inclusion_tag") def test_application_inclusion_tag(): test_application = target_application() - response = test_application.get("/inclusion_tag") - response.mustcontain("Inclusion tag") + response = test_application.get('/inclusion_tag') + response.mustcontain('Inclusion tag') _test_inclusion_tag_template_tags_scoped_metrics = [ - ("Function/django.core.handlers.wsgi:WSGIHandler.__call__", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/views:inclusion_tag", 1), - ("Template/Render/main.html", 1), + ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/views:inclusion_tag', 1), + ('Template/Render/main.html', 1), ] if DJANGO_VERSION < (1, 9): - _test_inclusion_tag_template_tags_scoped_metrics.extend( - [("Template/Include/results.html", 1), ("Template/Tag/show_results", 1)] - ) + _test_inclusion_tag_template_tags_scoped_metrics.extend([ + ('Template/Include/results.html', 1), + ('Template/Tag/show_results', 1)]) -_test_inclusion_tag_settings = {"instrumentation.templates.inclusion_tag": "*"} +_test_inclusion_tag_settings = { + 'instrumentation.templates.inclusion_tag': '*' +} if DJANGO_VERSION < (1, 10): - _test_inclusion_tag_template_tags_scoped_metrics.extend(_test_django_pre_1_10_url_resolver_scoped_metrics) + _test_inclusion_tag_template_tags_scoped_metrics.extend( + _test_django_pre_1_10_url_resolver_scoped_metrics) elif DJANGO_VERSION >= (2, 0): - _test_inclusion_tag_template_tags_scoped_metrics.extend(_test_django_post_2_0_url_resolver_scoped_metrics) + _test_inclusion_tag_template_tags_scoped_metrics.extend( + _test_django_post_2_0_url_resolver_scoped_metrics) else: - _test_inclusion_tag_template_tags_scoped_metrics.extend(_test_django_post_1_10_url_resolver_scoped_metrics) + _test_inclusion_tag_template_tags_scoped_metrics.extend( + _test_django_post_1_10_url_resolver_scoped_metrics) -if DJANGO_SETTINGS_MODULE == "settings_0110_old": - _test_inclusion_tag_template_tags_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) -elif DJANGO_SETTINGS_MODULE == "settings_0110_new": - _test_inclusion_tag_template_tags_scoped_metrics.extend(_test_django_post_1_10_middleware_scoped_metrics) +if DJANGO_SETTINGS_MODULE == 'settings_0110_old': + _test_inclusion_tag_template_tags_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) +elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': + _test_inclusion_tag_template_tags_scoped_metrics.extend( + _test_django_post_1_10_middleware_scoped_metrics) elif DJANGO_VERSION < (1, 10): - _test_inclusion_tag_template_tags_scoped_metrics.extend(_test_django_pre_1_10_middleware_scoped_metrics) + _test_inclusion_tag_template_tags_scoped_metrics.extend( + _test_django_pre_1_10_middleware_scoped_metrics) try: _test_inclusion_tag_template_tags_scoped_metrics.remove( - (("Function/newrelic.hooks.framework_django:" "browser_timing_insertion"), 1) + (('Function/newrelic.hooks.framework_django:' + 'browser_timing_insertion'), 1) ) except ValueError: pass @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("views:inclusion_tag", scoped_metrics=_test_inclusion_tag_template_tags_scoped_metrics) +@validate_transaction_metrics('views:inclusion_tag', + scoped_metrics=_test_inclusion_tag_template_tags_scoped_metrics) @override_generic_settings(django_settings, _test_inclusion_tag_settings) @validate_code_level_metrics("views", "inclusion_tag") def test_inclusion_tag_template_tag_metric(): test_application = target_application() - response = test_application.get("/inclusion_tag") - response.mustcontain("Inclusion tag") + response = test_application.get('/inclusion_tag') + response.mustcontain('Inclusion tag') _test_template_render_exception_scoped_metrics_base = [ - ("Function/django.core.handlers.wsgi:WSGIHandler.__call__", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), + ('Function/django.core.handlers.wsgi:WSGIHandler.__call__', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), ] if DJANGO_VERSION < (1, 5): _test_template_render_exception_scoped_metrics_base.append( - ("Function/django.http:HttpResponseServerError.close", 1) - ) + ('Function/django.http:HttpResponseServerError.close', 1)) elif DJANGO_VERSION < (1, 8): _test_template_render_exception_scoped_metrics_base.append( - ("Function/django.http.response:HttpResponseServerError.close", 1) - ) + ('Function/django.http.response:HttpResponseServerError.close', 1)) else: - _test_template_render_exception_scoped_metrics_base.append(("Function/django.http.response:HttpResponse.close", 1)) + _test_template_render_exception_scoped_metrics_base.append( + ('Function/django.http.response:HttpResponse.close', 1)) if DJANGO_VERSION < (1, 10): - _test_template_render_exception_scoped_metrics_base.extend(_test_django_pre_1_10_url_resolver_scoped_metrics) + _test_template_render_exception_scoped_metrics_base.extend( + _test_django_pre_1_10_url_resolver_scoped_metrics) elif DJANGO_VERSION >= (2, 0): - _test_template_render_exception_scoped_metrics_base.extend(_test_django_post_2_0_url_resolver_scoped_metrics) + _test_template_render_exception_scoped_metrics_base.extend( + _test_django_post_2_0_url_resolver_scoped_metrics) else: - _test_template_render_exception_scoped_metrics_base.extend(_test_django_post_1_10_url_resolver_scoped_metrics) - -if DJANGO_SETTINGS_MODULE == "settings_0110_old": - _test_template_render_exception_scoped_metrics_base.extend(_test_django_pre_1_10_middleware_scoped_metrics) -elif DJANGO_SETTINGS_MODULE == "settings_0110_new": - _test_template_render_exception_scoped_metrics_base.extend(_test_django_post_1_10_middleware_scoped_metrics) + _test_template_render_exception_scoped_metrics_base.extend( + _test_django_post_1_10_url_resolver_scoped_metrics) + +if DJANGO_SETTINGS_MODULE == 'settings_0110_old': + _test_template_render_exception_scoped_metrics_base.extend( + _test_django_pre_1_10_middleware_scoped_metrics) +elif DJANGO_SETTINGS_MODULE == 'settings_0110_new': + _test_template_render_exception_scoped_metrics_base.extend( + _test_django_post_1_10_middleware_scoped_metrics) elif DJANGO_VERSION < (1, 10): - _test_template_render_exception_scoped_metrics_base.extend(_test_django_pre_1_10_middleware_scoped_metrics) + _test_template_render_exception_scoped_metrics_base.extend( + _test_django_pre_1_10_middleware_scoped_metrics) if DJANGO_VERSION < (1, 9): - _test_template_render_exception_errors = ["django.template.base:TemplateSyntaxError"] + _test_template_render_exception_errors = [ + 'django.template.base:TemplateSyntaxError'] else: - _test_template_render_exception_errors = ["django.template.exceptions:TemplateSyntaxError"] + _test_template_render_exception_errors = [ + 'django.template.exceptions:TemplateSyntaxError'] -_test_template_render_exception_function_scoped_metrics = list(_test_template_render_exception_scoped_metrics_base) -_test_template_render_exception_function_scoped_metrics.extend( - [ - ("Function/views:render_exception_function", 1), - ] -) +_test_template_render_exception_function_scoped_metrics = list( + _test_template_render_exception_scoped_metrics_base) +_test_template_render_exception_function_scoped_metrics.extend([ + ('Function/views:render_exception_function', 1), +]) @validate_transaction_errors(errors=_test_template_render_exception_errors) -@validate_transaction_metrics( - "views:render_exception_function", scoped_metrics=_test_template_render_exception_function_scoped_metrics -) +@validate_transaction_metrics('views:render_exception_function', + scoped_metrics=_test_template_render_exception_function_scoped_metrics) @validate_code_level_metrics("views", "render_exception_function") def test_template_render_exception_function(): test_application = target_application() - test_application.get("/render_exception_function", status=500) + test_application.get('/render_exception_function', status=500) -_test_template_render_exception_class_scoped_metrics = list(_test_template_render_exception_scoped_metrics_base) -_test_template_render_exception_class_scoped_metrics.extend( - [ - ("Function/views:RenderExceptionClass", 1), - ("Function/views:RenderExceptionClass.get", 1), - ] -) +_test_template_render_exception_class_scoped_metrics = list( + _test_template_render_exception_scoped_metrics_base) +_test_template_render_exception_class_scoped_metrics.extend([ + ('Function/views:RenderExceptionClass', 1), + ('Function/views:RenderExceptionClass.get', 1), +]) @validate_transaction_errors(errors=_test_template_render_exception_errors) -@validate_transaction_metrics( - "views:RenderExceptionClass.get", scoped_metrics=_test_template_render_exception_class_scoped_metrics -) +@validate_transaction_metrics('views:RenderExceptionClass.get', + scoped_metrics=_test_template_render_exception_class_scoped_metrics) @validate_code_level_metrics("views.RenderExceptionClass", "get") def test_template_render_exception_class(): test_application = target_application() - test_application.get("/render_exception_class", status=500) + test_application.get('/render_exception_class', status=500) diff --git a/tests/framework_django/views.py b/tests/framework_django/views.py index e97e273ded..c5ce1526c7 100644 --- a/tests/framework_django/views.py +++ b/tests/framework_django/views.py @@ -12,21 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.core.exceptions import PermissionDenied from django.http import HttpResponse +from django.views.generic.base import View, TemplateView from django.shortcuts import render -from django.views.generic.base import TemplateView, View +from django.core.exceptions import PermissionDenied from middleware import Custom410 -from newrelic.api.transaction import get_browser_timing_header +from newrelic.api.transaction import (get_browser_timing_header, + get_browser_timing_footer) def index(request): - return HttpResponse("INDEX RESPONSE") + return HttpResponse('INDEX RESPONSE') def exception(request): - raise RuntimeError("exception") + raise RuntimeError('exception') def permission_denied(request): @@ -39,7 +40,7 @@ def middleware_410(request): class MyView(View): def get(self, request): - return HttpResponse("CBV RESPONSE") + return HttpResponse('CBV RESPONSE') def deferred_cbv(request): @@ -47,77 +48,69 @@ def deferred_cbv(request): def html_insertion(request): - return HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) + return HttpResponse('Some header' + '

My First Heading

My first paragraph.

' + '') def html_insertion_content_length(request): - content = ( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) + content = ('Some header' + '

My First Heading

My first paragraph.

' + '') response = HttpResponse(content) - response["Content-Length"] = len(content) + response['Content-Length'] = len(content) return response def html_insertion_manual(request): header = get_browser_timing_header() + footer = get_browser_timing_footer() + header = get_browser_timing_header() + footer = get_browser_timing_footer() - assert header == "" + assert header == '' + assert footer == '' - return HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) + return HttpResponse('Some header' + '

My First Heading

My first paragraph.

' + '') def html_insertion_unnamed_attachment_header(request): - response = HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) - response["Content-Disposition"] = "attachment" + response = HttpResponse('Some header' + '

My First Heading

My first paragraph.

' + '') + response['Content-Disposition'] = 'attachment' return response def html_insertion_named_attachment_header(request): - response = HttpResponse( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) - response["Content-Disposition"] = 'Attachment; filename="X"' + response = HttpResponse('Some header' + '

My First Heading

My first paragraph.

' + '') + response['Content-Disposition'] = 'Attachment; filename="X"' return response def inclusion_tag(request): - return render(request, "main.html", {}, content_type="text/html") + return render(request, 'main.html', {}, content_type="text/html") def template_tags(request): - return render(request, "main.html", {}, content_type="text/html") + return render(request, 'main.html', {}, content_type="text/html") def render_exception_function(request): - return render(request, "render_exception.html") + return render(request, 'render_exception.html') class RenderExceptionClass(TemplateView): - template_name = "render_exception.html" + template_name = 'render_exception.html' def gzip_html_insertion(request): # contents must be at least 200 bytes for gzip middleware to work - contents = "*" * 200 - return HttpResponse( - "Some header" - "

My First Heading

%s

" % contents - ) + contents = '*' * 200 + return HttpResponse('Some header' + '

My First Heading

%s

' % contents) diff --git a/tests/framework_flask/_test_compress.py b/tests/framework_flask/_test_compress.py index 1fbf207689..f3c9fbf2be 100644 --- a/tests/framework_flask/_test_compress.py +++ b/tests/framework_flask/_test_compress.py @@ -18,10 +18,14 @@ import StringIO as IO import webtest -from flask import Flask, Response, send_file + +from flask import Flask +from flask import Response +from flask import send_file from flask_compress import Compress -from newrelic.api.transaction import get_browser_timing_header +from newrelic.api.transaction import (get_browser_timing_header, + get_browser_timing_footer) application = Flask(__name__) @@ -29,57 +33,57 @@ compress.init_app(application) -@application.route("/compress") +@application.route('/compress') def index_page(): - return "" + 500 * "X" + "" + return '' + 500 * 'X' + '' -@application.route("/html_insertion") +@application.route('/html_insertion') def html_insertion(): - return ( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) + return ('Some header' + '

My First Heading

My first paragraph.

' + '') -@application.route("/html_insertion_manual") +@application.route('/html_insertion_manual') def html_insertion_manual(): header = get_browser_timing_header() + footer = get_browser_timing_footer() + header = get_browser_timing_header() + footer = get_browser_timing_footer() - assert header == "" + assert header == '' + assert footer == '' - return ( - "Some header" - "

My First Heading

My first paragraph.

" - "" - ) + return ('Some header' + '

My First Heading

My first paragraph.

' + '') -@application.route("/html_insertion_unnamed_attachment_header") +@application.route('/html_insertion_unnamed_attachment_header') def html_insertion_unnamed_attachment_header(): response = Response( - response="Some header" - "

My First Heading

My first paragraph.

" - "" - ) - response.headers.add("Content-Disposition", "attachment") + response='Some header' + '

My First Heading

My first paragraph.

' + '') + response.headers.add('Content-Disposition', + 'attachment') return response -@application.route("/html_insertion_named_attachment_header") +@application.route('/html_insertion_named_attachment_header') def html_insertion_named_attachment_header(): response = Response( - response="Some header" - "

My First Heading

My first paragraph.

" - "" - ) - response.headers.add("Content-Disposition", 'attachment; filename="X"') + response='Some header' + '

My First Heading

My first paragraph.

' + '') + response.headers.add('Content-Disposition', + 'attachment; filename="X"') return response -@application.route("/html_served_from_file") +@application.route('/html_served_from_file') def html_served_from_file(): file = IO() contents = b""" @@ -89,10 +93,10 @@ def html_served_from_file(): """ file.write(contents) file.seek(0) - return send_file(file, mimetype="text/html") + return send_file(file, mimetype='text/html') -@application.route("/text_served_from_file") +@application.route('/text_served_from_file') def text_served_from_file(): file = IO() contents = b""" @@ -102,19 +106,17 @@ def text_served_from_file(): """ file.write(contents) file.seek(0) - return send_file(file, mimetype="text/plain") + return send_file(file, mimetype='text/plain') _test_application = webtest.TestApp(application) -@application.route("/empty_content_type") +@application.route('/empty_content_type') def empty_content_type(): response = Response( - response="Some header" - "

My First Heading

My first paragraph.

" - "", - mimetype="", - ) + response='Some header' + '

My First Heading

My first paragraph.

' + '', mimetype='') assert response.mimetype is None return response diff --git a/tests/framework_flask/test_application.py b/tests/framework_flask/test_application.py index 508fb68934..de7a430191 100644 --- a/tests/framework_flask/test_application.py +++ b/tests/framework_flask/test_application.py @@ -13,29 +13,23 @@ # limitations under the License. import pytest -from conftest import async_handler_support, skip_if_not_async_handler_support + from testing_support.fixtures import ( override_application_settings, - validate_tt_parenting, -) -from testing_support.validators.validate_code_level_metrics import ( - validate_code_level_metrics, -) -from testing_support.validators.validate_transaction_errors import ( - validate_transaction_errors, -) -from testing_support.validators.validate_transaction_metrics import ( - validate_transaction_metrics, -) + validate_tt_parenting) +from testing_support.validators.validate_code_level_metrics import validate_code_level_metrics +from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics +from testing_support.validators.validate_transaction_errors import validate_transaction_errors from newrelic.packages import six +from conftest import async_handler_support, skip_if_not_async_handler_support + try: # The __version__ attribute was only added in 0.7.0. # Flask team does not use semantic versioning during development. from flask import __version__ as flask_version - - flask_version = tuple([int(v) for v in flask_version.split(".")]) + flask_version = tuple([int(v) for v in flask_version.split('.')]) is_gt_flask060 = True is_dev_version = False except ValueError: @@ -45,7 +39,8 @@ is_gt_flask060 = False is_dev_version = False -requires_endpoint_decorator = pytest.mark.skipif(not is_gt_flask060, reason="The endpoint decorator is not supported.") +requires_endpoint_decorator = pytest.mark.skipif(not is_gt_flask060, + reason="The endpoint decorator is not supported.") def target_application(): @@ -66,254 +61,226 @@ def target_application(): _test_application_index_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_test_application:index_page", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), -] + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_application:index_page', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] _test_application_index_tt_parenting = ( - "TransactionNode", - [ - ( - "FunctionNode", - [ - ( - "FunctionNode", - [ - ("FunctionNode", []), - ("FunctionNode", []), - ("FunctionNode", []), - # some flask versions have more FunctionNodes here, as appended - # below - ], - ), - ], - ), - ("FunctionNode", []), - ( - "FunctionNode", - [ - ("FunctionNode", []), - ], - ), - ], + 'TransactionNode', [ + ('FunctionNode', [ + ('FunctionNode', [ + ('FunctionNode', []), + ('FunctionNode', []), + ('FunctionNode', []), + # some flask versions have more FunctionNodes here, as appended + # below + ]), + ]), + ('FunctionNode', []), + ('FunctionNode', [ + ('FunctionNode', []), + ]), + ] ) if is_dev_version or (is_gt_flask060 and flask_version >= (0, 7)): _test_application_index_tt_parenting[1][0][1][0][1].append( - ("FunctionNode", []), + ('FunctionNode', []), ) if is_dev_version or (is_gt_flask060 and flask_version >= (0, 9)): _test_application_index_tt_parenting[1][0][1][0][1].append( - ("FunctionNode", []), + ('FunctionNode', []), ) - @validate_transaction_errors(errors=[]) -@validate_transaction_metrics("_test_application:index_page", scoped_metrics=_test_application_index_scoped_metrics) +@validate_transaction_metrics('_test_application:index_page', + scoped_metrics=_test_application_index_scoped_metrics) @validate_tt_parenting(_test_application_index_tt_parenting) @validate_code_level_metrics("_test_application", "index_page") def test_application_index(): application = target_application() - response = application.get("/index") - response.mustcontain("INDEX RESPONSE") - + response = application.get('/index') + response.mustcontain('INDEX RESPONSE') _test_application_async_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_test_application_async:async_page", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), -] - + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_application_async:async_page', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] @skip_if_not_async_handler_support @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_test_application_async:async_page", scoped_metrics=_test_application_async_scoped_metrics -) +@validate_transaction_metrics('_test_application_async:async_page', + scoped_metrics=_test_application_async_scoped_metrics) @validate_tt_parenting(_test_application_index_tt_parenting) @validate_code_level_metrics("_test_application_async", "async_page") def test_application_async(): application = target_application() - response = application.get("/async") - response.mustcontain("ASYNC RESPONSE") - + response = application.get('/async') + response.mustcontain('ASYNC RESPONSE') _test_application_endpoint_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_test_application:endpoint_page", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), -] + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_application:endpoint_page', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_test_application:endpoint_page", scoped_metrics=_test_application_endpoint_scoped_metrics -) +@validate_transaction_metrics('_test_application:endpoint_page', + scoped_metrics=_test_application_endpoint_scoped_metrics) @validate_code_level_metrics("_test_application", "endpoint_page") def test_application_endpoint(): application = target_application() - response = application.get("/endpoint") - response.mustcontain("ENDPOINT RESPONSE") + response = application.get('/endpoint') + response.mustcontain('ENDPOINT RESPONSE') _test_application_error_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_test_application:error_page", 1), - ("Function/flask.app:Flask.handle_exception", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), - ("Function/flask.app:Flask.handle_user_exception", 1), - ("Function/flask.app:Flask.handle_user_exception", 1), -] + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_application:error_page', 1), + ('Function/flask.app:Flask.handle_exception', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] if six.PY3: - _test_application_error_errors = ["builtins:RuntimeError"] + _test_application_error_errors = ['builtins:RuntimeError'] else: - _test_application_error_errors = ["exceptions:RuntimeError"] + _test_application_error_errors = ['exceptions:RuntimeError'] @validate_transaction_errors(errors=_test_application_error_errors) -@validate_transaction_metrics("_test_application:error_page", scoped_metrics=_test_application_error_scoped_metrics) +@validate_transaction_metrics('_test_application:error_page', + scoped_metrics=_test_application_error_scoped_metrics) @validate_code_level_metrics("_test_application", "error_page") def test_application_error(): application = target_application() - application.get("/error", status=500, expect_errors=True) + application.get('/error', status=500, expect_errors=True) _test_application_abort_404_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_test_application:abort_404_page", 1), - ("Function/flask.app:Flask.handle_http_exception", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), - ("Function/flask.app:Flask.handle_user_exception", 1), -] + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_application:abort_404_page', 1), + ('Function/flask.app:Flask.handle_http_exception', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_test_application:abort_404_page", scoped_metrics=_test_application_abort_404_scoped_metrics -) +@validate_transaction_metrics('_test_application:abort_404_page', + scoped_metrics=_test_application_abort_404_scoped_metrics) @validate_code_level_metrics("_test_application", "abort_404_page") def test_application_abort_404(): application = target_application() - application.get("/abort_404", status=404) + application.get('/abort_404', status=404) _test_application_exception_404_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_test_application:exception_404_page", 1), - ("Function/flask.app:Flask.handle_http_exception", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), - ("Function/flask.app:Flask.handle_user_exception", 1), -] + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_application:exception_404_page', 1), + ('Function/flask.app:Flask.handle_http_exception', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "_test_application:exception_404_page", scoped_metrics=_test_application_exception_404_scoped_metrics -) +@validate_transaction_metrics('_test_application:exception_404_page', + scoped_metrics=_test_application_exception_404_scoped_metrics) @validate_code_level_metrics("_test_application", "exception_404_page") def test_application_exception_404(): application = target_application() - application.get("/exception_404", status=404) + application.get('/exception_404', status=404) _test_application_not_found_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/flask.app:Flask.handle_http_exception", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), - ("Function/flask.app:Flask.handle_user_exception", 1), -] + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/flask.app:Flask.handle_http_exception', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] @validate_transaction_errors(errors=[]) -@validate_transaction_metrics( - "flask.app:Flask.handle_http_exception", scoped_metrics=_test_application_not_found_scoped_metrics -) +@validate_transaction_metrics('flask.app:Flask.handle_http_exception', + scoped_metrics=_test_application_not_found_scoped_metrics) def test_application_not_found(): application = target_application() - application.get("/missing", status=404) + application.get('/missing', status=404) _test_application_render_template_string_scoped_metrics = [ - ("Function/flask.app:Flask.wsgi_app", 1), - ("Python/WSGI/Application", 1), - ("Python/WSGI/Response", 1), - ("Python/WSGI/Finalize", 1), - ("Function/_test_application:template_string", 1), - ("Function/werkzeug.wsgi:ClosingIterator.close", 1), - ("Template/Compile/