Repository: cookiecutter/cookiecutter-django

Issue:
Migrate to the unified Sentry Python SDK
Sentry's raven library has a deprecation notice [in their documentation](https://docs.sentry.io/clients/python/)

> Deprecated Client
> 
> This SDK has been superseded by a new unified one. The documentation here is preserved for customers using the old client. For new projects have a look at the new client documentation: [Unified Python SDK](https://docs.sentry.io/quickstart/?platform=python).

We need to update our code [anywhere the raven library](https://github.com/pydanny/cookiecutter-django/search?q=raven&unscoped_q=raven) is used. We need to replace this with an equivalent from the new library. Relevant sections:

- [Django-specific](https://docs.sentry.io/platforms/python/django/)
- [logging](https://docs.sentry.io/platforms/python/logging/) 
- [Celery](https://docs.sentry.io/platforms/python/celery/).

We need to check which parts are required for us and implement them in the template [when Sentry is selected](https://github.com/pydanny/cookiecutter-django/search?q=use_sentry&unscoped_q=use_sentry).


Discussion:

Resolving patch:
diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py
--- a/{{cookiecutter.project_slug}}/config/settings/production.py
+++ b/{{cookiecutter.project_slug}}/config/settings/production.py
@@ -1,6 +1,14 @@
 {% if cookiecutter.use_sentry == 'y' -%}
 import logging
 
+import sentry_sdk
+
+from sentry_sdk.integrations.django import DjangoIntegration
+from sentry_sdk.integrations.logging import LoggingIntegration
+{%- if cookiecutter.use_celery == 'y' %}
+from sentry_sdk.integrations.celery import CeleryIntegration
+{% endif %}
+
 {% endif -%}
 from .base import *  # noqa
 from .base import env
@@ -175,7 +183,6 @@ class MediaRootS3Boto3Storage(S3Boto3Storage):
 COMPRESS_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
 # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL
 COMPRESS_URL = STATIC_URL
-
 {% endif %}
 {%- if cookiecutter.use_whitenoise == 'n' -%}
 # Collectfast
@@ -183,26 +190,20 @@ class MediaRootS3Boto3Storage(S3Boto3Storage):
 # https://github.com/antonagestam/collectfast#installation
 INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS  # noqa F405
 AWS_PRELOAD_METADATA = True
-
 {% endif %}
-{%- if cookiecutter.use_sentry == 'y' -%}
-# raven
-# ------------------------------------------------------------------------------
-# https://docs.sentry.io/clients/python/integrations/django/
-INSTALLED_APPS += ["raven.contrib.django.raven_compat"]  # noqa F405
-MIDDLEWARE = ["raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware"] + MIDDLEWARE
-
-# Sentry
+# LOGGING
 # ------------------------------------------------------------------------------
-SENTRY_DSN = env("SENTRY_DSN")
-SENTRY_CLIENT = env("DJANGO_SENTRY_CLIENT", default="raven.contrib.django.raven_compat.DjangoClient")
+# https://docs.djangoproject.com/en/dev/ref/settings/#logging
+# See https://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+{% if cookiecutter.use_sentry == 'n' -%}
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error when DEBUG=False.
 LOGGING = {
     "version": 1,
-    "disable_existing_loggers": True,
-    "root": {
-        "level": "WARNING",
-        "handlers": ["sentry"],
-    },
+    "disable_existing_loggers": False,
+    "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
     "formatters": {
         "verbose": {
             "format": "%(levelname)s %(asctime)s %(module)s "
@@ -210,9 +211,10 @@ class MediaRootS3Boto3Storage(S3Boto3Storage):
         }
     },
     "handlers": {
-        "sentry": {
+        "mail_admins": {
             "level": "ERROR",
-            "class": "raven.contrib.django.raven_compat.handlers.SentryHandler",
+            "filters": ["require_debug_false"],
+            "class": "django.utils.log.AdminEmailHandler",
         },
         "console": {
             "level": "DEBUG",
@@ -221,47 +223,22 @@ class MediaRootS3Boto3Storage(S3Boto3Storage):
         },
     },
     "loggers": {
-        "django.db.backends": {
+        "django.request": {
+            "handlers": ["mail_admins"],
             "level": "ERROR",
-            "handlers": ["console"],
-            "propagate": False,
-        },
-        "raven": {
-            "level": "DEBUG",
-            "handlers": ["console"],
-            "propagate": False,
-        },
-        "sentry.errors": {
-            "level": "DEBUG",
-            "handlers": ["console"],
-            "propagate": False,
+            "propagate": True,
         },
         "django.security.DisallowedHost": {
             "level": "ERROR",
-            "handlers": ["console", "sentry"],
-            "propagate": False,
+            "handlers": ["console", "mail_admins"],
+            "propagate": True,
         },
     },
 }
-
-SENTRY_CELERY_LOGLEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO)
-RAVEN_CONFIG = {
-    "dsn": SENTRY_DSN
-}
-
-{%- else %}
-# LOGGING
-# ------------------------------------------------------------------------------
-# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
-# A sample logging configuration. The only tangible logging
-# performed by this configuration is to send an email to
-# the site admins on every HTTP 500 error when DEBUG=False.
-# See https://docs.djangoproject.com/en/dev/topics/logging for
-# more details on how to customize your logging configuration.
+{% else %}
 LOGGING = {
     "version": 1,
-    "disable_existing_loggers": False,
-    "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
+    "disable_existing_loggers": True,
     "formatters": {
         "verbose": {
             "format": "%(levelname)s %(asctime)s %(module)s "
@@ -269,31 +246,46 @@ class MediaRootS3Boto3Storage(S3Boto3Storage):
         }
     },
     "handlers": {
-        "mail_admins": {
-            "level": "ERROR",
-            "filters": ["require_debug_false"],
-            "class": "django.utils.log.AdminEmailHandler",
-        },
         "console": {
             "level": "DEBUG",
             "class": "logging.StreamHandler",
             "formatter": "verbose",
-        },
+        }
     },
     "loggers": {
-        "django.request": {
-            "handlers": ["mail_admins"],
+        "django.db.backends": {
             "level": "ERROR",
-            "propagate": True,
+            "handlers": ["console"],
+            "propagate": False,
         },
+        # Errors logged by the SDK itself
+        "sentry_sdk": {"level": "ERROR", "handlers": ["console"], "propagate": False},
         "django.security.DisallowedHost": {
             "level": "ERROR",
-            "handlers": ["console", "mail_admins"],
-            "propagate": True,
+            "handlers": ["console"],
+            "propagate": False,
         },
     },
 }
 
+# Sentry
+# ------------------------------------------------------------------------------
+SENTRY_DSN = env("SENTRY_DSN")
+SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO)
+
+sentry_logging = LoggingIntegration(
+    level=SENTRY_LOG_LEVEL,  # Capture info and above as breadcrumbs
+    event_level=None,  # Send no events from log messages
+)
+
+{%- if cookiecutter.use_celery == 'y' %}
+sentry_sdk.init(
+    dsn=SENTRY_DSN,
+    integrations=[sentry_logging, DjangoIntegration(), CeleryIntegration()],
+)
+{% else %}
+sentry_sdk.init(dsn=SENTRY_DSN, integrations=[sentry_logging, DjangoIntegration()])
+{% endif -%}
 {% endif %}
 # Your stuff...
 # ------------------------------------------------------------------------------
diff --git a/{{cookiecutter.project_slug}}/config/wsgi.py b/{{cookiecutter.project_slug}}/config/wsgi.py
--- a/{{cookiecutter.project_slug}}/config/wsgi.py
+++ b/{{cookiecutter.project_slug}}/config/wsgi.py
@@ -24,10 +24,6 @@
     os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
 )
 sys.path.append(os.path.join(app_path, "{{ cookiecutter.project_slug }}"))
-{% if cookiecutter.use_sentry == 'y' -%}
-if os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.production":
-    from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
-{%- endif %}
 # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
 # if running multiple sites in the same mod_wsgi process. To fix this, use
 # mod_wsgi daemon mode with each site in its own daemon process, or use
@@ -38,10 +34,6 @@
 # file. This includes Django's development server, if the WSGI_APPLICATION
 # setting points here.
 application = get_wsgi_application()
-{% if cookiecutter.use_sentry == 'y' -%}
-if os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.production":
-    application = Sentry(application)
-{%- endif %}
 # Apply WSGI middleware here.
 # from helloworld.wsgi import HelloWorldApplication
 # application = HelloWorldApplication(application)
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/taskapp/celery.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/taskapp/celery.py
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/taskapp/celery.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/taskapp/celery.py
@@ -27,28 +27,6 @@ class CeleryAppConfig(AppConfig):
     def ready(self):
         installed_apps = [app_config.name for app_config in apps.get_app_configs()]
         app.autodiscover_tasks(lambda: installed_apps, force=True)
-        {%- if cookiecutter.use_sentry == 'y' %}
-
-        if hasattr(settings, 'RAVEN_CONFIG'):
-            # Celery signal registration
-{% if cookiecutter.use_pycharm == 'y' -%}
-	    # Since raven is required in production only,
-            # imports might (most surely will) be wiped out
-            # during PyCharm code clean up started
-            # in other environments.
-            # @formatter:off
-{%- endif %}
-            from raven import Client as RavenClient
-            from raven.contrib.celery import register_signal as raven_register_signal
-            from raven.contrib.celery import register_logger_signal as raven_register_logger_signal
-{% if cookiecutter.use_pycharm == 'y' -%}
-            # @formatter:on
-{%- endif %}
-
-            raven_client = RavenClient(dsn=settings.RAVEN_CONFIG['dsn'])
-            raven_register_logger_signal(raven_client)
-            raven_register_signal(raven_client)
-        {%- endif %}
 
 
 @app.task(bind=True)


Test patch: