diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54c7186..81ea265 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,8 @@
   ([PR #348](https://github.com/scoutapp/scout_apm_python/pull/348)).
 - Increase core agent socket timeout to reduce reconnections
   ([PR #247](https://github.com/scoutapp/scout_apm_python/pull/247)).
+- Fix the `RemovedInDjango20Warning` for `django.core.urlresolvers` on Django
+  1.11 ([PR #359](https://github.com/scoutapp/scout_apm_python/pull/359)).
 
 ## [2.7.0] 2019-11-03
 
diff --git a/src/scout_apm/async_/starlette.py b/src/scout_apm/async_/starlette.py
index 0376651..5d1d36b 100644
--- a/src/scout_apm/async_/starlette.py
+++ b/src/scout_apm/async_/starlette.py
@@ -55,7 +55,7 @@ class ScoutMiddleware:
             amazon_queue_time = request.headers.get("x-amzn-trace-id", default="")
             track_amazon_request_queue_time(amazon_queue_time, tracked_request)
 
-        def rename_controller_span_from_endpoint():
+        def grab_extra_data():
             if "endpoint" in scope:
                 # Rename top span
                 endpoint = scope["endpoint"]
@@ -64,13 +64,23 @@ class ScoutMiddleware:
                 )
                 tracked_request.is_real_request = True
 
+            # From AuthenticationMiddleware - bypass request.user because it
+            # throws AssertionError if 'user' is not in Scope, and we need a
+            # try/except already
+            try:
+                username = scope["user"].display_name
+            except (KeyError, AttributeError):
+                pass
+            else:
+                tracked_request.tag("username", username)
+
         async def wrapped_send(data):
             # Finish HTTP span when body finishes sending, not later (e.g.
             # after background tasks)
             if data.get("type", None) == "http.response.body" and not data.get(
                 "more_body", False
             ):
-                rename_controller_span_from_endpoint()
+                grab_extra_data()
                 tracked_request.stop_span()
             return await send(data)
 
@@ -81,7 +91,7 @@ class ScoutMiddleware:
             raise exc
         finally:
             if tracked_request.end_time is None:
-                rename_controller_span_from_endpoint()
+                grab_extra_data()
                 tracked_request.stop_span()
 
 
diff --git a/src/scout_apm/bottle.py b/src/scout_apm/bottle.py
index 746f7bf..ca16689 100644
--- a/src/scout_apm/bottle.py
+++ b/src/scout_apm/bottle.py
@@ -1,6 +1,7 @@
 # coding=utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+import wrapt
 from bottle import request
 
 import scout_apm.core
@@ -34,57 +35,55 @@ class ScoutPlugin(object):
     def apply(self, callback, context):
         if not scout_config.value("monitor"):
             return callback
+        return wrap_callback(callback)
 
-        def wrapper(*args, **kwargs):
-            tracked_request = TrackedRequest.instance()
-            tracked_request.is_real_request = True
 
-            path = request.path
-            # allitems() is an undocumented bottle internal
-            tracked_request.tag(
-                "path", create_filtered_path(path, request.query.allitems())
-            )
-            if ignore_path(path):
-                tracked_request.tag("ignore_transaction", True)
+@wrapt.decorator
+def wrap_callback(wrapped, instance, args, kwargs):
+    tracked_request = TrackedRequest.instance()
+    tracked_request.is_real_request = True
 
-            if request.route.name is not None:
-                controller_name = request.route.name
-            else:
-                controller_name = request.route.rule
-            if controller_name == "/":
-                controller_name = "/home"
-            if not controller_name.startswith("/"):
-                controller_name = "/" + controller_name
-            tracked_request.start_span(
-                operation="Controller{}".format(controller_name),
-                should_capture_backtrace=False,
-            )
+    path = request.path
+    # allitems() is an undocumented bottle internal
+    tracked_request.tag("path", create_filtered_path(path, request.query.allitems()))
+    if ignore_path(path):
+        tracked_request.tag("ignore_transaction", True)
 
-            # Determine a remote IP to associate with the request. The
-            # value is spoofable by the requester so this is not suitable
-            # to use in any security sensitive context.
-            user_ip = (
-                request.headers.get("x-forwarded-for", "").split(",")[0]
-                or request.headers.get("client-ip", "").split(",")[0]
-                or request.environ.get("REMOTE_ADDR")
-            )
-            tracked_request.tag("user_ip", user_ip)
+    if request.route.name is not None:
+        controller_name = request.route.name
+    else:
+        controller_name = request.route.rule
+    if controller_name == "/":
+        controller_name = "/home"
+    if not controller_name.startswith("/"):
+        controller_name = "/" + controller_name
+    tracked_request.start_span(
+        operation="Controller{}".format(controller_name), should_capture_backtrace=False
+    )
 
-            tracked_queue_time = False
-            queue_time = request.headers.get(
-                "x-queue-start", ""
-            ) or request.headers.get("x-request-start", "")
-            tracked_queue_time = track_request_queue_time(queue_time, tracked_request)
-            if not tracked_queue_time:
-                amazon_queue_time = request.headers.get("x-amzn-trace-id", "")
-                track_amazon_request_queue_time(amazon_queue_time, tracked_request)
+    # Determine a remote IP to associate with the request. The
+    # value is spoofable by the requester so this is not suitable
+    # to use in any security sensitive context.
+    user_ip = (
+        request.headers.get("x-forwarded-for", "").split(",")[0]
+        or request.headers.get("client-ip", "").split(",")[0]
+        or request.environ.get("REMOTE_ADDR")
+    )
+    tracked_request.tag("user_ip", user_ip)
 
-            try:
-                return callback(*args, **kwargs)
-            except Exception:
-                tracked_request.tag("error", "true")
-                raise
-            finally:
-                tracked_request.stop_span()
+    tracked_queue_time = False
+    queue_time = request.headers.get("x-queue-start", "") or request.headers.get(
+        "x-request-start", ""
+    )
+    tracked_queue_time = track_request_queue_time(queue_time, tracked_request)
+    if not tracked_queue_time:
+        amazon_queue_time = request.headers.get("x-amzn-trace-id", "")
+        track_amazon_request_queue_time(amazon_queue_time, tracked_request)
 
-        return wrapper
+    try:
+        return wrapped(*args, **kwargs)
+    except Exception:
+        tracked_request.tag("error", "true")
+        raise
+    finally:
+        tracked_request.stop_span()
diff --git a/src/scout_apm/core/tracked_request.py b/src/scout_apm/core/tracked_request.py
index 9b8bc1d..df3ed52 100644
--- a/src/scout_apm/core/tracked_request.py
+++ b/src/scout_apm/core/tracked_request.py
@@ -67,21 +67,30 @@ class TrackedRequest(object):
             )
         self.tags[key] = value
 
-    def start_span(self, *args, **kwargs):
-        maybe_parent = self.current_span()
-
-        if maybe_parent is not None:
-            parent_id = maybe_parent.span_id
-            if maybe_parent.ignore_children:
-                kwargs["ignore"] = True
-                kwargs["ignore_children"] = True
+    def start_span(
+        self,
+        operation,
+        ignore=False,
+        ignore_children=False,
+        should_capture_backtrace=True,
+    ):
+        parent = self.current_span()
+        if parent is not None:
+            parent_id = parent.span_id
+            if parent.ignore_children:
+                ignore = True
+                ignore_children = True
         else:
             parent_id = None
 
-        kwargs["parent"] = parent_id
-        kwargs["request_id"] = self.request_id
-
-        new_span = Span(**kwargs)
+        new_span = Span(
+            request_id=self.request_id,
+            operation=operation,
+            ignore=ignore,
+            ignore_children=ignore_children,
+            parent=parent_id,
+            should_capture_backtrace=should_capture_backtrace,
+        )
         self.active_spans.append(new_span)
         return new_span
 
@@ -100,7 +109,7 @@ class TrackedRequest(object):
             self.finish()
 
     def current_span(self):
-        if len(self.active_spans) > 0:
+        if self.active_spans:
             return self.active_spans[-1]
         else:
             return None
diff --git a/src/scout_apm/django/middleware.py b/src/scout_apm/django/middleware.py
index d5e205e..e10ef30 100644
--- a/src/scout_apm/django/middleware.py
+++ b/src/scout_apm/django/middleware.py
@@ -16,7 +16,7 @@ from scout_apm.core.web_requests import (
     track_request_queue_time,
 )
 
-if django.VERSION >= (2, 0):
+if django.VERSION >= (1, 11):
     from django.urls import get_urlconf
 else:
     from django.core.urlresolvers import get_urlconf
diff --git a/src/scout_apm/instruments/elasticsearch.py b/src/scout_apm/instruments/elasticsearch.py
index b255134..4450e04 100644
--- a/src/scout_apm/instruments/elasticsearch.py
+++ b/src/scout_apm/instruments/elasticsearch.py
@@ -70,11 +70,11 @@ class Instrument(object):
             logger.info("Elasticsearch instruments are not installed. Skipping.")
             return False
 
-        self.__class__.installed = False
-
         self.uninstrument_client()
         self.uninstrument_transport()
 
+        self.__class__.installed = False
+
     def instrument_client(self):
         for name in self.__class__.CLIENT_METHODS:
             method = getattr(Elasticsearch, name, None)
diff --git a/src/scout_apm/instruments/pymongo.py b/src/scout_apm/instruments/pymongo.py
index 0119895..2e53c5d 100644
--- a/src/scout_apm/instruments/pymongo.py
+++ b/src/scout_apm/instruments/pymongo.py
@@ -3,10 +3,14 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import logging
 
-# Used in the exec() call below.
-import wrapt  # noqa: F401
+import wrapt
 
-from scout_apm.core.tracked_request import TrackedRequest  # noqa: F401
+from scout_apm.core.tracked_request import TrackedRequest
+
+try:
+    from pymongo.collection import Collection
+except ImportError:
+    Collection = None
 
 logger = logging.getLogger(__name__)
 
@@ -49,9 +53,7 @@ class Instrument(object):
     ]
 
     def installable(self):
-        try:
-            from pymongo.collection import Collection  # noqa: F401
-        except ImportError:
+        if Collection is None:
             logger.info("Unable to import for PyMongo instruments")
             return False
         if self.installed:
@@ -66,44 +68,10 @@ class Instrument(object):
 
         self.__class__.installed = True
 
-        try:
-            from pymongo.collection import Collection  # noqa: F401
-        # There is no way the import can fail if self.installable() succeeded.
-        except ImportError:  # pragma: no cover
-            logger.info(
-                "Unable to import for PyMongo instruments. Instrument install failed."
-            )
-            return False
-
-        for method_str in self.__class__.PYMONGO_METHODS:
-            try:
-                code_str = """
-@wrapt.decorator
-def wrapped_{method_str}(wrapped, instance, args, kwargs):
-    tracked_request = TrackedRequest.instance()
-    name = '/'.join(['MongoDB', instance.name, '{camel_name}'])
-    span = tracked_request.start_span(operation=name, ignore_children=True)
-    span.tag('name', instance.name)
-
-    try:
-        return wrapped(*args, **kwargs)
-    finally:
-        tracked_request.stop_span()
-
-Collection.{method_str} = wrapped_{method_str}(Collection.{method_str})
-""".format(
-                    method_str=method_str,
-                    camel_name="".join(c.title() for c in method_str.split("_")),
-                )
-
-                exec(code_str)
-                logger.info("Instrumented PyMongo Collection.%s", method_str)
-
-            except Exception as e:
-                logger.warning(
-                    "Unable to instrument for PyMongo Collection.%s: %r", method_str, e
-                )
-                return False
+        for name in self.__class__.PYMONGO_METHODS:
+            method = getattr(Collection, name, None)
+            if method is not None:
+                setattr(Collection, name, wrap_collection_method(method))
         return True
 
     def uninstall(self):
@@ -111,9 +79,23 @@ Collection.{method_str} = wrapped_{method_str}(Collection.{method_str})
             logger.info("PyMongo instruments are not installed. Skipping.")
             return False
 
+        for name in self.__class__.PYMONGO_METHODS:
+            method = getattr(Collection, name, None)
+            if method is not None:
+                setattr(Collection, name, method.__wrapped__)
+
         self.__class__.installed = False
 
-        from pymongo.collection import Collection
 
-        for method_str in self.__class__.PYMONGO_METHODS:
-            setattr(Collection, method_str, getattr(Collection, method_str).__wrapped__)
+@wrapt.decorator
+def wrap_collection_method(wrapped, instance, args, kwargs):
+    tracked_request = TrackedRequest.instance()
+    camel_name = "".join(c.title() for c in wrapped.__name__.split("_"))
+    operation = "MongoDB/{}.{}".format(instance.name, camel_name)
+    span = tracked_request.start_span(operation=operation, ignore_children=True)
+    span.tag("name", instance.name)
+
+    try:
+        return wrapped(*args, **kwargs)
+    finally:
+        tracked_request.stop_span()

Instrument Starlette authentication
Starlette has a basic authentication framework: https://www.starlette.io/authentication/

We should probably copy what we do for Django here, tagging user ID's/ display names on traces.
diff --git a/tests/integration/django_app.py b/tests/integration/django_app.py
index bdc8e81..728fbf3 100644
--- a/tests/integration/django_app.py
+++ b/tests/integration/django_app.py
@@ -1,9 +1,8 @@
 # coding: utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-from functools import wraps
-
 import django
+import wrapt
 from django.conf import settings
 from django.template.response import TemplateResponse
 
@@ -135,14 +134,11 @@ def template(request):
     return HttpResponse(template.render(context))
 
 
-def exclaimify_template_response_name(func):
-    @wraps(func)
-    def wrapper(*args, **kwargs):
-        response = func(*args, **kwargs)
-        response.context_data["name"] = response.context_data["name"] + "!!"
-        return response
-
-    return wrapper
+@wrapt.decorator
+def exclaimify_template_response_name(wrapped, instance, args, kwargs):
+    response = wrapped(*args, **kwargs)
+    response.context_data["name"] = response.context_data["name"] + "!!"
+    return response
 
 
 @exclaimify_template_response_name
diff --git a/tests/integration/instruments/test_elasticsearch.py b/tests/integration/instruments/test_elasticsearch.py
index b22bfac..c2b9616 100644
--- a/tests/integration/instruments/test_elasticsearch.py
+++ b/tests/integration/instruments/test_elasticsearch.py
@@ -170,7 +170,10 @@ def test_instrument_client_install_missing_attribute():
         "scout_apm.instruments.elasticsearch.Elasticsearch"
     ) as mock_elasticsearch:
         del mock_elasticsearch.bulk
-    instrument.instrument_client()  # no crash
+    try:
+        instrument.instrument_client()  # no crash
+    finally:
+        instrument.uninstrument_client()
 
 
 @mock.patch(
diff --git a/tests/integration/instruments/test_pymongo.py b/tests/integration/instruments/test_pymongo.py
index 8204b8c..207dc90 100644
--- a/tests/integration/instruments/test_pymongo.py
+++ b/tests/integration/instruments/test_pymongo.py
@@ -9,11 +9,9 @@ import pytest
 
 from scout_apm.instruments.pymongo import Instrument
 from tests.compat import mock
-from tests.tools import pretend_package_unavailable
 
 # e.g. export MONGODB_URL="mongodb://localhost:27017/"
 MONGODB_URL = os.environ.get("MONGODB_URL")
-
 skip_if_mongodb_not_running = pytest.mark.skipif(
     MONGODB_URL is None, reason="MongoDB isn't available"
 )
@@ -57,22 +55,22 @@ def test_installable():
 
 
 def test_installable_no_pymongo_module():
-    with pretend_package_unavailable("pymongo"):
+    with mock.patch("scout_apm.instruments.pymongo.Collection", new=None):
         assert not instrument.installable()
 
 
 def test_install_no_pymongo_module():
-    with pretend_package_unavailable("pymongo"):
+    with mock.patch("scout_apm.instruments.pymongo.Collection", new=None):
         assert not instrument.install()
 
 
-@mock.patch("scout_apm.instruments.pymongo.wrapt.decorator", side_effect=RuntimeError)
-def test_install_failure(mock_decorator):
+def test_install_missing_attribute():
+    with mock.patch("scout_apm.instruments.pymongo.Collection") as mock_collection:
+        del mock_collection.aggregate
     try:
-        assert not instrument.install()  # doesn't crash
+        instrument.install()  # no crash
     finally:
-        # Currently installed = True even if installing failed.
-        Instrument.installed = False
+        instrument.uninstall()
 
 
 def test_install_is_idempotent():
diff --git a/tests/integration/test_starlette_py36plus.py b/tests/integration/test_starlette_py36plus.py
index f2fca51..9d3f8bc 100644
--- a/tests/integration/test_starlette_py36plus.py
+++ b/tests/integration/test_starlette_py36plus.py
@@ -8,8 +8,10 @@ from urllib.parse import urlencode
 import pytest
 from asgiref.testing import ApplicationCommunicator
 from starlette.applications import Starlette
+from starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUser
 from starlette.background import BackgroundTasks
 from starlette.endpoints import HTTPEndpoint
+from starlette.middleware.authentication import AuthenticationMiddleware
 from starlette.responses import PlainTextResponse
 
 from scout_apm.api import Config
@@ -24,7 +26,7 @@ from tests.tools import async_test
 
 
 @contextmanager
-def app_with_scout(*, scout_config=None):
+def app_with_scout(*, app=None, scout_config=None):
     """
     Context manager that configures and installs the Scout plugin for a basic
     Starlette application.
@@ -35,7 +37,8 @@ def app_with_scout(*, scout_config=None):
     scout_config["core_agent_launch"] = False
     scout_config.setdefault("monitor", True)
 
-    app = Starlette()
+    if app is None:
+        app = Starlette()
 
     @app.exception_handler(500)
     async def error(request, exc):
@@ -329,3 +332,49 @@ async def test_background_jobs(tracked_requests):
         "Job/tests.integration.test_starlette_py36plus."
         + "app_with_scout.<locals>.background_jobs.<locals>.async_noop"
     )
+
+
+@async_test
+async def test_username(tracked_requests):
+    base_app = Starlette()
+
+    class DummyBackend(AuthenticationBackend):
+        async def authenticate(self, request):
+            return AuthCredentials(), SimpleUser("dummy")
+
+    base_app.add_middleware(AuthenticationMiddleware, backend=DummyBackend())
+
+    with app_with_scout(app=base_app) as app:
+        communicator = ApplicationCommunicator(app, get_scope(path="/"))
+        await communicator.send_input({"type": "http.request"})
+        response_start = await communicator.receive_output()
+        await communicator.receive_output()
+
+    assert response_start["type"] == "http.response.start"
+    assert response_start["status"] == 200
+    assert len(tracked_requests) == 1
+    tracked_request = tracked_requests[0]
+    assert tracked_request.tags["username"] == "dummy"
+
+
+@async_test
+async def test_username_bad_user(tracked_requests):
+    base_app = Starlette()
+
+    class BadUserBackend(AuthenticationBackend):
+        async def authenticate(self, request):
+            return AuthCredentials(), object()
+
+    base_app.add_middleware(AuthenticationMiddleware, backend=BadUserBackend())
+
+    with app_with_scout(app=base_app) as app:
+        communicator = ApplicationCommunicator(app, get_scope(path="/"))
+        await communicator.send_input({"type": "http.request"})
+        response_start = await communicator.receive_output()
+        await communicator.receive_output()
+
+    assert response_start["type"] == "http.response.start"
+    assert response_start["status"] == 200
+    assert len(tracked_requests) == 1
+    tracked_request = tracked_requests[0]
+    assert "username" not in tracked_request.tags
diff --git a/tests/unit/core/test_commands.py b/tests/unit/core/test_commands.py
index 66dea34..f924058 100644
--- a/tests/unit/core/test_commands.py
+++ b/tests/unit/core/test_commands.py
@@ -257,7 +257,7 @@ def test_batch_command_from_tracked_request_with_tag():
 @skip_if_objtrace_not_extension
 def test_batch_command_from_tracked_request_with_span():
     tracked_request = TrackedRequest()
-    tracked_request.start_span()
+    tracked_request.start_span("myoperation")
     tracked_request.stop_span()
     make_tracked_request_instance_deterministic(tracked_request)
 
@@ -270,7 +270,7 @@ def test_batch_command_from_tracked_request_with_span():
     }
     assert message_commands[1] == {
         "StartSpan": {
-            "operation": None,
+            "operation": "myoperation",
             "parent_id": None,
             "request_id": REQUEST_ID,
             "span_id": SPAN_ID,
@@ -321,7 +321,7 @@ def test_batch_command_from_tracked_request_with_span():
 @skip_if_objtrace_not_extension
 def test_batch_command_from_tracked_request_with_span_no_objtrace():
     tracked_request = TrackedRequest()
-    tracked_request.start_span()
+    tracked_request.start_span(operation="myoperation")
     tracked_request.stop_span()
     make_tracked_request_instance_deterministic(tracked_request)
 
@@ -334,7 +334,7 @@ def test_batch_command_from_tracked_request_with_span_no_objtrace():
     }
     assert message_commands[1] == {
         "StartSpan": {
-            "operation": None,
+            "operation": "myoperation",
             "parent_id": None,
             "request_id": REQUEST_ID,
             "span_id": SPAN_ID,
diff --git a/tests/unit/core/test_tracked_request.py b/tests/unit/core/test_tracked_request.py
index c1b5b97..5d471dc 100644
--- a/tests/unit/core/test_tracked_request.py
+++ b/tests/unit/core/test_tracked_request.py
@@ -2,6 +2,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import datetime as dt
+import sys
 
 from scout_apm.core import objtrace
 from scout_apm.core.tracked_request import TrackedRequest
@@ -39,15 +40,19 @@ def test_tag_request_overwrite(tracked_request):
 
 
 def test_span_repr(tracked_request):
-    span = tracked_request.start_span()
-    try:
-        assert repr(span).startswith("<Span(")
-    finally:
-        tracked_request.stop_span()
+    span = tracked_request.start_span(operation="myoperation")
+    repr_ = repr(span)
+    tracked_request.stop_span()
+
+    assert repr_.startswith("<Span(")
+    if sys.version_info[0] == 2:
+        assert "operation=u'myoperation'" in repr_
+    else:
+        assert "operation='myoperation'" in repr_
 
 
 def test_tag_span(tracked_request):
-    span = tracked_request.start_span()
+    span = tracked_request.start_span(operation="myoperation")
     span.tag("foo", "bar")
     tracked_request.stop_span()
 
@@ -55,7 +60,7 @@ def test_tag_span(tracked_request):
 
 
 def test_tag_span_overwrite(tracked_request):
-    span = tracked_request.start_span()
+    span = tracked_request.start_span(operation="myoperation")
     span.tag("foo", "bar")
     span.tag("foo", "baz")
     tracked_request.stop_span()
@@ -64,8 +69,8 @@ def test_tag_span_overwrite(tracked_request):
 
 
 def test_start_span_wires_parents(tracked_request):
-    span1 = tracked_request.start_span()
-    span2 = tracked_request.start_span()
+    span1 = tracked_request.start_span(operation="myoperation")
+    span2 = tracked_request.start_span(operation="myoperation2")
     assert span1.parent is None
     assert span2.parent == span1.span_id
 
@@ -73,7 +78,7 @@ def test_start_span_wires_parents(tracked_request):
 @skip_if_objtrace_not_extension
 def test_tags_allocations_for_spans(tracked_request):
     objtrace.enable()
-    span = tracked_request.start_span()
+    span = tracked_request.start_span(operation="myoperation")
     tracked_request.stop_span()
     assert span.tags["allocations"] > 0
 
@@ -81,17 +86,17 @@ def test_tags_allocations_for_spans(tracked_request):
 @skip_if_objtrace_is_extension
 def test_tags_allocations_for_spans_no_objtrace_extension(tracked_request):
     objtrace.enable()
-    span = tracked_request.start_span()
+    span = tracked_request.start_span(operation="myoperation")
     tracked_request.stop_span()
     assert "allocations" not in span.tags
 
 
 def test_start_span_does_not_ignore_children(tracked_request):
     tracked_request.start_span(operation="parent")
-    child1 = tracked_request.start_span()
+    child1 = tracked_request.start_span(operation="myoperation")
     assert not child1.ignore
     assert not child1.ignore_children
-    child2 = tracked_request.start_span()
+    child2 = tracked_request.start_span(operation="myoperation")
     assert not child2.ignore
     assert not child2.ignore_children
     tracked_request.stop_span()
@@ -103,10 +108,10 @@ def test_start_span_does_not_ignore_children(tracked_request):
 
 def test_start_span_ignores_children(tracked_request):
     tracked_request.start_span(operation="parent", ignore_children=True)
-    child1 = tracked_request.start_span()
+    child1 = tracked_request.start_span(operation="child1")
     assert child1.ignore
     assert child1.ignore_children
-    child2 = tracked_request.start_span()
+    child2 = tracked_request.start_span(operation="child2")
     assert child2.ignore
     assert child2.ignore_children
     tracked_request.stop_span()
@@ -125,7 +130,7 @@ def test_span_captures_backtrace(tracked_request):
 
 
 def test_should_capture_backtrace_default_true(tracked_request):
-    span = tracked_request.start_span("Something")
+    span = tracked_request.start_span(operation="Something")
     # Trigger 'slow' condition
     span.start_time -= dt.timedelta(seconds=2)
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
black flake8 isort tox bottle celery Django elasticsearch flask flask-sqlalchemy jinja2 mock pymongo pyramid pytest pytest-travis-fold pytest-cov redis six sqlalchemy starlette urllib3 webtest
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
amqp==5.3.1
anyio==3.7.1
asgiref==3.7.2
async-timeout==4.0.3
beautifulsoup4==4.13.3
billiard==3.6.4.0
black==23.3.0
bottle==0.13.2
cached-property==1.5.2
celery==5.2.7
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
click==8.1.8
click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.3.0
coverage==7.2.7
distlib==0.3.9
Django==3.2.25
dnspython==2.3.0
elastic-transport==8.13.1
elasticsearch==8.14.0
exceptiongroup==1.2.2
filelock==3.12.2
flake8==5.0.4
Flask==2.2.5
Flask-SQLAlchemy==3.0.5
greenlet==3.1.1
hupper==1.12.1
idna==3.10
importlib-metadata==4.2.0
iniconfig==2.0.0
isort==5.11.5
itsdangerous==2.1.2
Jinja2==3.1.6
kombu==5.2.4
MarkupSafe==2.1.5
mccabe==0.7.0
mock==5.2.0
mypy-extensions==1.0.0
packaging==24.0
PasteDeploy==3.1.0
pathspec==0.11.2
plaster==1.1.2
plaster-pastedeploy==1.0.1
platformdirs==2.6.2
pluggy==1.2.0
prompt_toolkit==3.0.48
psutil==5.9.8
py==1.11.0
pycodestyle==2.9.1
pyflakes==2.5.0
pymongo==4.7.3
pyramid==2.0.2
pytest==7.4.4
pytest-cov==4.1.0
pytest-travis-fold==1.3.0
pytz==2025.2
redis==5.0.8
requests==2.31.0
-e git+https://github.com/scoutapp/scout_apm_python.git@6f58e0598b74ce3680407e3092a3ab8cbbc4e3fa#egg=scout_apm
six==1.17.0
sniffio==1.3.1
soupsieve==2.4.1
SQLAlchemy==2.0.40
sqlparse==0.4.4
starlette==0.29.0
tomli==2.0.1
tox==3.28.0
translationstring==1.4
typed-ast==1.5.5
typing_extensions==4.7.1
urllib3==2.0.7
venusian==3.1.1
vine==5.1.0
virtualenv==20.16.2
waitress==2.1.2
wcwidth==0.2.13
WebOb==1.8.9
WebTest==3.0.1
Werkzeug==2.2.3
wrapt==1.16.0
zipp==3.15.0
zope.deprecation==5.0
zope.interface==6.4.post2

name: scout_apm_python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - amqp==5.3.1
      - anyio==3.7.1
      - asgiref==3.7.2
      - async-timeout==4.0.3
      - beautifulsoup4==4.13.3
      - billiard==3.6.4.0
      - black==23.3.0
      - bottle==0.13.2
      - cached-property==1.5.2
      - celery==5.2.7
      - charset-normalizer==3.4.1
      - click==8.1.8
      - click-didyoumean==0.3.1
      - click-plugins==1.1.1
      - click-repl==0.3.0
      - coverage==7.2.7
      - distlib==0.3.9
      - django==3.2.25
      - dnspython==2.3.0
      - elastic-transport==8.13.1
      - elasticsearch==8.14.0
      - exceptiongroup==1.2.2
      - filelock==3.12.2
      - flake8==5.0.4
      - flask==2.2.5
      - flask-sqlalchemy==3.0.5
      - greenlet==3.1.1
      - hupper==1.12.1
      - idna==3.10
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - isort==5.11.5
      - itsdangerous==2.1.2
      - jinja2==3.1.6
      - kombu==5.2.4
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - mock==5.2.0
      - mypy-extensions==1.0.0
      - packaging==24.0
      - pastedeploy==3.1.0
      - pathspec==0.11.2
      - plaster==1.1.2
      - plaster-pastedeploy==1.0.1
      - platformdirs==2.6.2
      - pluggy==1.2.0
      - prompt-toolkit==3.0.48
      - psutil==5.9.8
      - py==1.11.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pymongo==4.7.3
      - pyramid==2.0.2
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pytest-travis-fold==1.3.0
      - pytz==2025.2
      - redis==5.0.8
      - requests==2.31.0
      - six==1.17.0
      - sniffio==1.3.1
      - soupsieve==2.4.1
      - sqlalchemy==2.0.40
      - sqlparse==0.4.4
      - starlette==0.29.0
      - tomli==2.0.1
      - tox==3.28.0
      - translationstring==1.4
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - venusian==3.1.1
      - vine==5.1.0
      - virtualenv==20.16.2
      - waitress==2.1.2
      - wcwidth==0.2.13
      - webob==1.8.9
      - webtest==3.0.1
      - werkzeug==2.2.3
      - wrapt==1.16.0
      - zipp==3.15.0
      - zope-deprecation==5.0
      - zope-interface==6.4.post2
prefix: /opt/conda/envs/scout_apm_python

tests/integration/instruments/test_pymongo.py::test_installable_no_pymongo_module tests/integration/instruments/test_pymongo.py::test_install_no_pymongo_module tests/integration/instruments/test_pymongo.py::test_install_missing_attribute tests/integration/instruments/test_pymongo.py::test_install_is_idempotent
tests/integration/instruments/test_elasticsearch.py::test_installed tests/integration/instruments/test_elasticsearch.py::test_installable tests/integration/instruments/test_elasticsearch.py::test_install_is_idempotent tests/integration/test_starlette_py36plus.py::test_home tests/integration/test_starlette_py36plus.py::test_home_ignored tests/integration/test_starlette_py36plus.py::test_hello tests/integration/test_starlette_py36plus.py::test_not_found tests/integration/test_starlette_py36plus.py::test_filtered_params[params0-/?foo=bar] tests/integration/test_starlette_py36plus.py::test_filtered_params[params1-/?foo=bar&foo=bar2] tests/integration/test_starlette_py36plus.py::test_filtered_params[params2-/?password=%5BFILTERED%5D] tests/integration/test_starlette_py36plus.py::test_filtered_params[params3-/?password=%5BFILTERED%5D&password=%5BFILTERED%5D] tests/integration/test_starlette_py36plus.py::test_queue_time[X-Queue-Start] tests/integration/test_starlette_py36plus.py::test_queue_time[X-Request-Start] tests/integration/test_starlette_py36plus.py::test_amazon_queue_time tests/integration/test_starlette_py36plus.py::test_server_error tests/integration/test_starlette_py36plus.py::test_background_jobs tests/integration/test_starlette_py36plus.py::test_username tests/integration/test_starlette_py36plus.py::test_username_bad_user
tests/integration/instruments/test_elasticsearch.py::test_installable_no_elasticsearch_module tests/integration/instruments/test_elasticsearch.py::test_install_no_elasticsearch_module tests/integration/instruments/test_elasticsearch.py::test_instrument_client_install_missing_attribute tests/integration/instruments/test_elasticsearch.py::test_instrument_transport_install_failure tests/integration/instruments/test_elasticsearch.py::test_uninstall_is_idempotent tests/integration/instruments/test_pymongo.py::test_uninstall_is_idempotent tests/integration/test_starlette_py36plus.py::test_user_ip[headers0-None-None] tests/integration/test_starlette_py36plus.py::test_user_ip[headers1-1.1.1.1-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers2-None-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers3-None-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers4-2.2.2.2-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers5-3.3.3.3-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers6-None-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers7-None-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers8-2.2.2.2-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_user_ip[headers9-2.2.2.2-1.1.1.1] tests/integration/test_starlette_py36plus.py::test_no_monitor tests/integration/test_starlette_py36plus.py::test_unknown_asgi_scope tests/unit/core/test_commands.py::test_command_message[command0-message0] tests/unit/core/test_commands.py::test_command_message[command1-message1] tests/unit/core/test_commands.py::test_command_message[command2-message2] tests/unit/core/test_commands.py::test_command_message[command3-message3] tests/unit/core/test_commands.py::test_command_message[command4-message4] tests/unit/core/test_commands.py::test_command_message[command5-message5] tests/unit/core/test_commands.py::test_command_message[command6-message6] tests/unit/core/test_commands.py::test_command_message[command7-message7] tests/unit/core/test_commands.py::test_command_message[command8-message8] tests/unit/core/test_commands.py::test_batch_command_from_tracked_request tests/unit/core/test_commands.py::test_batch_command_from_tracked_request_with_tag tests/unit/core/test_tracked_request.py::test_tracked_request_repr tests/unit/core/test_tracked_request.py::test_tracked_request_instance_is_a_singleton tests/unit/core/test_tracked_request.py::test_is_real_request_default_false tests/unit/core/test_tracked_request.py::test_tag_request tests/unit/core/test_tracked_request.py::test_tag_request_overwrite tests/unit/core/test_tracked_request.py::test_span_repr tests/unit/core/test_tracked_request.py::test_tag_span tests/unit/core/test_tracked_request.py::test_tag_span_overwrite tests/unit/core/test_tracked_request.py::test_start_span_wires_parents tests/unit/core/test_tracked_request.py::test_start_span_does_not_ignore_children tests/unit/core/test_tracked_request.py::test_start_span_ignores_children tests/unit/core/test_tracked_request.py::test_span_captures_backtrace tests/unit/core/test_tracked_request.py::test_should_capture_backtrace_default_true tests/unit/core/test_tracked_request.py::test_should_capture_backtrace_false tests/unit/core/test_tracked_request.py::test_extra_stop_span_is_ignored tests/unit/core/test_tracked_request.py::test_finish_does_not_capture_memory tests/unit/core/test_tracked_request.py::test_finish_does_captures_memory_on_real_requests tests/unit/core/test_tracked_request.py::test_is_ignored_default_false tests/unit/core/test_tracked_request.py::test_is_ignored_mark_true
src/scout_apm/core/tracked_request.py src/scout_apm/instruments/pymongo.py src/scout_apm/django/middleware.py src/scout_apm/instruments/elasticsearch.py src/scout_apm/async_/starlette.py CHANGELOG.md src/scout_apm/bottle.py
src/scout_apm/core/tracked_request.py src/scout_apm/instruments/pymongo.py src/scout_apm/django/middleware.py src/scout_apm/instruments/elasticsearch.py src/scout_apm/async_/starlette.py CHANGELOG.md src/scout_apm/bottle.py