Repository: netbox-community/netbox

Issue:
Enable integration with Sentry for error reporting
### NetBox version

v3.2.2

### Feature type

New functionality

### Proposed functionality

Enable error reporting via [Sentry](https://sentry.io/). This will entail installation of the [Sentry SDK for Python](https://pypi.org/project/sentry-sdk/) as well as the introduction of several new configuration parameters:

* `SENTRY_ENABLED` - Master toggle for error reporting (disabled by default)
* `SENTRY_DSN` - This will default to the DSN designated for the NetBox project, but can be overridden by users who want to point NetBox to their own Sentry DSN
* `SENTRY_TAGS` - An optional dictionary of tags to set on error reports

When configured and enabled, NetBox will begin sending exception reports to the configured Sentry DSN. Implementing this functionality requires only calling `sentry_sdk.init()` with the appropriate parameters in `settings.py`; the Django integration takes care of everything else.

### Use case

There are two primary motivations for this integration:

* Enable NetBox users to easily collect and analyze their own errors via Sentry
* Enable the NetBox development team to collect and analyze errors from community members who have opted to enable error reporting

On the latter point, this will allow us to more proactively detect and resolve bugs and other issues. Even just enabling it for the [public demo](https://demo.netbox.dev/) is sure to yield some interesting reports.

### Database changes

None

### External dependencies

This will introduce [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) as a new dependency. (The SDK as well as Sentry itself are available as open source software.)


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
@@ -8,9 +8,11 @@
 import warnings
 from urllib.parse import urlsplit
 
+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 sentry_sdk.integrations.django import DjangoIntegration
 
 from netbox.config import PARAMS
 
@@ -113,6 +115,9 @@
 REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
 RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
 SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
+SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
+SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
+SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
 SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
 SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
 SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
@@ -428,6 +433,26 @@ def _setting(name, default=None):
 )
 
 
+#
+# Sentry
+#
+
+if SENTRY_ENABLED:
+    if not SENTRY_DSN:
+        raise ImproperlyConfigured("SENTRY_ENABLED is True but SENTRY_DSN has not been defined.")
+    sentry_sdk.init(
+        dsn=SENTRY_DSN,
+        release=VERSION,
+        integrations=[DjangoIntegration()],
+        traces_sample_rate=1.0,
+        send_default_pii=True,
+        http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
+        https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
+    )
+    for k, v in SENTRY_TAGS.items():
+        sentry_sdk.set_tag(k, v)
+
+
 #
 # Django social auth
 #
diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py
--- a/netbox/netbox/urls.py
+++ b/netbox/netbox/urls.py
@@ -100,4 +100,5 @@
     path('{}'.format(settings.BASE_PATH), include(_patterns))
 ]
 
+handler404 = 'netbox.views.handler_404'
 handler500 = 'netbox.views.server_error'
diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py
--- a/netbox/netbox/views/__init__.py
+++ b/netbox/netbox/views/__init__.py
@@ -2,7 +2,6 @@
 import sys
 
 from django.conf import settings
-from django.contrib.contenttypes.models import ContentType
 from django.core.cache import cache
 from django.db.models import F
 from django.http import HttpResponseServerError
@@ -11,9 +10,10 @@
 from django.template.exceptions import TemplateDoesNotExist
 from django.urls import reverse
 from django.views.decorators.csrf import requires_csrf_token
-from django.views.defaults import ERROR_500_TEMPLATE_NAME
+from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
 from django.views.generic import View
 from packaging import version
+from sentry_sdk import capture_message
 
 from circuits.models import Circuit, Provider
 from dcim.models import (
@@ -190,13 +190,21 @@ class StaticMediaFailureView(View):
     """
     Display a user-friendly error message with troubleshooting tips when a static media file fails to load.
     """
-
     def get(self, request):
         return render(request, 'media_failure.html', {
             'filename': request.GET.get('filename')
         })
 
 
+def handler_404(request, exception):
+    """
+    Wrap Django's default 404 handler to enable Sentry reporting.
+    """
+    capture_message("Page not found", level="error")
+
+    return page_not_found(request, exception)
+
+
 @requires_csrf_token
 def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
     """


Test patch: