Repository: scoutapp/scout_apm_python

Issue:
object has no attribute '__qualname__'
I am getting the following error when trying to enable the scout APM on a starlette application:
```
Traceback (most recent call last):
  File "/app/.heroku/python/lib/python3.8/site-packages/uvicorn/protocols/http/httptools_impl.py", line 391, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/app/.heroku/python/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/app/.heroku/python/lib/python3.8/site-packages/starlette/applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/app/.heroku/python/lib/python3.8/site-packages/starlette/middleware/errors.py", line 181, in __call__
    raise exc from None
  File "/app/.heroku/python/lib/python3.8/site-packages/starlette/middleware/errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "/app/.heroku/python/lib/python3.8/site-packages/scout_apm/async_/starlette.py", line 67, in __call__
    grab_extra_data()
  File "/app/.heroku/python/lib/python3.8/site-packages/scout_apm/async_/starlette.py", line 35, in grab_extra_data
    endpoint.__module__, endpoint.__qualname__
AttributeError: 'GraphQLWithBackground' object has no attribute '__qualname__'
```

`GraphQLWithBackground` is a subclass of `ariadne.asgi.GraphQL`.


Discussion:

Resolving patch:
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
@@ -31,8 +31,11 @@ def grab_extra_data():
             if "endpoint" in scope:
                 # Rename top span
                 endpoint = scope["endpoint"]
+                if not hasattr(endpoint, "__qualname__"):
+                    endpoint = endpoint.__class__
                 controller_span.operation = "Controller/{}.{}".format(
-                    endpoint.__module__, endpoint.__qualname__
+                    endpoint.__module__,
+                    endpoint.__qualname__,
                 )
                 tracked_request.is_real_request = True
 


Test patch:
diff --git a/tests/integration/test_starlette_py36plus.py b/tests/integration/test_starlette_py36plus.py
--- a/tests/integration/test_starlette_py36plus.py
+++ b/tests/integration/test_starlette_py36plus.py
@@ -72,6 +72,13 @@ async def async_noop():
 
         return PlainTextResponse("Triggering background jobs", background=tasks)
 
+    class InstanceApp:
+        async def __call__(self, scope, receive, send):
+            resp = PlainTextResponse(
+                "Welcome home from an app that's a class instance."
+            )
+            await resp(scope, receive, send)
+
     routes = [
         Route("/", endpoint=home),
         Route("/sync-home/", endpoint=sync_home),
@@ -80,6 +87,7 @@ async def async_noop():
         Route("/crash/", endpoint=crash),
         Route("/return-error/", endpoint=return_error),
         Route("/background-jobs/", endpoint=background_jobs),
+        Route("/instance-app/", endpoint=InstanceApp()),
     ]
 
     async def raise_error_handler(request, exc):
@@ -464,3 +472,29 @@ async def authenticate(self, request):
     assert len(tracked_requests) == 1
     tracked_request = tracked_requests[0]
     assert "username" not in tracked_request.tags
+
+
+@async_test
+async def test_instance_app(tracked_requests):
+    with app_with_scout() as app:
+        communicator = ApplicationCommunicator(
+            app, asgi_http_scope(path="/instance-app/")
+        )
+        await communicator.send_input({"type": "http.request"})
+        # Read the response.
+        response_start = await communicator.receive_output()
+        response_body = await communicator.receive_output()
+
+    assert response_start["type"] == "http.response.start"
+    assert response_start["status"] == 200
+    assert response_body["type"] == "http.response.body"
+    assert response_body["body"] == b"Welcome home from an app that's a class instance."
+    assert len(tracked_requests) == 1
+    tracked_request = tracked_requests[0]
+    assert len(tracked_request.complete_spans) == 1
+    assert tracked_request.tags["path"] == "/instance-app/"
+    span = tracked_request.complete_spans[0]
+    assert span.operation == (
+        "Controller/tests.integration.test_starlette_py36plus."
+        + "app_with_scout.<locals>.InstanceApp"
+    )