prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Instrument Starlette background tasks
  Starlette supports [background tasks](https://www.starlette.io/background/). We should instrument these as background transactions.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `src/scout_apm/async_/starlette.py`
  Content:
  ```
  1 # coding=utf-8
  2 from __future__ import absolute_import, division, print_function, unicode_literals
  3 
  4 from starlette.requests import Request
  5 
  6 import scout_apm.core
  7 from scout_apm.core.tracked_request import TrackedRequest
  8 from scout_apm.core.web_requests import (
  9     create_filtered_path,
  10     ignore_path,
  11     track_amazon_request_queue_time,
  12     track_request_queue_time,
  13 )
  14 
  15 
  16 class ScoutMiddleware:
  17     def __init__(self, app):
  18         self.app = app
  19         installed = scout_apm.core.install()
  20         self._do_nothing = not installed
  21 
  22     async def __call__(self, scope, receive, send):
  23         if self._do_nothing or scope["type"] != "http":
  24             await self.app(scope, receive, send)
  25             return
  26 
  27         request = Request(scope)
  28         tracked_request = TrackedRequest.instance()
  29         # Can't name controller until post-routing - see final clause
  30         controller_span = tracked_request.start_span(operation="Controller/Unknown")
  31 
  32         tracked_request.tag(
  33             "path",
  34             create_filtered_path(request.url.path, request.query_params.multi_items()),
  35         )
  36         if ignore_path(request.url.path):
  37             tracked_request.tag("ignore_transaction", True)
  38 
  39         user_ip = (
  40             request.headers.get("x-forwarded-for", default="").split(",")[0]
  41             or request.headers.get("client-ip", default="").split(",")[0]
  42             or request.client.host
  43         )
  44         tracked_request.tag("user_ip", user_ip)
  45 
  46         queue_time = request.headers.get(
  47             "x-queue-start", default=""
  48         ) or request.headers.get("x-request-start", default="")
  49         tracked_queue_time = track_request_queue_time(queue_time, tracked_request)
  50         if not tracked_queue_time:
  51             amazon_queue_time = request.headers.get("x-amzn-trace-id", default="")
  52             track_amazon_request_queue_time(amazon_queue_time, tracked_request)
  53 
  54         try:
  55             await self.app(scope, receive, send)
  56         except Exception as exc:
  57             tracked_request.tag("error", "true")
  58             raise exc
  59         finally:
  60             if "endpoint" in scope:
  61                 endpoint = scope["endpoint"]
  62                 controller_span.operation = "Controller/{}.{}".format(
  63                     endpoint.__module__, endpoint.__qualname__
  64                 )
  65                 tracked_request.is_real_request = True
  66             tracked_request.stop_span()
  67
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/src/scout_apm/async_/starlette.py b/src/scout_apm/async_/starlette.py
  --- a/src/scout_apm/async_/starlette.py
  +++ b/src/scout_apm/async_/starlette.py
  @@ -1,6 +1,8 @@
   # coding=utf-8
   from __future__ import absolute_import, division, print_function, unicode_literals
   
  +import wrapt
  +from starlette.background import BackgroundTask
   from starlette.requests import Request
   
   import scout_apm.core
  @@ -18,6 +20,8 @@
           self.app = app
           installed = scout_apm.core.install()
           self._do_nothing = not installed
  +        if installed:
  +            install_background_instrumentation()
   
       async def __call__(self, scope, receive, send):
           if self._do_nothing or scope["type"] != "http":
  @@ -51,16 +55,57 @@
               amazon_queue_time = request.headers.get("x-amzn-trace-id", default="")
               track_amazon_request_queue_time(amazon_queue_time, tracked_request)
   
  -        try:
  -            await self.app(scope, receive, send)
  -        except Exception as exc:
  -            tracked_request.tag("error", "true")
  -            raise exc
  -        finally:
  +        def rename_controller_span_from_endpoint():
               if "endpoint" in scope:
  +                # Rename top span
                   endpoint = scope["endpoint"]
                   controller_span.operation = "Controller/{}.{}".format(
                       endpoint.__module__, endpoint.__qualname__
                   )
                   tracked_request.is_real_request = True
  +
  +        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()
  +                tracked_request.stop_span()
  +            return await send(data)
  +
  +        try:
  +            await self.app(scope, receive, wrapped_send)
  +        except Exception as exc:
  +            tracked_request.tag("error", "true")
  +            raise exc
  +        finally:
  +            if tracked_request.end_time is None:
  +                rename_controller_span_from_endpoint()
  +                tracked_request.stop_span()
  +
  +
  +background_instrumentation_installed = False
  +
  +
  +def install_background_instrumentation():
  +    global background_instrumentation_installed
  +    if background_instrumentation_installed:
  +        return
  +    background_instrumentation_installed = True
  +
  +    @wrapt.decorator
  +    async def wrapped_background_call(wrapped, instance, args, kwargs):
  +        tracked_request = TrackedRequest.instance()
  +        tracked_request.is_real_request = True
  +        tracked_request.start_span(
  +            operation="Job/{}.{}".format(
  +                instance.func.__module__, instance.func.__qualname__
  +            )
  +        )
  +        try:
  +            return await wrapped(*args, **kwargs)
  +        finally:
               tracked_request.stop_span()
  +
  +    BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)
  
verification_info: {"golden_diff": "diff --git a/src/scout_apm/async_/starlette.py b/src/scout_apm/async_/starlette.py\n--- a/src/scout_apm/async_/starlette.py\n+++ b/src/scout_apm/async_/starlette.py\n@@ -1,6 +1,8 @@\n # coding=utf-8\n from __future__ import absolute_import, division, print_function, unicode_literals\n \n+import wrapt\n+from starlette.background import BackgroundTask\n from starlette.requests import Request\n \n import scout_apm.core\n@@ -18,6 +20,8 @@\n         self.app = app\n         installed = scout_apm.core.install()\n         self._do_nothing = not installed\n+        if installed:\n+            install_background_instrumentation()\n \n     async def __call__(self, scope, receive, send):\n         if self._do_nothing or scope[\"type\"] != \"http\":\n@@ -51,16 +55,57 @@\n             amazon_queue_time = request.headers.get(\"x-amzn-trace-id\", default=\"\")\n             track_amazon_request_queue_time(amazon_queue_time, tracked_request)\n \n-        try:\n-            await self.app(scope, receive, send)\n-        except Exception as exc:\n-            tracked_request.tag(\"error\", \"true\")\n-            raise exc\n-        finally:\n+        def rename_controller_span_from_endpoint():\n             if \"endpoint\" in scope:\n+                # Rename top span\n                 endpoint = scope[\"endpoint\"]\n                 controller_span.operation = \"Controller/{}.{}\".format(\n                     endpoint.__module__, endpoint.__qualname__\n                 )\n                 tracked_request.is_real_request = True\n+\n+        async def wrapped_send(data):\n+            # Finish HTTP span when body finishes sending, not later (e.g.\n+            # after background tasks)\n+            if data.get(\"type\", None) == \"http.response.body\" and not data.get(\n+                \"more_body\", False\n+            ):\n+                rename_controller_span_from_endpoint()\n+                tracked_request.stop_span()\n+            return await send(data)\n+\n+        try:\n+            await self.app(scope, receive, wrapped_send)\n+        except Exception as exc:\n+            tracked_request.tag(\"error\", \"true\")\n+            raise exc\n+        finally:\n+            if tracked_request.end_time is None:\n+                rename_controller_span_from_endpoint()\n+                tracked_request.stop_span()\n+\n+\n+background_instrumentation_installed = False\n+\n+\n+def install_background_instrumentation():\n+    global background_instrumentation_installed\n+    if background_instrumentation_installed:\n+        return\n+    background_instrumentation_installed = True\n+\n+    @wrapt.decorator\n+    async def wrapped_background_call(wrapped, instance, args, kwargs):\n+        tracked_request = TrackedRequest.instance()\n+        tracked_request.is_real_request = True\n+        tracked_request.start_span(\n+            operation=\"Job/{}.{}\".format(\n+                instance.func.__module__, instance.func.__qualname__\n+            )\n+        )\n+        try:\n+            return await wrapped(*args, **kwargs)\n+        finally:\n             tracked_request.stop_span()\n+\n+    BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)\n", "issue": "Instrument Starlette background tasks\nStarlette supports [background tasks](https://www.starlette.io/background/). We should instrument these as background transactions.\n", "before_files": [{"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom starlette.requests import Request\n\nimport scout_apm.core\nfrom scout_apm.core.tracked_request import TrackedRequest\nfrom scout_apm.core.web_requests import (\n    create_filtered_path,\n    ignore_path,\n    track_amazon_request_queue_time,\n    track_request_queue_time,\n)\n\n\nclass ScoutMiddleware:\n    def __init__(self, app):\n        self.app = app\n        installed = scout_apm.core.install()\n        self._do_nothing = not installed\n\n    async def __call__(self, scope, receive, send):\n        if self._do_nothing or scope[\"type\"] != \"http\":\n            await self.app(scope, receive, send)\n            return\n\n        request = Request(scope)\n        tracked_request = TrackedRequest.instance()\n        # Can't name controller until post-routing - see final clause\n        controller_span = tracked_request.start_span(operation=\"Controller/Unknown\")\n\n        tracked_request.tag(\n            \"path\",\n            create_filtered_path(request.url.path, request.query_params.multi_items()),\n        )\n        if ignore_path(request.url.path):\n            tracked_request.tag(\"ignore_transaction\", True)\n\n        user_ip = (\n            request.headers.get(\"x-forwarded-for\", default=\"\").split(\",\")[0]\n            or request.headers.get(\"client-ip\", default=\"\").split(\",\")[0]\n            or request.client.host\n        )\n        tracked_request.tag(\"user_ip\", user_ip)\n\n        queue_time = request.headers.get(\n            \"x-queue-start\", default=\"\"\n        ) or request.headers.get(\"x-request-start\", default=\"\")\n        tracked_queue_time = track_request_queue_time(queue_time, tracked_request)\n        if not tracked_queue_time:\n            amazon_queue_time = request.headers.get(\"x-amzn-trace-id\", default=\"\")\n            track_amazon_request_queue_time(amazon_queue_time, tracked_request)\n\n        try:\n            await self.app(scope, receive, send)\n        except Exception as exc:\n            tracked_request.tag(\"error\", \"true\")\n            raise exc\n        finally:\n            if \"endpoint\" in scope:\n                endpoint = scope[\"endpoint\"]\n                controller_span.operation = \"Controller/{}.{}\".format(\n                    endpoint.__module__, endpoint.__qualname__\n                )\n                tracked_request.is_real_request = True\n            tracked_request.stop_span()\n", "path": "src/scout_apm/async_/starlette.py"}], "after_files": [{"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport wrapt\nfrom starlette.background import BackgroundTask\nfrom starlette.requests import Request\n\nimport scout_apm.core\nfrom scout_apm.core.tracked_request import TrackedRequest\nfrom scout_apm.core.web_requests import (\n    create_filtered_path,\n    ignore_path,\n    track_amazon_request_queue_time,\n    track_request_queue_time,\n)\n\n\nclass ScoutMiddleware:\n    def __init__(self, app):\n        self.app = app\n        installed = scout_apm.core.install()\n        self._do_nothing = not installed\n        if installed:\n            install_background_instrumentation()\n\n    async def __call__(self, scope, receive, send):\n        if self._do_nothing or scope[\"type\"] != \"http\":\n            await self.app(scope, receive, send)\n            return\n\n        request = Request(scope)\n        tracked_request = TrackedRequest.instance()\n        # Can't name controller until post-routing - see final clause\n        controller_span = tracked_request.start_span(operation=\"Controller/Unknown\")\n\n        tracked_request.tag(\n            \"path\",\n            create_filtered_path(request.url.path, request.query_params.multi_items()),\n        )\n        if ignore_path(request.url.path):\n            tracked_request.tag(\"ignore_transaction\", True)\n\n        user_ip = (\n            request.headers.get(\"x-forwarded-for\", default=\"\").split(\",\")[0]\n            or request.headers.get(\"client-ip\", default=\"\").split(\",\")[0]\n            or request.client.host\n        )\n        tracked_request.tag(\"user_ip\", user_ip)\n\n        queue_time = request.headers.get(\n            \"x-queue-start\", default=\"\"\n        ) or request.headers.get(\"x-request-start\", default=\"\")\n        tracked_queue_time = track_request_queue_time(queue_time, tracked_request)\n        if not tracked_queue_time:\n            amazon_queue_time = request.headers.get(\"x-amzn-trace-id\", default=\"\")\n            track_amazon_request_queue_time(amazon_queue_time, tracked_request)\n\n        def rename_controller_span_from_endpoint():\n            if \"endpoint\" in scope:\n                # Rename top span\n                endpoint = scope[\"endpoint\"]\n                controller_span.operation = \"Controller/{}.{}\".format(\n                    endpoint.__module__, endpoint.__qualname__\n                )\n                tracked_request.is_real_request = True\n\n        async def wrapped_send(data):\n            # Finish HTTP span when body finishes sending, not later (e.g.\n            # after background tasks)\n            if data.get(\"type\", None) == \"http.response.body\" and not data.get(\n                \"more_body\", False\n            ):\n                rename_controller_span_from_endpoint()\n                tracked_request.stop_span()\n            return await send(data)\n\n        try:\n            await self.app(scope, receive, wrapped_send)\n        except Exception as exc:\n            tracked_request.tag(\"error\", \"true\")\n            raise exc\n        finally:\n            if tracked_request.end_time is None:\n                rename_controller_span_from_endpoint()\n                tracked_request.stop_span()\n\n\nbackground_instrumentation_installed = False\n\n\ndef install_background_instrumentation():\n    global background_instrumentation_installed\n    if background_instrumentation_installed:\n        return\n    background_instrumentation_installed = True\n\n    @wrapt.decorator\n    async def wrapped_background_call(wrapped, instance, args, kwargs):\n        tracked_request = TrackedRequest.instance()\n        tracked_request.is_real_request = True\n        tracked_request.start_span(\n            operation=\"Job/{}.{}\".format(\n                instance.func.__module__, instance.func.__qualname__\n            )\n        )\n        try:\n            return await wrapped(*args, **kwargs)\n        finally:\n            tracked_request.stop_span()\n\n    BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)\n", "path": "src/scout_apm/async_/starlette.py"}]}