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 = "
%sRESPONSE
"
+ text = "%sRESPONSE
%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 = "%sRESPONSE
"
+ text = "%sRESPONSE
%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