Repository: elastic/apm-agent-python

Issue:
Wrong transaction name in Django 4.0+
**Describe the bug**:
When using Django versions 4.0+, the transaction names are incorrect.

Expected: `django_app.views.ListUsers`
Actual: `django_app.views.view`


**Environment (please complete the following information)**
- OS: Debian Bullseye
- Python version: 3.9.13
- Framework and version: Django 4.0.5
- APM Server version: 7.12.0
- Agent version: 6.9.1


**Additional context**

This issue seems to be introduced with the following [PR](https://github.com/django/django/pull/14124) to Django.
https://github.com/django/django/blob/e96320c91724830034033a9cb8afd9cf8c11e2fd/django/views/generic/base.py#L108-#L110

I will try to provide a fix shortly.


Discussion:

Resolving patch:
diff --git a/elasticapm/utils/__init__.py b/elasticapm/utils/__init__.py
--- a/elasticapm/utils/__init__.py
+++ b/elasticapm/utils/__init__.py
@@ -83,7 +83,9 @@ def get_name_from_func(func: FunctionType) -> str:
 
     module = func.__module__
 
-    if hasattr(func, "__name__"):
+    if hasattr(func, "view_class"):
+        view_name = func.view_class.__name__
+    elif hasattr(func, "__name__"):
         view_name = func.__name__
     else:  # Fall back if there's no __name__
         view_name = func.__class__.__name__


Test patch:
diff --git a/tests/contrib/django/django_tests.py b/tests/contrib/django/django_tests.py
--- a/tests/contrib/django/django_tests.py
+++ b/tests/contrib/django/django_tests.py
@@ -1662,3 +1662,12 @@ def test_default_app_config_present_by_version():
         assert default_app_config_is_defined
     else:
         assert not default_app_config_is_defined
+
+
+def test_transaction_name_from_class_based_view(client, django_elasticapm_client):
+    with override_settings(
+        **middleware_setting(django.VERSION, ["elasticapm.contrib.django.middleware.TracingMiddleware"])
+    ):
+        client.get(reverse("elasticapm-class-based"))
+        transaction = django_elasticapm_client.events[TRANSACTION][0]
+        assert transaction["name"] == "GET tests.contrib.django.testapp.views.ClassBasedView"
diff --git a/tests/contrib/django/testapp/urls.py b/tests/contrib/django/testapp/urls.py
--- a/tests/contrib/django/testapp/urls.py
+++ b/tests/contrib/django/testapp/urls.py
@@ -66,6 +66,7 @@ def handler500(request):
     re_path(r"^trigger-500-log-request$", views.logging_request_exc, name="elasticapm-log-request-exc"),
     re_path(r"^streaming$", views.streaming_view, name="elasticapm-streaming-view"),
     re_path(r"^name-override$", views.override_transaction_name_view, name="elasticapm-name-override"),
+    re_path(r"^class-based$", views.ClassBasedView.as_view(), name="elasticapm-class-based"),
 )
 
 
diff --git a/tests/contrib/django/testapp/views.py b/tests/contrib/django/testapp/views.py
--- a/tests/contrib/django/testapp/views.py
+++ b/tests/contrib/django/testapp/views.py
@@ -36,6 +36,7 @@
 from django.contrib.auth.models import User
 from django.http import HttpResponse, StreamingHttpResponse
 from django.shortcuts import get_object_or_404, render
+from django.views import View
 
 import elasticapm
 
@@ -141,3 +142,8 @@ def override_transaction_name_view(request):
     elasticapm.set_transaction_name("foo")
     elasticapm.set_transaction_result("okydoky")
     return HttpResponse()
+
+
+class ClassBasedView(View):
+    def get(self, request):
+        return HttpResponse()