We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
internal: allow Sentry DSN to be configured by end user
## Details

The existing Sentry instrumentation is fairly valuable for performance optimization purposes, but having Sentry hard-coded to report to `https://sentry.beryju.org` makes it a pain to override during local development.

## Changes

### New Features

* Allow the Sentry DSN to be overridden via `AUTHENTIK_ERROR_REPORTING__SENTRY_DSN`.

## Additional

I updated the configuration docs to list the new configuration setting, and _slightly_ tweaked the wording of the surrounding documentation to explain the dichotomy between the default DSN -- useful for debugging issues in collaboration with authentik devs, anonymous performance data, etc -- and specifying a custom one.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `authentik/lib/sentry.py`
Content:
```
1 """authentik sentry integration"""
2 from asyncio.exceptions import CancelledError
3 from typing import Any, Optional
4 
5 from billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError
6 from celery.exceptions import CeleryError
7 from channels.middleware import BaseMiddleware
8 from channels_redis.core import ChannelFull
9 from django.conf import settings
10 from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError
11 from django.db import DatabaseError, InternalError, OperationalError, ProgrammingError
12 from django.http.response import Http404
13 from django_redis.exceptions import ConnectionInterrupted
14 from docker.errors import DockerException
15 from h11 import LocalProtocolError
16 from ldap3.core.exceptions import LDAPException
17 from redis.exceptions import ConnectionError as RedisConnectionError
18 from redis.exceptions import RedisError, ResponseError
19 from rest_framework.exceptions import APIException
20 from sentry_sdk import HttpTransport, Hub
21 from sentry_sdk import init as sentry_sdk_init
22 from sentry_sdk.api import set_tag
23 from sentry_sdk.integrations.celery import CeleryIntegration
24 from sentry_sdk.integrations.django import DjangoIntegration
25 from sentry_sdk.integrations.redis import RedisIntegration
26 from sentry_sdk.integrations.threading import ThreadingIntegration
27 from sentry_sdk.tracing import Transaction
28 from structlog.stdlib import get_logger
29 from websockets.exceptions import WebSocketException
30 
31 from authentik import __version__, get_build_hash
32 from authentik.lib.config import CONFIG
33 from authentik.lib.utils.http import authentik_user_agent
34 from authentik.lib.utils.reflection import class_to_path, get_env
35 
36 LOGGER = get_logger()
37 SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
38 
39 
40 class SentryWSMiddleware(BaseMiddleware):
41     """Sentry Websocket middleweare to set the transaction name based on
42     consumer class path"""
43 
44     async def __call__(self, scope, receive, send):
45         transaction: Optional[Transaction] = Hub.current.scope.transaction
46         class_path = class_to_path(self.inner.consumer_class)
47         if transaction:
48             transaction.name = class_path
49         return await self.inner(scope, receive, send)
50 
51 
52 class SentryIgnoredException(Exception):
53     """Base Class for all errors that are suppressed, and not sent to sentry."""
54 
55 
56 class SentryTransport(HttpTransport):
57     """Custom sentry transport with custom user-agent"""
58 
59     def __init__(self, options: dict[str, Any]) -> None:
60         super().__init__(options)
61         self._auth = self.parsed_dsn.to_auth(authentik_user_agent())
62 
63 
64 def sentry_init(**sentry_init_kwargs):
65     """Configure sentry SDK"""
66     sentry_env = CONFIG.y("error_reporting.environment", "customer")
67     kwargs = {
68         "environment": sentry_env,
69         "send_default_pii": CONFIG.y_bool("error_reporting.send_pii", False),
70     }
71     kwargs.update(**sentry_init_kwargs)
72     # pylint: disable=abstract-class-instantiated
73     sentry_sdk_init(
74         dsn=SENTRY_DSN,
75         integrations=[
76             DjangoIntegration(transaction_style="function_name"),
77             CeleryIntegration(),
78             RedisIntegration(),
79             ThreadingIntegration(propagate_hub=True),
80         ],
81         before_send=before_send,
82         traces_sampler=traces_sampler,
83         release=f"authentik@{__version__}",
84         transport=SentryTransport,
85         **kwargs,
86     )
87     set_tag("authentik.build_hash", get_build_hash("tagged"))
88     set_tag("authentik.env", get_env())
89     set_tag("authentik.component", "backend")
90 
91 
92 def traces_sampler(sampling_context: dict) -> float:
93     """Custom sampler to ignore certain routes"""
94     path = sampling_context.get("asgi_scope", {}).get("path", "")
95     # Ignore all healthcheck routes
96     if path.startswith("/-/health") or path.startswith("/-/metrics"):
97         return 0
98     return float(CONFIG.y("error_reporting.sample_rate", 0.1))
99 
100 
101 def before_send(event: dict, hint: dict) -> Optional[dict]:
102     """Check if error is database error, and ignore if so"""
103     # pylint: disable=no-name-in-module
104     from psycopg2.errors import Error
105 
106     ignored_classes = (
107         # Inbuilt types
108         KeyboardInterrupt,
109         ConnectionResetError,
110         OSError,
111         PermissionError,
112         # Django Errors
113         Error,
114         ImproperlyConfigured,
115         DatabaseError,
116         OperationalError,
117         InternalError,
118         ProgrammingError,
119         SuspiciousOperation,
120         ValidationError,
121         # Redis errors
122         RedisConnectionError,
123         ConnectionInterrupted,
124         RedisError,
125         ResponseError,
126         # websocket errors
127         ChannelFull,
128         WebSocketException,
129         LocalProtocolError,
130         # rest_framework error
131         APIException,
132         # celery errors
133         WorkerLostError,
134         CeleryError,
135         SoftTimeLimitExceeded,
136         # custom baseclass
137         SentryIgnoredException,
138         # ldap errors
139         LDAPException,
140         # Docker errors
141         DockerException,
142         # End-user errors
143         Http404,
144         # AsyncIO
145         CancelledError,
146     )
147     exc_value = None
148     if "exc_info" in hint:
149         _, exc_value, _ = hint["exc_info"]
150         if isinstance(exc_value, ignored_classes):
151             LOGGER.debug("dropping exception", exc=exc_value)
152             return None
153     if "logger" in event:
154         if event["logger"] in [
155             "kombu",
156             "asyncio",
157             "multiprocessing",
158             "django_redis",
159             "django.security.DisallowedHost",
160             "django_redis.cache",
161             "celery.backends.redis",
162             "celery.worker",
163             "paramiko.transport",
164         ]:
165             return None
166     LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))
167     if settings.DEBUG:
168         return None
169     return event
170
```

Path: `authentik/api/v3/config.py`
Content:
```
1 """core Configs API"""
2 from os import path
3 
4 from django.conf import settings
5 from django.db import models
6 from drf_spectacular.utils import extend_schema
7 from rest_framework.fields import (
8     BooleanField,
9     CharField,
10     ChoiceField,
11     FloatField,
12     IntegerField,
13     ListField,
14 )
15 from rest_framework.permissions import AllowAny
16 from rest_framework.request import Request
17 from rest_framework.response import Response
18 from rest_framework.views import APIView
19 
20 from authentik.core.api.utils import PassiveSerializer
21 from authentik.events.geo import GEOIP_READER
22 from authentik.lib.config import CONFIG
23 
24 
25 class Capabilities(models.TextChoices):
26     """Define capabilities which influence which APIs can/should be used"""
27 
28     CAN_SAVE_MEDIA = "can_save_media"
29     CAN_GEO_IP = "can_geo_ip"
30     CAN_IMPERSONATE = "can_impersonate"
31     CAN_DEBUG = "can_debug"
32 
33 
34 class ErrorReportingConfigSerializer(PassiveSerializer):
35     """Config for error reporting"""
36 
37     enabled = BooleanField(read_only=True)
38     environment = CharField(read_only=True)
39     send_pii = BooleanField(read_only=True)
40     traces_sample_rate = FloatField(read_only=True)
41 
42 
43 class ConfigSerializer(PassiveSerializer):
44     """Serialize authentik Config into DRF Object"""
45 
46     error_reporting = ErrorReportingConfigSerializer(required=True)
47     capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))
48 
49     cache_timeout = IntegerField(required=True)
50     cache_timeout_flows = IntegerField(required=True)
51     cache_timeout_policies = IntegerField(required=True)
52     cache_timeout_reputation = IntegerField(required=True)
53 
54 
55 class ConfigView(APIView):
56     """Read-only view set that returns the current session's Configs"""
57 
58     permission_classes = [AllowAny]
59 
60     def get_capabilities(self) -> list[Capabilities]:
61         """Get all capabilities this server instance supports"""
62         caps = []
63         deb_test = settings.DEBUG or settings.TEST
64         if path.ismount(settings.MEDIA_ROOT) or deb_test:
65             caps.append(Capabilities.CAN_SAVE_MEDIA)
66         if GEOIP_READER.enabled:
67             caps.append(Capabilities.CAN_GEO_IP)
68         if CONFIG.y_bool("impersonation"):
69             caps.append(Capabilities.CAN_IMPERSONATE)
70         if settings.DEBUG:
71             caps.append(Capabilities.CAN_DEBUG)
72         return caps
73 
74     def get_config(self) -> ConfigSerializer:
75         """Get Config"""
76         return ConfigSerializer(
77             {
78                 "error_reporting": {
79                     "enabled": CONFIG.y("error_reporting.enabled"),
80                     "environment": CONFIG.y("error_reporting.environment"),
81                     "send_pii": CONFIG.y("error_reporting.send_pii"),
82                     "traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)),
83                 },
84                 "capabilities": self.get_capabilities(),
85                 "cache_timeout": int(CONFIG.y("redis.cache_timeout")),
86                 "cache_timeout_flows": int(CONFIG.y("redis.cache_timeout_flows")),
87                 "cache_timeout_policies": int(CONFIG.y("redis.cache_timeout_policies")),
88                 "cache_timeout_reputation": int(CONFIG.y("redis.cache_timeout_reputation")),
89             }
90         )
91 
92     @extend_schema(responses={200: ConfigSerializer(many=False)})
93     def get(self, request: Request) -> Response:
94         """Retrieve public configuration options"""
95         return Response(self.get_config().data)
96
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/authentik/api/v3/config.py b/authentik/api/v3/config.py
--- a/authentik/api/v3/config.py
+++ b/authentik/api/v3/config.py
@@ -35,6 +35,7 @@
     """Config for error reporting"""
 
     enabled = BooleanField(read_only=True)
+    sentry_dsn = CharField(read_only=True)
     environment = CharField(read_only=True)
     send_pii = BooleanField(read_only=True)
     traces_sample_rate = FloatField(read_only=True)
@@ -77,6 +78,7 @@
             {
                 "error_reporting": {
                     "enabled": CONFIG.y("error_reporting.enabled"),
+                    "sentry_dsn": CONFIG.y("error_reporting.sentry_dsn"),
                     "environment": CONFIG.y("error_reporting.environment"),
                     "send_pii": CONFIG.y("error_reporting.send_pii"),
                     "traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)),
diff --git a/authentik/lib/sentry.py b/authentik/lib/sentry.py
--- a/authentik/lib/sentry.py
+++ b/authentik/lib/sentry.py
@@ -34,7 +34,6 @@
 from authentik.lib.utils.reflection import class_to_path, get_env
 
 LOGGER = get_logger()
-SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
 
 
 class SentryWSMiddleware(BaseMiddleware):
@@ -71,7 +70,7 @@
     kwargs.update(**sentry_init_kwargs)
     # pylint: disable=abstract-class-instantiated
     sentry_sdk_init(
-        dsn=SENTRY_DSN,
+        dsn=CONFIG.y("error_reporting.sentry_dsn"),
         integrations=[
             DjangoIntegration(transaction_style="function_name"),
             CeleryIntegration(),

{"golden_diff": "diff --git a/authentik/api/v3/config.py b/authentik/api/v3/config.py\n--- a/authentik/api/v3/config.py\n+++ b/authentik/api/v3/config.py\n@@ -35,6 +35,7 @@\n     \"\"\"Config for error reporting\"\"\"\n \n     enabled = BooleanField(read_only=True)\n+    sentry_dsn = CharField(read_only=True)\n     environment = CharField(read_only=True)\n     send_pii = BooleanField(read_only=True)\n     traces_sample_rate = FloatField(read_only=True)\n@@ -77,6 +78,7 @@\n             {\n                 \"error_reporting\": {\n                     \"enabled\": CONFIG.y(\"error_reporting.enabled\"),\n+                    \"sentry_dsn\": CONFIG.y(\"error_reporting.sentry_dsn\"),\n                     \"environment\": CONFIG.y(\"error_reporting.environment\"),\n                     \"send_pii\": CONFIG.y(\"error_reporting.send_pii\"),\n                     \"traces_sample_rate\": float(CONFIG.y(\"error_reporting.sample_rate\", 0.4)),\ndiff --git a/authentik/lib/sentry.py b/authentik/lib/sentry.py\n--- a/authentik/lib/sentry.py\n+++ b/authentik/lib/sentry.py\n@@ -34,7 +34,6 @@\n from authentik.lib.utils.reflection import class_to_path, get_env\n \n LOGGER = get_logger()\n-SENTRY_DSN = \"https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8\"\n \n \n class SentryWSMiddleware(BaseMiddleware):\n@@ -71,7 +70,7 @@\n     kwargs.update(**sentry_init_kwargs)\n     # pylint: disable=abstract-class-instantiated\n     sentry_sdk_init(\n-        dsn=SENTRY_DSN,\n+        dsn=CONFIG.y(\"error_reporting.sentry_dsn\"),\n         integrations=[\n             DjangoIntegration(transaction_style=\"function_name\"),\n             CeleryIntegration(),\n", "issue": "internal: allow Sentry DSN to be configured by end user\n## Details\r\n\r\nThe existing Sentry instrumentation is fairly valuable for performance optimization purposes, but having Sentry hard-coded to report to `https://sentry.beryju.org` makes it a pain to override during local development.\r\n\r\n## Changes\r\n\r\n### New Features\r\n\r\n* Allow the Sentry DSN to be overridden via `AUTHENTIK_ERROR_REPORTING__SENTRY_DSN`.\r\n\r\n## Additional\r\n\r\nI updated the configuration docs to list the new configuration setting, and _slightly_ tweaked the wording of the surrounding documentation to explain the dichotomy between the default DSN -- useful for debugging issues in collaboration with authentik devs, anonymous performance data, etc -- and specifying a custom one.\n", "before_files": [{"content": "\"\"\"authentik sentry integration\"\"\"\nfrom asyncio.exceptions import CancelledError\nfrom typing import Any, Optional\n\nfrom billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError\nfrom celery.exceptions import CeleryError\nfrom channels.middleware import BaseMiddleware\nfrom channels_redis.core import ChannelFull\nfrom django.conf import settings\nfrom django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError\nfrom django.db import DatabaseError, InternalError, OperationalError, ProgrammingError\nfrom django.http.response import Http404\nfrom django_redis.exceptions import ConnectionInterrupted\nfrom docker.errors import DockerException\nfrom h11 import LocalProtocolError\nfrom ldap3.core.exceptions import LDAPException\nfrom redis.exceptions import ConnectionError as RedisConnectionError\nfrom redis.exceptions import RedisError, ResponseError\nfrom rest_framework.exceptions import APIException\nfrom sentry_sdk import HttpTransport, Hub\nfrom sentry_sdk import init as sentry_sdk_init\nfrom sentry_sdk.api import set_tag\nfrom sentry_sdk.integrations.celery import CeleryIntegration\nfrom sentry_sdk.integrations.django import DjangoIntegration\nfrom sentry_sdk.integrations.redis import RedisIntegration\nfrom sentry_sdk.integrations.threading import ThreadingIntegration\nfrom sentry_sdk.tracing import Transaction\nfrom structlog.stdlib import get_logger\nfrom websockets.exceptions import WebSocketException\n\nfrom authentik import __version__, get_build_hash\nfrom authentik.lib.config import CONFIG\nfrom authentik.lib.utils.http import authentik_user_agent\nfrom authentik.lib.utils.reflection import class_to_path, get_env\n\nLOGGER = get_logger()\nSENTRY_DSN = \"https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8\"\n\n\nclass SentryWSMiddleware(BaseMiddleware):\n    \"\"\"Sentry Websocket middleweare to set the transaction name based on\n    consumer class path\"\"\"\n\n    async def __call__(self, scope, receive, send):\n        transaction: Optional[Transaction] = Hub.current.scope.transaction\n        class_path = class_to_path(self.inner.consumer_class)\n        if transaction:\n            transaction.name = class_path\n        return await self.inner(scope, receive, send)\n\n\nclass SentryIgnoredException(Exception):\n    \"\"\"Base Class for all errors that are suppressed, and not sent to sentry.\"\"\"\n\n\nclass SentryTransport(HttpTransport):\n    \"\"\"Custom sentry transport with custom user-agent\"\"\"\n\n    def __init__(self, options: dict[str, Any]) -> None:\n        super().__init__(options)\n        self._auth = self.parsed_dsn.to_auth(authentik_user_agent())\n\n\ndef sentry_init(**sentry_init_kwargs):\n    \"\"\"Configure sentry SDK\"\"\"\n    sentry_env = CONFIG.y(\"error_reporting.environment\", \"customer\")\n    kwargs = {\n        \"environment\": sentry_env,\n        \"send_default_pii\": CONFIG.y_bool(\"error_reporting.send_pii\", False),\n    }\n    kwargs.update(**sentry_init_kwargs)\n    # pylint: disable=abstract-class-instantiated\n    sentry_sdk_init(\n        dsn=SENTRY_DSN,\n        integrations=[\n            DjangoIntegration(transaction_style=\"function_name\"),\n            CeleryIntegration(),\n            RedisIntegration(),\n            ThreadingIntegration(propagate_hub=True),\n        ],\n        before_send=before_send,\n        traces_sampler=traces_sampler,\n        release=f\"authentik@{__version__}\",\n        transport=SentryTransport,\n        **kwargs,\n    )\n    set_tag(\"authentik.build_hash\", get_build_hash(\"tagged\"))\n    set_tag(\"authentik.env\", get_env())\n    set_tag(\"authentik.component\", \"backend\")\n\n\ndef traces_sampler(sampling_context: dict) -> float:\n    \"\"\"Custom sampler to ignore certain routes\"\"\"\n    path = sampling_context.get(\"asgi_scope\", {}).get(\"path\", \"\")\n    # Ignore all healthcheck routes\n    if path.startswith(\"/-/health\") or path.startswith(\"/-/metrics\"):\n        return 0\n    return float(CONFIG.y(\"error_reporting.sample_rate\", 0.1))\n\n\ndef before_send(event: dict, hint: dict) -> Optional[dict]:\n    \"\"\"Check if error is database error, and ignore if so\"\"\"\n    # pylint: disable=no-name-in-module\n    from psycopg2.errors import Error\n\n    ignored_classes = (\n        # Inbuilt types\n        KeyboardInterrupt,\n        ConnectionResetError,\n        OSError,\n        PermissionError,\n        # Django Errors\n        Error,\n        ImproperlyConfigured,\n        DatabaseError,\n        OperationalError,\n        InternalError,\n        ProgrammingError,\n        SuspiciousOperation,\n        ValidationError,\n        # Redis errors\n        RedisConnectionError,\n        ConnectionInterrupted,\n        RedisError,\n        ResponseError,\n        # websocket errors\n        ChannelFull,\n        WebSocketException,\n        LocalProtocolError,\n        # rest_framework error\n        APIException,\n        # celery errors\n        WorkerLostError,\n        CeleryError,\n        SoftTimeLimitExceeded,\n        # custom baseclass\n        SentryIgnoredException,\n        # ldap errors\n        LDAPException,\n        # Docker errors\n        DockerException,\n        # End-user errors\n        Http404,\n        # AsyncIO\n        CancelledError,\n    )\n    exc_value = None\n    if \"exc_info\" in hint:\n        _, exc_value, _ = hint[\"exc_info\"]\n        if isinstance(exc_value, ignored_classes):\n            LOGGER.debug(\"dropping exception\", exc=exc_value)\n            return None\n    if \"logger\" in event:\n        if event[\"logger\"] in [\n            \"kombu\",\n            \"asyncio\",\n            \"multiprocessing\",\n            \"django_redis\",\n            \"django.security.DisallowedHost\",\n            \"django_redis.cache\",\n            \"celery.backends.redis\",\n            \"celery.worker\",\n            \"paramiko.transport\",\n        ]:\n            return None\n    LOGGER.debug(\"sending event to sentry\", exc=exc_value, source_logger=event.get(\"logger\", None))\n    if settings.DEBUG:\n        return None\n    return event\n", "path": "authentik/lib/sentry.py"}, {"content": "\"\"\"core Configs API\"\"\"\nfrom os import path\n\nfrom django.conf import settings\nfrom django.db import models\nfrom drf_spectacular.utils import extend_schema\nfrom rest_framework.fields import (\n    BooleanField,\n    CharField,\n    ChoiceField,\n    FloatField,\n    IntegerField,\n    ListField,\n)\nfrom rest_framework.permissions import AllowAny\nfrom rest_framework.request import Request\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom authentik.core.api.utils import PassiveSerializer\nfrom authentik.events.geo import GEOIP_READER\nfrom authentik.lib.config import CONFIG\n\n\nclass Capabilities(models.TextChoices):\n    \"\"\"Define capabilities which influence which APIs can/should be used\"\"\"\n\n    CAN_SAVE_MEDIA = \"can_save_media\"\n    CAN_GEO_IP = \"can_geo_ip\"\n    CAN_IMPERSONATE = \"can_impersonate\"\n    CAN_DEBUG = \"can_debug\"\n\n\nclass ErrorReportingConfigSerializer(PassiveSerializer):\n    \"\"\"Config for error reporting\"\"\"\n\n    enabled = BooleanField(read_only=True)\n    environment = CharField(read_only=True)\n    send_pii = BooleanField(read_only=True)\n    traces_sample_rate = FloatField(read_only=True)\n\n\nclass ConfigSerializer(PassiveSerializer):\n    \"\"\"Serialize authentik Config into DRF Object\"\"\"\n\n    error_reporting = ErrorReportingConfigSerializer(required=True)\n    capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))\n\n    cache_timeout = IntegerField(required=True)\n    cache_timeout_flows = IntegerField(required=True)\n    cache_timeout_policies = IntegerField(required=True)\n    cache_timeout_reputation = IntegerField(required=True)\n\n\nclass ConfigView(APIView):\n    \"\"\"Read-only view set that returns the current session's Configs\"\"\"\n\n    permission_classes = [AllowAny]\n\n    def get_capabilities(self) -> list[Capabilities]:\n        \"\"\"Get all capabilities this server instance supports\"\"\"\n        caps = []\n        deb_test = settings.DEBUG or settings.TEST\n        if path.ismount(settings.MEDIA_ROOT) or deb_test:\n            caps.append(Capabilities.CAN_SAVE_MEDIA)\n        if GEOIP_READER.enabled:\n            caps.append(Capabilities.CAN_GEO_IP)\n        if CONFIG.y_bool(\"impersonation\"):\n            caps.append(Capabilities.CAN_IMPERSONATE)\n        if settings.DEBUG:\n            caps.append(Capabilities.CAN_DEBUG)\n        return caps\n\n    def get_config(self) -> ConfigSerializer:\n        \"\"\"Get Config\"\"\"\n        return ConfigSerializer(\n            {\n                \"error_reporting\": {\n                    \"enabled\": CONFIG.y(\"error_reporting.enabled\"),\n                    \"environment\": CONFIG.y(\"error_reporting.environment\"),\n                    \"send_pii\": CONFIG.y(\"error_reporting.send_pii\"),\n                    \"traces_sample_rate\": float(CONFIG.y(\"error_reporting.sample_rate\", 0.4)),\n                },\n                \"capabilities\": self.get_capabilities(),\n                \"cache_timeout\": int(CONFIG.y(\"redis.cache_timeout\")),\n                \"cache_timeout_flows\": int(CONFIG.y(\"redis.cache_timeout_flows\")),\n                \"cache_timeout_policies\": int(CONFIG.y(\"redis.cache_timeout_policies\")),\n                \"cache_timeout_reputation\": int(CONFIG.y(\"redis.cache_timeout_reputation\")),\n            }\n        )\n\n    @extend_schema(responses={200: ConfigSerializer(many=False)})\n    def get(self, request: Request) -> Response:\n        \"\"\"Retrieve public configuration options\"\"\"\n        return Response(self.get_config().data)\n", "path": "authentik/api/v3/config.py"}], "after_files": [{"content": "\"\"\"authentik sentry integration\"\"\"\nfrom asyncio.exceptions import CancelledError\nfrom typing import Any, Optional\n\nfrom billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError\nfrom celery.exceptions import CeleryError\nfrom channels.middleware import BaseMiddleware\nfrom channels_redis.core import ChannelFull\nfrom django.conf import settings\nfrom django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError\nfrom django.db import DatabaseError, InternalError, OperationalError, ProgrammingError\nfrom django.http.response import Http404\nfrom django_redis.exceptions import ConnectionInterrupted\nfrom docker.errors import DockerException\nfrom h11 import LocalProtocolError\nfrom ldap3.core.exceptions import LDAPException\nfrom redis.exceptions import ConnectionError as RedisConnectionError\nfrom redis.exceptions import RedisError, ResponseError\nfrom rest_framework.exceptions import APIException\nfrom sentry_sdk import HttpTransport, Hub\nfrom sentry_sdk import init as sentry_sdk_init\nfrom sentry_sdk.api import set_tag\nfrom sentry_sdk.integrations.celery import CeleryIntegration\nfrom sentry_sdk.integrations.django import DjangoIntegration\nfrom sentry_sdk.integrations.redis import RedisIntegration\nfrom sentry_sdk.integrations.threading import ThreadingIntegration\nfrom sentry_sdk.tracing import Transaction\nfrom structlog.stdlib import get_logger\nfrom websockets.exceptions import WebSocketException\n\nfrom authentik import __version__, get_build_hash\nfrom authentik.lib.config import CONFIG\nfrom authentik.lib.utils.http import authentik_user_agent\nfrom authentik.lib.utils.reflection import class_to_path, get_env\n\nLOGGER = get_logger()\n\n\nclass SentryWSMiddleware(BaseMiddleware):\n    \"\"\"Sentry Websocket middleweare to set the transaction name based on\n    consumer class path\"\"\"\n\n    async def __call__(self, scope, receive, send):\n        transaction: Optional[Transaction] = Hub.current.scope.transaction\n        class_path = class_to_path(self.inner.consumer_class)\n        if transaction:\n            transaction.name = class_path\n        return await self.inner(scope, receive, send)\n\n\nclass SentryIgnoredException(Exception):\n    \"\"\"Base Class for all errors that are suppressed, and not sent to sentry.\"\"\"\n\n\nclass SentryTransport(HttpTransport):\n    \"\"\"Custom sentry transport with custom user-agent\"\"\"\n\n    def __init__(self, options: dict[str, Any]) -> None:\n        super().__init__(options)\n        self._auth = self.parsed_dsn.to_auth(authentik_user_agent())\n\n\ndef sentry_init(**sentry_init_kwargs):\n    \"\"\"Configure sentry SDK\"\"\"\n    sentry_env = CONFIG.y(\"error_reporting.environment\", \"customer\")\n    kwargs = {\n        \"environment\": sentry_env,\n        \"send_default_pii\": CONFIG.y_bool(\"error_reporting.send_pii\", False),\n    }\n    kwargs.update(**sentry_init_kwargs)\n    # pylint: disable=abstract-class-instantiated\n    sentry_sdk_init(\n        dsn=CONFIG.y(\"error_reporting.sentry_dsn\"),\n        integrations=[\n            DjangoIntegration(transaction_style=\"function_name\"),\n            CeleryIntegration(),\n            RedisIntegration(),\n            ThreadingIntegration(propagate_hub=True),\n        ],\n        before_send=before_send,\n        traces_sampler=traces_sampler,\n        release=f\"authentik@{__version__}\",\n        transport=SentryTransport,\n        **kwargs,\n    )\n    set_tag(\"authentik.build_hash\", get_build_hash(\"tagged\"))\n    set_tag(\"authentik.env\", get_env())\n    set_tag(\"authentik.component\", \"backend\")\n\n\ndef traces_sampler(sampling_context: dict) -> float:\n    \"\"\"Custom sampler to ignore certain routes\"\"\"\n    path = sampling_context.get(\"asgi_scope\", {}).get(\"path\", \"\")\n    # Ignore all healthcheck routes\n    if path.startswith(\"/-/health\") or path.startswith(\"/-/metrics\"):\n        return 0\n    return float(CONFIG.y(\"error_reporting.sample_rate\", 0.1))\n\n\ndef before_send(event: dict, hint: dict) -> Optional[dict]:\n    \"\"\"Check if error is database error, and ignore if so\"\"\"\n    # pylint: disable=no-name-in-module\n    from psycopg2.errors import Error\n\n    ignored_classes = (\n        # Inbuilt types\n        KeyboardInterrupt,\n        ConnectionResetError,\n        OSError,\n        PermissionError,\n        # Django Errors\n        Error,\n        ImproperlyConfigured,\n        DatabaseError,\n        OperationalError,\n        InternalError,\n        ProgrammingError,\n        SuspiciousOperation,\n        ValidationError,\n        # Redis errors\n        RedisConnectionError,\n        ConnectionInterrupted,\n        RedisError,\n        ResponseError,\n        # websocket errors\n        ChannelFull,\n        WebSocketException,\n        LocalProtocolError,\n        # rest_framework error\n        APIException,\n        # celery errors\n        WorkerLostError,\n        CeleryError,\n        SoftTimeLimitExceeded,\n        # custom baseclass\n        SentryIgnoredException,\n        # ldap errors\n        LDAPException,\n        # Docker errors\n        DockerException,\n        # End-user errors\n        Http404,\n        # AsyncIO\n        CancelledError,\n    )\n    exc_value = None\n    if \"exc_info\" in hint:\n        _, exc_value, _ = hint[\"exc_info\"]\n        if isinstance(exc_value, ignored_classes):\n            LOGGER.debug(\"dropping exception\", exc=exc_value)\n            return None\n    if \"logger\" in event:\n        if event[\"logger\"] in [\n            \"kombu\",\n            \"asyncio\",\n            \"multiprocessing\",\n            \"django_redis\",\n            \"django.security.DisallowedHost\",\n            \"django_redis.cache\",\n            \"celery.backends.redis\",\n            \"celery.worker\",\n            \"paramiko.transport\",\n        ]:\n            return None\n    LOGGER.debug(\"sending event to sentry\", exc=exc_value, source_logger=event.get(\"logger\", None))\n    if settings.DEBUG:\n        return None\n    return event\n", "path": "authentik/lib/sentry.py"}, {"content": "\"\"\"core Configs API\"\"\"\nfrom os import path\n\nfrom django.conf import settings\nfrom django.db import models\nfrom drf_spectacular.utils import extend_schema\nfrom rest_framework.fields import (\n    BooleanField,\n    CharField,\n    ChoiceField,\n    FloatField,\n    IntegerField,\n    ListField,\n)\nfrom rest_framework.permissions import AllowAny\nfrom rest_framework.request import Request\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom authentik.core.api.utils import PassiveSerializer\nfrom authentik.events.geo import GEOIP_READER\nfrom authentik.lib.config import CONFIG\n\n\nclass Capabilities(models.TextChoices):\n    \"\"\"Define capabilities which influence which APIs can/should be used\"\"\"\n\n    CAN_SAVE_MEDIA = \"can_save_media\"\n    CAN_GEO_IP = \"can_geo_ip\"\n    CAN_IMPERSONATE = \"can_impersonate\"\n    CAN_DEBUG = \"can_debug\"\n\n\nclass ErrorReportingConfigSerializer(PassiveSerializer):\n    \"\"\"Config for error reporting\"\"\"\n\n    enabled = BooleanField(read_only=True)\n    sentry_dsn = CharField(read_only=True)\n    environment = CharField(read_only=True)\n    send_pii = BooleanField(read_only=True)\n    traces_sample_rate = FloatField(read_only=True)\n\n\nclass ConfigSerializer(PassiveSerializer):\n    \"\"\"Serialize authentik Config into DRF Object\"\"\"\n\n    error_reporting = ErrorReportingConfigSerializer(required=True)\n    capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))\n\n    cache_timeout = IntegerField(required=True)\n    cache_timeout_flows = IntegerField(required=True)\n    cache_timeout_policies = IntegerField(required=True)\n    cache_timeout_reputation = IntegerField(required=True)\n\n\nclass ConfigView(APIView):\n    \"\"\"Read-only view set that returns the current session's Configs\"\"\"\n\n    permission_classes = [AllowAny]\n\n    def get_capabilities(self) -> list[Capabilities]:\n        \"\"\"Get all capabilities this server instance supports\"\"\"\n        caps = []\n        deb_test = settings.DEBUG or settings.TEST\n        if path.ismount(settings.MEDIA_ROOT) or deb_test:\n            caps.append(Capabilities.CAN_SAVE_MEDIA)\n        if GEOIP_READER.enabled:\n            caps.append(Capabilities.CAN_GEO_IP)\n        if CONFIG.y_bool(\"impersonation\"):\n            caps.append(Capabilities.CAN_IMPERSONATE)\n        if settings.DEBUG:\n            caps.append(Capabilities.CAN_DEBUG)\n        return caps\n\n    def get_config(self) -> ConfigSerializer:\n        \"\"\"Get Config\"\"\"\n        return ConfigSerializer(\n            {\n                \"error_reporting\": {\n                    \"enabled\": CONFIG.y(\"error_reporting.enabled\"),\n                    \"sentry_dsn\": CONFIG.y(\"error_reporting.sentry_dsn\"),\n                    \"environment\": CONFIG.y(\"error_reporting.environment\"),\n                    \"send_pii\": CONFIG.y(\"error_reporting.send_pii\"),\n                    \"traces_sample_rate\": float(CONFIG.y(\"error_reporting.sample_rate\", 0.4)),\n                },\n                \"capabilities\": self.get_capabilities(),\n                \"cache_timeout\": int(CONFIG.y(\"redis.cache_timeout\")),\n                \"cache_timeout_flows\": int(CONFIG.y(\"redis.cache_timeout_flows\")),\n                \"cache_timeout_policies\": int(CONFIG.y(\"redis.cache_timeout_policies\")),\n                \"cache_timeout_reputation\": int(CONFIG.y(\"redis.cache_timeout_reputation\")),\n            }\n        )\n\n    @extend_schema(responses={200: ConfigSerializer(many=False)})\n    def get(self, request: Request) -> Response:\n        \"\"\"Retrieve public configuration options\"\"\"\n        return Response(self.get_config().data)\n", "path": "authentik/api/v3/config.py"}]}