Repository: getsentry/sentry-python

Issue:
Add support for djongo
### Problem Statement

It looks like when we add Sentry to a Django application with MongoDB, Djongo seems to be monkeypatching the connection object that we rely on, causing the django integration to break. The error thrown is:
```
File "/Users/user/opt/anaconda3/envs/ZETA/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1175, in execute_sql
cursor.execute(sql, params)
File "/Users/user/opt/anaconda3/envs/ZETA/lib/python3.8/site-packages/django/db/backends/utils.py", line 98, in execute
return super().execute(sql, params)
File "/Users/user/opt/anaconda3/envs/ZETA/lib/python3.8/site-packages/sentry_sdk/integrations/django/__init__.py", line 615, in execute
_set_db_data(span, self)
File "/Users/user/opt/anaconda3/envs/ZETA/lib/python3.8/site-packages/sentry_sdk/integrations/django/__init__.py", line 657, in _set_db_data
cursor_or_db.connection.get_dsn_parameters()
File "/Users/user/opt/anaconda3/envs/ZETA/lib/python3.8/site-packages/pymongo/collection.py", line 3491, in __call__
raise TypeError("'Collection' object is not callable. If you meant to "
TypeError: 'Collection' object is not callable. If you meant to call the 'get_dsn_parameters' method on a 'Collection' object it is failing because no such method exists.
```

### Solution Brainstorm

We should add support for Djongo so that adding Sentry's Django SDK works without any issues.


Discussion:
@InterstellarStella Looking at the stacktrace it seems like they're on a slightly older Python SDK version. Upgrading to 1.32.0 might help because we're now a bit more careful about accessing the params: https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/django/__init__.py#L668-L673
@sentrivana I went to the place you highlighted in the code and updated that part to immediately go to the else block.
` connection_params = (
        db.get_connection_params() 
)`

It is working great. For now we need to update it locally for everyone in the team until you guys find a solution. 

Thank you for your help @InterstellarStella @sentrivana.
Hey @omarsagheer, thanks for the follow up! Just to clarify, you're on SDK version 1.32.0 and it still required the patch you mentioned?
hello @sentrivana yes I updated and tried, same issue, still needed to manually update that part I mentioned.
Thanks! We'll look into fixing that in the next release.
Great! Thank you for your time and effort.

Resolving patch:
diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py
--- a/sentry_sdk/integrations/django/__init__.py
+++ b/sentry_sdk/integrations/django/__init__.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+import inspect
 import sys
 import threading
 import weakref
@@ -665,12 +666,21 @@ def _set_db_data(span, cursor_or_db):
     vendor = db.vendor
     span.set_data(SPANDATA.DB_SYSTEM, vendor)
 
-    connection_params = (
-        cursor_or_db.connection.get_dsn_parameters()
-        if hasattr(cursor_or_db, "connection")
+    if (
+        hasattr(cursor_or_db, "connection")
         and hasattr(cursor_or_db.connection, "get_dsn_parameters")
-        else db.get_connection_params()
-    )
+        and inspect.isfunction(cursor_or_db.connection.get_dsn_parameters)
+    ):
+        # Some custom backends override `__getattr__`, making it look like `cursor_or_db`
+        # actually has a `connection` and the `connection` has a `get_dsn_parameters`
+        # attribute, only to throw an error once you actually want to call it.
+        # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable
+        # function.
+        connection_params = cursor_or_db.connection.get_dsn_parameters()
+
+    else:
+        connection_params = db.get_connection_params()
+
     db_name = connection_params.get("dbname") or connection_params.get("database")
     if db_name is not None:
         span.set_data(SPANDATA.DB_NAME, db_name)


Test patch:
diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py
--- a/tests/integrations/django/test_basic.py
+++ b/tests/integrations/django/test_basic.py
@@ -22,10 +22,11 @@
 from sentry_sdk._compat import PY2, PY310
 from sentry_sdk import capture_message, capture_exception, configure_scope
 from sentry_sdk.consts import SPANDATA
-from sentry_sdk.integrations.django import DjangoIntegration
+from sentry_sdk.integrations.django import DjangoIntegration, _set_db_data
 from sentry_sdk.integrations.django.signals_handlers import _get_receiver_name
 from sentry_sdk.integrations.django.caching import _get_span_description
 from sentry_sdk.integrations.executing import ExecutingIntegration
+from sentry_sdk.tracing import Span
 from tests.integrations.django.myapp.wsgi import application
 from tests.integrations.django.utils import pytest_mark_django_db_decorator
 
@@ -656,6 +657,24 @@ def test_db_connection_span_data(sentry_init, client, capture_events):
             assert data.get(SPANDATA.SERVER_PORT) == "5432"
 
 
+def test_set_db_data_custom_backend():
+    class DummyBackend(object):
+        # https://github.com/mongodb/mongo-python-driver/blob/6ffae5522c960252b8c9adfe2a19b29ff28187cb/pymongo/collection.py#L126
+        def __getattr__(self, attr):
+            return self
+
+        def __call__(self):
+            raise TypeError
+
+        def get_connection_params(self):
+            return {}
+
+    try:
+        _set_db_data(Span(), DummyBackend())
+    except TypeError:
+        pytest.fail("A TypeError was raised")
+
+
 @pytest.mark.parametrize(
     "transaction_style,client_url,expected_transaction,expected_source,expected_response",
     [