We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
assert scope["type"] == "http"`on websocket connection 
`
    ...lib/python3.9/site-packages/elasticapm/contrib/starlette/__init__.py", line 135, in __call__
    assert scope["type"] == "http"
AssertionError
`

**To Reproduce**

```py
import uvicorn
from elasticapm.contrib.starlette import make_apm_client, ElasticAPM
from fastapi import FastAPI, WebSocket

apm = make_apm_client({'SERVICE_NAME': 'test', 'SERVER_URL': 'http://0.0.0.0:8200'})
app = FastAPI()
app.add_middleware(ElasticAPM, client=apm)


@app.websocket('/ws/')
async def websocket_endpoint(
        websocket: WebSocket,
):
    await websocket.accept()


if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=18000)

```

run in Chrome console
```js
socket = new WebSocket('ws://127.0.0.1:18000/ws/');
```

**Environment (please complete the following information)**
- OS: MacOs
- Python version: 3.9
- Framework and version: FastAPI
- APM Server version: 3.6.1



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `elasticapm/contrib/starlette/__init__.py`
Content:
```
1 #  BSD 3-Clause License
2 #
3 #  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details
4 #  Copyright (c) 2019, Elasticsearch BV
5 #  All rights reserved.
6 #
7 #  Redistribution and use in source and binary forms, with or without
8 #  modification, are permitted provided that the following conditions are met:
9 #
10 #  * Redistributions of source code must retain the above copyright notice, this
11 #    list of conditions and the following disclaimer.
12 #
13 #  * Redistributions in binary form must reproduce the above copyright notice,
14 #    this list of conditions and the following disclaimer in the documentation
15 #    and/or other materials provided with the distribution.
16 #
17 #  * Neither the name of the copyright holder nor the names of its
18 #    contributors may be used to endorse or promote products derived from
19 #    this software without specific prior written permission.
20 #
21 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 
31 
32 from __future__ import absolute_import
33 
34 import asyncio
35 import functools
36 from typing import Dict, Optional
37 
38 import starlette
39 from starlette.requests import Request
40 from starlette.routing import Match, Mount
41 from starlette.types import ASGIApp, Message
42 
43 import elasticapm
44 import elasticapm.instrumentation.control
45 from elasticapm.base import Client
46 from elasticapm.conf import constants
47 from elasticapm.contrib.asyncio.traces import set_context
48 from elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response
49 from elasticapm.utils.disttracing import TraceParent
50 from elasticapm.utils.logging import get_logger
51 
52 logger = get_logger("elasticapm.errors.client")
53 
54 
55 def make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client:
56     """Builds ElasticAPM client.
57 
58     Args:
59         config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.
60         client_cls (Client): Must be Client or its child.
61         **defaults: Additional parameters for Client. See `elasticapm.base.Client`
62 
63     Returns:
64         Client
65     """
66     if "framework_name" not in defaults:
67         defaults["framework_name"] = "starlette"
68         defaults["framework_version"] = starlette.__version__
69 
70     return client_cls(config, **defaults)
71 
72 
73 class ElasticAPM:
74     """
75     Starlette / FastAPI middleware for Elastic APM capturing.
76 
77     >>> apm = make_apm_client({
78         >>> 'SERVICE_NAME': 'myapp',
79         >>> 'DEBUG': True,
80         >>> 'SERVER_URL': 'http://localhost:8200',
81         >>> 'CAPTURE_HEADERS': True,
82         >>> 'CAPTURE_BODY': 'all'
83     >>> })
84 
85     >>> app.add_middleware(ElasticAPM, client=apm)
86 
87     Pass an arbitrary APP_NAME and SECRET_TOKEN::
88 
89     >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')
90 
91     Pass an explicit client (don't pass in additional options in this case)::
92 
93     >>> elasticapm = ElasticAPM(app, client=client)
94 
95     Capture an exception::
96 
97     >>> try:
98     >>>     1 / 0
99     >>> except ZeroDivisionError:
100     >>>     elasticapm.capture_exception()
101 
102     Capture a message::
103 
104     >>> elasticapm.capture_message('hello, world!')
105     """
106 
107     def __init__(self, app: ASGIApp, client: Optional[Client], **kwargs):
108         """
109 
110         Args:
111             app (ASGIApp): Starlette app
112             client (Client): ElasticAPM Client
113         """
114         if client:
115             self.client = client
116         else:
117             self.client = make_apm_client(**kwargs)
118 
119         if self.client.config.instrument and self.client.config.enabled:
120             elasticapm.instrumentation.control.instrument()
121 
122         # If we ever make this a general-use ASGI middleware we should use
123         # `asgiref.conpatibility.guarantee_single_callable(app)` here
124         self.app = app
125 
126     async def __call__(self, scope, receive, send):
127         """
128         Args:
129             scope: ASGI scope dictionary
130             receive: receive awaitable callable
131             send: send awaitable callable
132         """
133         # we only handle the http scope, raise an exception for anything else
134         # see https://www.uvicorn.org/#the-asgi-interface
135         assert scope["type"] == "http"
136 
137         @functools.wraps(send)
138         async def wrapped_send(message):
139             if message.get("type") == "http.response.start":
140                 await set_context(
141                     lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), "response"
142                 )
143                 result = "HTTP {}xx".format(message["status"] // 100)
144                 elasticapm.set_transaction_result(result, override=False)
145             await send(message)
146 
147         # When we consume the body from receive, we replace the streaming
148         # mechanism with a mocked version -- this workaround came from
149         # https://github.com/encode/starlette/issues/495#issuecomment-513138055
150         body = b""
151         while True:
152             message = await receive()
153             if not message:
154                 break
155             if message["type"] == "http.request":
156                 b = message.get("body", b"")
157                 if b:
158                     body += b
159                 if not message.get("more_body", False):
160                     break
161             if message["type"] == "http.disconnect":
162                 break
163 
164         async def _receive() -> Message:
165             await asyncio.sleep(0)
166             return {"type": "http.request", "body": body}
167 
168         request = Request(scope, receive=_receive)
169         await self._request_started(request)
170 
171         try:
172             await self.app(scope, _receive, wrapped_send)
173             elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)
174         except Exception:
175             await self.capture_exception(
176                 context={"request": await get_data_from_request(request, self.client.config, constants.ERROR)}
177             )
178             elasticapm.set_transaction_result("HTTP 5xx", override=False)
179             elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)
180             elasticapm.set_context({"status_code": 500}, "response")
181 
182             raise
183         finally:
184             self.client.end_transaction()
185 
186     async def capture_exception(self, *args, **kwargs):
187         """Captures your exception.
188 
189         Args:
190             *args:
191             **kwargs:
192         """
193         self.client.capture_exception(*args, **kwargs)
194 
195     async def capture_message(self, *args, **kwargs):
196         """Captures your message.
197 
198         Args:
199             *args: Whatever
200             **kwargs: Whatever
201         """
202         self.client.capture_message(*args, **kwargs)
203 
204     async def _request_started(self, request: Request):
205         """Captures the begin of the request processing to APM.
206 
207         Args:
208             request (Request)
209         """
210         # When we consume the body, we replace the streaming mechanism with
211         # a mocked version -- this workaround came from
212         # https://github.com/encode/starlette/issues/495#issuecomment-513138055
213         # and we call the workaround here to make sure that regardless of
214         # `capture_body` settings, we will have access to the body if we need it.
215         if self.client.config.capture_body != "off":
216             await get_body(request)
217 
218         if not self.client.should_ignore_url(request.url.path):
219             trace_parent = TraceParent.from_headers(dict(request.headers))
220             self.client.begin_transaction("request", trace_parent=trace_parent)
221 
222             await set_context(
223                 lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request"
224             )
225             transaction_name = self.get_route_name(request) or request.url.path
226             elasticapm.set_transaction_name("{} {}".format(request.method, transaction_name), override=False)
227 
228     def get_route_name(self, request: Request) -> str:
229         app = request.app
230         scope = request.scope
231         routes = app.routes
232         route_name = self._get_route_name(scope, routes)
233 
234         # Starlette magically redirects requests if the path matches a route name with a trailing slash
235         # appended or removed. To not spam the transaction names list, we do the same here and put these
236         # redirects all in the same "redirect trailing slashes" transaction name
237         if not route_name and app.router.redirect_slashes and scope["path"] != "/":
238             redirect_scope = dict(scope)
239             if scope["path"].endswith("/"):
240                 redirect_scope["path"] = scope["path"][:-1]
241                 trim = True
242             else:
243                 redirect_scope["path"] = scope["path"] + "/"
244                 trim = False
245 
246             route_name = self._get_route_name(redirect_scope, routes)
247             if route_name is not None:
248                 route_name = route_name + "/" if trim else route_name[:-1]
249         return route_name
250 
251     def _get_route_name(self, scope, routes, route_name=None):
252         for route in routes:
253             match, child_scope = route.matches(scope)
254             if match == Match.FULL:
255                 route_name = route.path
256                 child_scope = {**scope, **child_scope}
257                 if isinstance(route, Mount) and route.routes:
258                     child_route_name = self._get_route_name(child_scope, route.routes, route_name)
259                     if child_route_name is None:
260                         route_name = None
261                     else:
262                         route_name += child_route_name
263                 return route_name
264             elif match == Match.PARTIAL and route_name is None:
265                 route_name = route.path
266
```
--- 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()

```

diff --git a/elasticapm/contrib/starlette/__init__.py b/elasticapm/contrib/starlette/__init__.py
--- a/elasticapm/contrib/starlette/__init__.py
+++ b/elasticapm/contrib/starlette/__init__.py
@@ -130,9 +130,10 @@
             receive: receive awaitable callable
             send: send awaitable callable
         """
-        # we only handle the http scope, raise an exception for anything else
-        # see https://www.uvicorn.org/#the-asgi-interface
-        assert scope["type"] == "http"
+        # we only handle the http scope, skip anything else.
+        if scope["type"] != "http":
+            await self.app(scope, receive, send)
+            return
 
         @functools.wraps(send)
         async def wrapped_send(message):

{"golden_diff": "diff --git a/elasticapm/contrib/starlette/__init__.py b/elasticapm/contrib/starlette/__init__.py\n--- a/elasticapm/contrib/starlette/__init__.py\n+++ b/elasticapm/contrib/starlette/__init__.py\n@@ -130,9 +130,10 @@\n             receive: receive awaitable callable\n             send: send awaitable callable\n         \"\"\"\n-        # we only handle the http scope, raise an exception for anything else\n-        # see https://www.uvicorn.org/#the-asgi-interface\n-        assert scope[\"type\"] == \"http\"\n+        # we only handle the http scope, skip anything else.\n+        if scope[\"type\"] != \"http\":\n+            await self.app(scope, receive, send)\n+            return\n \n         @functools.wraps(send)\n         async def wrapped_send(message):\n", "issue": "assert scope[\"type\"] == \"http\"`on websocket connection \n`\r\n    ...lib/python3.9/site-packages/elasticapm/contrib/starlette/__init__.py\", line 135, in __call__\r\n    assert scope[\"type\"] == \"http\"\r\nAssertionError\r\n`\r\n\r\n**To Reproduce**\r\n\r\n```py\r\nimport uvicorn\r\nfrom elasticapm.contrib.starlette import make_apm_client, ElasticAPM\r\nfrom fastapi import FastAPI, WebSocket\r\n\r\napm = make_apm_client({'SERVICE_NAME': 'test', 'SERVER_URL': 'http://0.0.0.0:8200'})\r\napp = FastAPI()\r\napp.add_middleware(ElasticAPM, client=apm)\r\n\r\n\r\n@app.websocket('/ws/')\r\nasync def websocket_endpoint(\r\n        websocket: WebSocket,\r\n):\r\n    await websocket.accept()\r\n\r\n\r\nif __name__ == '__main__':\r\n    uvicorn.run(app, host='0.0.0.0', port=18000)\r\n\r\n```\r\n\r\nrun in Chrome console\r\n```js\r\nsocket = new WebSocket('ws://127.0.0.1:18000/ws/');\r\n```\r\n\r\n**Environment (please complete the following information)**\r\n- OS: MacOs\r\n- Python version: 3.9\r\n- Framework and version: FastAPI\r\n- APM Server version: 3.6.1\r\n\r\n\n", "before_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n\n\nfrom __future__ import absolute_import\n\nimport asyncio\nimport functools\nfrom typing import Dict, Optional\n\nimport starlette\nfrom starlette.requests import Request\nfrom starlette.routing import Match, Mount\nfrom starlette.types import ASGIApp, Message\n\nimport elasticapm\nimport elasticapm.instrumentation.control\nfrom elasticapm.base import Client\nfrom elasticapm.conf import constants\nfrom elasticapm.contrib.asyncio.traces import set_context\nfrom elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response\nfrom elasticapm.utils.disttracing import TraceParent\nfrom elasticapm.utils.logging import get_logger\n\nlogger = get_logger(\"elasticapm.errors.client\")\n\n\ndef make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client:\n    \"\"\"Builds ElasticAPM client.\n\n    Args:\n        config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.\n        client_cls (Client): Must be Client or its child.\n        **defaults: Additional parameters for Client. See `elasticapm.base.Client`\n\n    Returns:\n        Client\n    \"\"\"\n    if \"framework_name\" not in defaults:\n        defaults[\"framework_name\"] = \"starlette\"\n        defaults[\"framework_version\"] = starlette.__version__\n\n    return client_cls(config, **defaults)\n\n\nclass ElasticAPM:\n    \"\"\"\n    Starlette / FastAPI middleware for Elastic APM capturing.\n\n    >>> apm = make_apm_client({\n        >>> 'SERVICE_NAME': 'myapp',\n        >>> 'DEBUG': True,\n        >>> 'SERVER_URL': 'http://localhost:8200',\n        >>> 'CAPTURE_HEADERS': True,\n        >>> 'CAPTURE_BODY': 'all'\n    >>> })\n\n    >>> app.add_middleware(ElasticAPM, client=apm)\n\n    Pass an arbitrary APP_NAME and SECRET_TOKEN::\n\n    >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')\n\n    Pass an explicit client (don't pass in additional options in this case)::\n\n    >>> elasticapm = ElasticAPM(app, client=client)\n\n    Capture an exception::\n\n    >>> try:\n    >>>     1 / 0\n    >>> except ZeroDivisionError:\n    >>>     elasticapm.capture_exception()\n\n    Capture a message::\n\n    >>> elasticapm.capture_message('hello, world!')\n    \"\"\"\n\n    def __init__(self, app: ASGIApp, client: Optional[Client], **kwargs):\n        \"\"\"\n\n        Args:\n            app (ASGIApp): Starlette app\n            client (Client): ElasticAPM Client\n        \"\"\"\n        if client:\n            self.client = client\n        else:\n            self.client = make_apm_client(**kwargs)\n\n        if self.client.config.instrument and self.client.config.enabled:\n            elasticapm.instrumentation.control.instrument()\n\n        # If we ever make this a general-use ASGI middleware we should use\n        # `asgiref.conpatibility.guarantee_single_callable(app)` here\n        self.app = app\n\n    async def __call__(self, scope, receive, send):\n        \"\"\"\n        Args:\n            scope: ASGI scope dictionary\n            receive: receive awaitable callable\n            send: send awaitable callable\n        \"\"\"\n        # we only handle the http scope, raise an exception for anything else\n        # see https://www.uvicorn.org/#the-asgi-interface\n        assert scope[\"type\"] == \"http\"\n\n        @functools.wraps(send)\n        async def wrapped_send(message):\n            if message.get(\"type\") == \"http.response.start\":\n                await set_context(\n                    lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), \"response\"\n                )\n                result = \"HTTP {}xx\".format(message[\"status\"] // 100)\n                elasticapm.set_transaction_result(result, override=False)\n            await send(message)\n\n        # When we consume the body from receive, we replace the streaming\n        # mechanism with a mocked version -- this workaround came from\n        # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n        body = b\"\"\n        while True:\n            message = await receive()\n            if not message:\n                break\n            if message[\"type\"] == \"http.request\":\n                b = message.get(\"body\", b\"\")\n                if b:\n                    body += b\n                if not message.get(\"more_body\", False):\n                    break\n            if message[\"type\"] == \"http.disconnect\":\n                break\n\n        async def _receive() -> Message:\n            await asyncio.sleep(0)\n            return {\"type\": \"http.request\", \"body\": body}\n\n        request = Request(scope, receive=_receive)\n        await self._request_started(request)\n\n        try:\n            await self.app(scope, _receive, wrapped_send)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)\n        except Exception:\n            await self.capture_exception(\n                context={\"request\": await get_data_from_request(request, self.client.config, constants.ERROR)}\n            )\n            elasticapm.set_transaction_result(\"HTTP 5xx\", override=False)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)\n            elasticapm.set_context({\"status_code\": 500}, \"response\")\n\n            raise\n        finally:\n            self.client.end_transaction()\n\n    async def capture_exception(self, *args, **kwargs):\n        \"\"\"Captures your exception.\n\n        Args:\n            *args:\n            **kwargs:\n        \"\"\"\n        self.client.capture_exception(*args, **kwargs)\n\n    async def capture_message(self, *args, **kwargs):\n        \"\"\"Captures your message.\n\n        Args:\n            *args: Whatever\n            **kwargs: Whatever\n        \"\"\"\n        self.client.capture_message(*args, **kwargs)\n\n    async def _request_started(self, request: Request):\n        \"\"\"Captures the begin of the request processing to APM.\n\n        Args:\n            request (Request)\n        \"\"\"\n        # When we consume the body, we replace the streaming mechanism with\n        # a mocked version -- this workaround came from\n        # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n        # and we call the workaround here to make sure that regardless of\n        # `capture_body` settings, we will have access to the body if we need it.\n        if self.client.config.capture_body != \"off\":\n            await get_body(request)\n\n        if not self.client.should_ignore_url(request.url.path):\n            trace_parent = TraceParent.from_headers(dict(request.headers))\n            self.client.begin_transaction(\"request\", trace_parent=trace_parent)\n\n            await set_context(\n                lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), \"request\"\n            )\n            transaction_name = self.get_route_name(request) or request.url.path\n            elasticapm.set_transaction_name(\"{} {}\".format(request.method, transaction_name), override=False)\n\n    def get_route_name(self, request: Request) -> str:\n        app = request.app\n        scope = request.scope\n        routes = app.routes\n        route_name = self._get_route_name(scope, routes)\n\n        # Starlette magically redirects requests if the path matches a route name with a trailing slash\n        # appended or removed. To not spam the transaction names list, we do the same here and put these\n        # redirects all in the same \"redirect trailing slashes\" transaction name\n        if not route_name and app.router.redirect_slashes and scope[\"path\"] != \"/\":\n            redirect_scope = dict(scope)\n            if scope[\"path\"].endswith(\"/\"):\n                redirect_scope[\"path\"] = scope[\"path\"][:-1]\n                trim = True\n            else:\n                redirect_scope[\"path\"] = scope[\"path\"] + \"/\"\n                trim = False\n\n            route_name = self._get_route_name(redirect_scope, routes)\n            if route_name is not None:\n                route_name = route_name + \"/\" if trim else route_name[:-1]\n        return route_name\n\n    def _get_route_name(self, scope, routes, route_name=None):\n        for route in routes:\n            match, child_scope = route.matches(scope)\n            if match == Match.FULL:\n                route_name = route.path\n                child_scope = {**scope, **child_scope}\n                if isinstance(route, Mount) and route.routes:\n                    child_route_name = self._get_route_name(child_scope, route.routes, route_name)\n                    if child_route_name is None:\n                        route_name = None\n                    else:\n                        route_name += child_route_name\n                return route_name\n            elif match == Match.PARTIAL and route_name is None:\n                route_name = route.path\n", "path": "elasticapm/contrib/starlette/__init__.py"}], "after_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n\n\nfrom __future__ import absolute_import\n\nimport asyncio\nimport functools\nfrom typing import Dict, Optional\n\nimport starlette\nfrom starlette.requests import Request\nfrom starlette.routing import Match, Mount\nfrom starlette.types import ASGIApp, Message\n\nimport elasticapm\nimport elasticapm.instrumentation.control\nfrom elasticapm.base import Client\nfrom elasticapm.conf import constants\nfrom elasticapm.contrib.asyncio.traces import set_context\nfrom elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response\nfrom elasticapm.utils.disttracing import TraceParent\nfrom elasticapm.utils.logging import get_logger\n\nlogger = get_logger(\"elasticapm.errors.client\")\n\n\ndef make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client:\n    \"\"\"Builds ElasticAPM client.\n\n    Args:\n        config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`.\n        client_cls (Client): Must be Client or its child.\n        **defaults: Additional parameters for Client. See `elasticapm.base.Client`\n\n    Returns:\n        Client\n    \"\"\"\n    if \"framework_name\" not in defaults:\n        defaults[\"framework_name\"] = \"starlette\"\n        defaults[\"framework_version\"] = starlette.__version__\n\n    return client_cls(config, **defaults)\n\n\nclass ElasticAPM:\n    \"\"\"\n    Starlette / FastAPI middleware for Elastic APM capturing.\n\n    >>> apm = make_apm_client({\n        >>> 'SERVICE_NAME': 'myapp',\n        >>> 'DEBUG': True,\n        >>> 'SERVER_URL': 'http://localhost:8200',\n        >>> 'CAPTURE_HEADERS': True,\n        >>> 'CAPTURE_BODY': 'all'\n    >>> })\n\n    >>> app.add_middleware(ElasticAPM, client=apm)\n\n    Pass an arbitrary APP_NAME and SECRET_TOKEN::\n\n    >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')\n\n    Pass an explicit client (don't pass in additional options in this case)::\n\n    >>> elasticapm = ElasticAPM(app, client=client)\n\n    Capture an exception::\n\n    >>> try:\n    >>>     1 / 0\n    >>> except ZeroDivisionError:\n    >>>     elasticapm.capture_exception()\n\n    Capture a message::\n\n    >>> elasticapm.capture_message('hello, world!')\n    \"\"\"\n\n    def __init__(self, app: ASGIApp, client: Optional[Client], **kwargs):\n        \"\"\"\n\n        Args:\n            app (ASGIApp): Starlette app\n            client (Client): ElasticAPM Client\n        \"\"\"\n        if client:\n            self.client = client\n        else:\n            self.client = make_apm_client(**kwargs)\n\n        if self.client.config.instrument and self.client.config.enabled:\n            elasticapm.instrumentation.control.instrument()\n\n        # If we ever make this a general-use ASGI middleware we should use\n        # `asgiref.conpatibility.guarantee_single_callable(app)` here\n        self.app = app\n\n    async def __call__(self, scope, receive, send):\n        \"\"\"\n        Args:\n            scope: ASGI scope dictionary\n            receive: receive awaitable callable\n            send: send awaitable callable\n        \"\"\"\n        # we only handle the http scope, skip anything else.\n        if scope[\"type\"] != \"http\":\n            await self.app(scope, receive, send)\n            return\n\n        @functools.wraps(send)\n        async def wrapped_send(message):\n            if message.get(\"type\") == \"http.response.start\":\n                await set_context(\n                    lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), \"response\"\n                )\n                result = \"HTTP {}xx\".format(message[\"status\"] // 100)\n                elasticapm.set_transaction_result(result, override=False)\n            await send(message)\n\n        # When we consume the body from receive, we replace the streaming\n        # mechanism with a mocked version -- this workaround came from\n        # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n        body = b\"\"\n        while True:\n            message = await receive()\n            if not message:\n                break\n            if message[\"type\"] == \"http.request\":\n                b = message.get(\"body\", b\"\")\n                if b:\n                    body += b\n                if not message.get(\"more_body\", False):\n                    break\n            if message[\"type\"] == \"http.disconnect\":\n                break\n\n        async def _receive() -> Message:\n            await asyncio.sleep(0)\n            return {\"type\": \"http.request\", \"body\": body}\n\n        request = Request(scope, receive=_receive)\n        await self._request_started(request)\n\n        try:\n            await self.app(scope, _receive, wrapped_send)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)\n        except Exception:\n            await self.capture_exception(\n                context={\"request\": await get_data_from_request(request, self.client.config, constants.ERROR)}\n            )\n            elasticapm.set_transaction_result(\"HTTP 5xx\", override=False)\n            elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)\n            elasticapm.set_context({\"status_code\": 500}, \"response\")\n\n            raise\n        finally:\n            self.client.end_transaction()\n\n    async def capture_exception(self, *args, **kwargs):\n        \"\"\"Captures your exception.\n\n        Args:\n            *args:\n            **kwargs:\n        \"\"\"\n        self.client.capture_exception(*args, **kwargs)\n\n    async def capture_message(self, *args, **kwargs):\n        \"\"\"Captures your message.\n\n        Args:\n            *args: Whatever\n            **kwargs: Whatever\n        \"\"\"\n        self.client.capture_message(*args, **kwargs)\n\n    async def _request_started(self, request: Request):\n        \"\"\"Captures the begin of the request processing to APM.\n\n        Args:\n            request (Request)\n        \"\"\"\n        # When we consume the body, we replace the streaming mechanism with\n        # a mocked version -- this workaround came from\n        # https://github.com/encode/starlette/issues/495#issuecomment-513138055\n        # and we call the workaround here to make sure that regardless of\n        # `capture_body` settings, we will have access to the body if we need it.\n        if self.client.config.capture_body != \"off\":\n            await get_body(request)\n\n        if not self.client.should_ignore_url(request.url.path):\n            trace_parent = TraceParent.from_headers(dict(request.headers))\n            self.client.begin_transaction(\"request\", trace_parent=trace_parent)\n\n            await set_context(\n                lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), \"request\"\n            )\n            transaction_name = self.get_route_name(request) or request.url.path\n            elasticapm.set_transaction_name(\"{} {}\".format(request.method, transaction_name), override=False)\n\n    def get_route_name(self, request: Request) -> str:\n        app = request.app\n        scope = request.scope\n        routes = app.routes\n        route_name = self._get_route_name(scope, routes)\n\n        # Starlette magically redirects requests if the path matches a route name with a trailing slash\n        # appended or removed. To not spam the transaction names list, we do the same here and put these\n        # redirects all in the same \"redirect trailing slashes\" transaction name\n        if not route_name and app.router.redirect_slashes and scope[\"path\"] != \"/\":\n            redirect_scope = dict(scope)\n            if scope[\"path\"].endswith(\"/\"):\n                redirect_scope[\"path\"] = scope[\"path\"][:-1]\n                trim = True\n            else:\n                redirect_scope[\"path\"] = scope[\"path\"] + \"/\"\n                trim = False\n\n            route_name = self._get_route_name(redirect_scope, routes)\n            if route_name is not None:\n                route_name = route_name + \"/\" if trim else route_name[:-1]\n        return route_name\n\n    def _get_route_name(self, scope, routes, route_name=None):\n        for route in routes:\n            match, child_scope = route.matches(scope)\n            if match == Match.FULL:\n                route_name = route.path\n                child_scope = {**scope, **child_scope}\n                if isinstance(route, Mount) and route.routes:\n                    child_route_name = self._get_route_name(child_scope, route.routes, route_name)\n                    if child_route_name is None:\n                        route_name = None\n                    else:\n                        route_name += child_route_name\n                return route_name\n            elif match == Match.PARTIAL and route_name is None:\n                route_name = route.path\n", "path": "elasticapm/contrib/starlette/__init__.py"}]}