Repository: netbox-community/netbox

Issue:
Sentry SDK should be an optional package
### NetBox version

v3.6.0

### Feature type

Change to existing functionality

### Proposed functionality

NetBox supports [error reporting](https://docs.netbox.dev/en/stable/administration/error-reporting/#sentry) via integration with [Sentry](https://sentry.io/) as an optional feature, however [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) is currently a required package. It should be made optional. (This is similar to what we did for `boto3` in #12906.)

### Use case

This library is only needed if the Sentry integration is enabled.

### Database changes

N/A

### External dependencies

N/A


Discussion:

Resolving patch:
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -9,12 +9,14 @@
 from urllib.parse import urlencode, urlsplit
 
 import django
-import sentry_sdk
 from django.contrib.messages import constants as messages
 from django.core.exceptions import ImproperlyConfigured, ValidationError
 from django.core.validators import URLValidator
 from django.utils.encoding import force_str
-from sentry_sdk.integrations.django import DjangoIntegration
+try:
+    import sentry_sdk
+except ModuleNotFoundError:
+    pass
 
 from netbox.config import PARAMS
 from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
@@ -39,8 +41,6 @@
         f"NetBox requires Python 3.8 or later. (Currently installed: Python {platform.python_version()})"
     )
 
-DEFAULT_SENTRY_DSN = 'https://198cf560b29d4054ab8e583a1d10ea58@o1242133.ingest.sentry.io/6396485'
-
 #
 # Configuration import
 #
@@ -158,7 +158,7 @@
 SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
 SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
 SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
-SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN)
+SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
 SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
 SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
 SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
@@ -517,12 +517,12 @@ def _setting(name, default=None):
 #
 
 if SENTRY_ENABLED:
+    try:
+        from sentry_sdk.integrations.django import DjangoIntegration
+    except ModuleNotFoundError:
+        raise ImproperlyConfigured("SENTRY_ENABLED is True but the sentry-sdk package is not installed.")
     if not SENTRY_DSN:
         raise ImproperlyConfigured("SENTRY_ENABLED is True but SENTRY_DSN has not been defined.")
-    # If using the default DSN, force sampling rates
-    if SENTRY_DSN == DEFAULT_SENTRY_DSN:
-        SENTRY_SAMPLE_RATE = 1.0
-        SENTRY_TRACES_SAMPLE_RATE = 0
     # Initialize the SDK
     sentry_sdk.init(
         dsn=SENTRY_DSN,
@@ -537,9 +537,6 @@ def _setting(name, default=None):
     # Assign any configured tags
     for k, v in SENTRY_TAGS.items():
         sentry_sdk.set_tag(k, v)
-    # If using the default DSN, append a unique deployment ID tag for error correlation
-    if SENTRY_DSN == DEFAULT_SENTRY_DSN:
-        sentry_sdk.set_tag('netbox.deployment_id', DEPLOYMENT_ID)
 
 
 #
diff --git a/netbox/netbox/views/errors.py b/netbox/netbox/views/errors.py
--- a/netbox/netbox/views/errors.py
+++ b/netbox/netbox/views/errors.py
@@ -9,7 +9,6 @@
 from django.views.decorators.csrf import requires_csrf_token
 from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
 from django.views.generic import View
-from sentry_sdk import capture_message
 
 from netbox.plugins.utils import get_installed_plugins
 
@@ -34,7 +33,9 @@ def handler_404(request, exception):
     """
     Wrap Django's default 404 handler to enable Sentry reporting.
     """
-    capture_message("Page not found", level="error")
+    if settings.SENTRY_ENABLED:
+        from sentry_sdk import capture_message
+        capture_message("Page not found", level="error")
 
     return page_not_found(request, exception)
 


Test patch: