From 8bdf17685af92ce10b21becc2e2fe5c8c78a4d25 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 30 Mar 2023 13:51:34 +0200 Subject: [PATCH] Make sentry errors more useful --- server/api/urls.py | 31 +-- server/core/middleware.py | 52 +++-- server/core/settings.py | 422 +++++++++++++++++++------------------- server/core/views.py | 41 +++- 4 files changed, 298 insertions(+), 248 deletions(-) diff --git a/server/api/urls.py b/server/api/urls.py index cd6718d5..6abfc1b7 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -2,24 +2,31 @@ from django.conf import settings from django.conf.urls import url from django.urls import include from django.views.decorators.csrf import csrf_exempt -from graphene_django.views import GraphQLView from api.schema_public import schema -from core.views import PrivateGraphQLView +from core.views import SentryGraphQLView, PrivateGraphQLView -app_name = 'api' +app_name = "api" urlpatterns = [ - url(r'^graphql-public', csrf_exempt(GraphQLView.as_view(schema=schema))), - url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())), - + url(r"^graphql-public", csrf_exempt(SentryGraphQLView.as_view(schema=schema))), + url(r"^graphql", csrf_exempt(PrivateGraphQLView.as_view())), # oauth - url(r'^oauth/', include('oauth.urls', namespace="oauth")), + url(r"^oauth/", include("oauth.urls", namespace="oauth")), ] if settings.DEBUG: - urlpatterns += [url(r'^graphiql-public', csrf_exempt(GraphQLView.as_view(schema=schema, graphiql=True, - pretty=True)))] - urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))] - - + urlpatterns += [ + url( + r"^graphiql-public", + csrf_exempt( + SentryGraphQLView.as_view(schema=schema, graphiql=True, pretty=True) + ), + ) + ] + urlpatterns += [ + url( + r"^graphiql", + csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)), + ) + ] diff --git a/server/core/middleware.py b/server/core/middleware.py index 11d31c6f..6e80e6e9 100644 --- a/server/core/middleware.py +++ b/server/core/middleware.py @@ -4,6 +4,8 @@ from django.conf import settings from django.http import Http404, HttpResponsePermanentRedirect from django.shortcuts import redirect from django.utils.deprecation import MiddlewareMixin +from graphene import ResolveInfo +from sentry_sdk import capture_exception try: @@ -15,25 +17,25 @@ _thread_locals = local() def get_current_request(): - """ returns the request object for this thread """ + """returns the request object for this thread""" return getattr(_thread_locals, "request", None) def get_current_user(): - """ returns the current user, if exist, otherwise returns None """ + """returns the current user, if exist, otherwise returns None""" request = get_current_request() if request: return getattr(request, "user", None) class ThreadLocalMiddleware(MiddlewareMixin): - """ Simple middleware that adds the request object in thread local storage.""" + """Simple middleware that adds the request object in thread local storage.""" def process_request(self, request): _thread_locals.request = request def process_response(self, request, response): - if hasattr(_thread_locals, 'request'): + if hasattr(_thread_locals, "request"): del _thread_locals.request return response @@ -42,9 +44,14 @@ class CommonRedirectMiddleware(MiddlewareMixin): """ redirects common bad requests or missing images """ + DEFAULT_REDIRECTS = [ - (re.compile("(?i)(.+\.php|.+wp-admin.+|.+\.htm|\/wordpress\/|\/wp\/|\/mm5\/|\/wp-content\/)"), - 'http://example.org/'), + ( + re.compile( + "(?i)(.+\.php|.+wp-admin.+|.+\.htm|\/wordpress\/|\/wp\/|\/mm5\/|\/wp-content\/)" + ), + "http://example.org/", + ), ] def process_exception(self, request, exception): @@ -72,10 +79,17 @@ class CommonRedirectMiddleware(MiddlewareMixin): # some static image is missing show a placeholder (use full for local dev) m = re.match(r".*(max|fill)-(?P\d+x\d+)\.(jpg|png|svg)", path) if m: - return 'https://picsum.photos/{}'.format(m.group('dimensions').replace('x', '/')) + return "https://picsum.photos/{}".format( + m.group("dimensions").replace("x", "/") + ) # or dummy image: return 'http://via.placeholder.com/{}'.format(m.group('dimensions')) - if '.png' in path or '.jpg' in path or '.svg' in path or 'not-found' in path: - return 'https://picsum.photos/400/400' + if ( + ".png" in path + or ".jpg" in path + or ".svg" in path + or "not-found" in path + ): + return "https://picsum.photos/400/400" # https://stackoverflow.com/questions/4898408/how-to-set-a-login-cookie-in-django @@ -86,14 +100,24 @@ class UserLoggedInCookieMiddleWare(MiddlewareMixin): If the user is not authenticated and the cookie remains, delete it """ - cookie_name = 'loginStatus' + cookie_name = "loginStatus" def process_response(self, request, response): - #if user and no cookie, set cookie + # if user and no cookie, set cookie if request.user.is_authenticated and not request.COOKIES.get(self.cookie_name): - response.set_cookie(self.cookie_name, 'true') - elif not request.user.is_authenticated and request.COOKIES.get(self.cookie_name): - #else if if no user and cookie remove user cookie, logout + response.set_cookie(self.cookie_name, "true") + elif not request.user.is_authenticated and request.COOKIES.get( + self.cookie_name + ): + # else if if no user and cookie remove user cookie, logout response.delete_cookie(self.cookie_name) return response + +class SentryMiddleware(object): + def on_error(self, error): + capture_exception(error) + raise error + + def resolve(self, next, root, info: ResolveInfo, **args): + return next(root, info, **args).catch(self.on_error) diff --git a/server/core/settings.py b/server/core/settings.py index c8876a6d..79af64da 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -16,142 +16,138 @@ load_dotenv(find_dotenv()) # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get('SECRET_KEY') -SIGNING_SECRET = os.environ.get('SIGNING_SECRET') +SECRET_KEY = os.environ.get("SECRET_KEY") +SIGNING_SECRET = os.environ.get("SIGNING_SECRET") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool_value(os.environ.get('DEBUG', '')) -TEST = 'test' in sys.argv -ENABLE_SILKY = bool_value(os.environ.get('ENABLE_SILKY', '')) -SERVE_VIA_WEBPACK = bool_value(os.environ.get('SERVE_VIA_WEBPACK', DEBUG)) -ENABLE_SENTRY = not DEBUG or bool_value(os.environ.get('ENABLE_SENTRY_DEBUG', '')) +DEBUG = bool_value(os.environ.get("DEBUG", "")) +TEST = "test" in sys.argv +ENABLE_SILKY = bool_value(os.environ.get("ENABLE_SILKY", "")) +SERVE_VIA_WEBPACK = bool_value(os.environ.get("SERVE_VIA_WEBPACK", DEBUG)) +ENABLE_SENTRY = not DEBUG or bool_value(os.environ.get("ENABLE_SENTRY_DEBUG", "")) -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] if not DEBUG: SECURE_SSL_REDIRECT = True - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition INSTALLED_APPS = [ - 'core', - 'api', - 'users', - 'books', - 'objectives', - 'rooms', - 'assignments', - 'basicknowledge', - 'portfolio', - 'statistics', - 'surveys', - 'notes', - 'news', - 'oauth', - - 'wagtail.contrib.redirects', - 'wagtail.contrib.modeladmin', - 'wagtail.embeds', - 'wagtail.sites', - 'wagtail.users', - 'wagtail.snippets', - 'wagtail.documents', - 'wagtail.images', - 'wagtail.search', - 'wagtail.admin', - 'wagtail', - 'wagtail.api.v2', - 'wagtailautocomplete', - - 'taggit', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', + "core", + "api", + "users", + "books", + "objectives", + "rooms", + "assignments", + "basicknowledge", + "portfolio", + "statistics", + "surveys", + "notes", + "news", + "oauth", + "wagtail.contrib.redirects", + "wagtail.contrib.modeladmin", + "wagtail.embeds", + "wagtail.sites", + "wagtail.users", + "wagtail.snippets", + "wagtail.documents", + "wagtail.images", + "wagtail.search", + "wagtail.admin", + "wagtail", + "wagtail.api.v2", + "wagtailautocomplete", + "taggit", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", # 'raven.contrib.django.raven_compat', - 'whitenoise.runserver_nostatic', - 'django.contrib.staticfiles', - 'django_filters', - 'graphene_django', - 'django_extensions', - 'compressor', + "whitenoise.runserver_nostatic", + "django.contrib.staticfiles", + "django_filters", + "graphene_django", + "django_extensions", + "compressor", ] if DEBUG: - INSTALLED_APPS += ['wagtail.contrib.styleguide'] + INSTALLED_APPS += ["wagtail.contrib.styleguide"] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.middleware.gzip.GZipMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware' + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.middleware.gzip.GZipMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", ] # Enable CORS for local development if DEBUG: - INSTALLED_APPS += ['corsheaders'] - MIDDLEWARE += ['corsheaders.middleware.CorsMiddleware'] - CORS_ORIGIN_WHITELIST = ( - 'http://localhost:8080', - ) + INSTALLED_APPS += ["corsheaders"] + MIDDLEWARE += ["corsheaders.middleware.CorsMiddleware"] + CORS_ORIGIN_WHITELIST = ("http://localhost:8080",) CORS_ALLOW_CREDENTIALS = True # enable silk for performance measuring if ENABLE_SILKY: - INSTALLED_APPS += ['silk'] - MIDDLEWARE += ['silk.middleware.SilkyMiddleware', ] + INSTALLED_APPS += ["silk"] + MIDDLEWARE += [ + "silk.middleware.SilkyMiddleware", + ] MIDDLEWARE += [ - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - - 'wagtail.contrib.redirects.middleware.RedirectMiddleware', - - 'core.middleware.ThreadLocalMiddleware', - 'core.middleware.CommonRedirectMiddleware', - 'core.middleware.UserLoggedInCookieMiddleWare', - 'oauth.middleware.user_has_license_middleware', + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "wagtail.contrib.redirects.middleware.RedirectMiddleware", + "core.middleware.ThreadLocalMiddleware", + "core.middleware.CommonRedirectMiddleware", + "core.middleware.UserLoggedInCookieMiddleWare", + "oauth.middleware.user_has_license_middleware", ] -ROOT_URLCONF = 'core.urls' +ROOT_URLCONF = "core.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, '..', 'client/dist'), os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'core.context_processors.settings_context', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(BASE_DIR, "..", "client/dist"), + os.path.join(BASE_DIR, "templates"), + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "core.context_processors.settings_context", ], }, }, ] -WSGI_APPLICATION = 'core.wsgi.application' +WSGI_APPLICATION = "core.wsgi.application" # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases # Database -DATABASES = { - 'default': dj_database_url.config(conn_max_age=600) -} +DATABASES = {"default": dj_database_url.config(conn_max_age=600)} # Django custom user -AUTH_USER_MODEL = 'users.User' +AUTH_USER_MODEL = "users.User" # Password validation # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators @@ -162,30 +158,30 @@ if WEAK_PASSWORDS: else: AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -LOGOUT_REDIRECT_URL = '/login' -LOGIN_REDIRECT_URL = '/login' +LOGOUT_REDIRECT_URL = "/login" +LOGIN_REDIRECT_URL = "/login" LOGIN_URL = LOGIN_REDIRECT_URL # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ -LANGUAGE_CODE = 'de' +LANGUAGE_CODE = "de" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -194,167 +190,161 @@ USE_L10N = True USE_TZ = True LANGUAGES = [ - ('de', _('German')), - ('en', _('English')), + ("de", _("German")), + ("en", _("English")), ] -LOCALE_PATHS = [ - os.path.join(BASE_DIR, 'locale') -] +LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")] # Honor the 'X-Forwarded-Proto' header for request.is_secure() -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ -STATIC_ROOT = os.path.join(BASE_DIR, 'static') -STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATIC_URL = "/static/" STATICFILES_DIRS = ( - os.path.join(BASE_DIR, '..', 'client/dist/static'), - os.path.join(BASE_DIR, '..', 'client/src/assets'), + os.path.join(BASE_DIR, "..", "client/dist/static"), + os.path.join(BASE_DIR, "..", "client/src/assets"), ) if not TEST: - STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" COMPRESS_CSS_FILTERS = [ # 'django_compressor_autoprefixer.AutoprefixerFilter', - 'compressor.filters.cssmin.CSSMinFilter', + "compressor.filters.cssmin.CSSMinFilter", ] STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'compressor.finders.CompressorFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "compressor.finders.CompressorFinder", ) -COMPRESS_PRECOMPILERS = ( - ('text/x-scss', 'django_libsass.SassCompiler'), -) +COMPRESS_PRECOMPILERS = (("text/x-scss", "django_libsass.SassCompiler"),) COMPRESS_ENABLED = True if not DEBUG: - COMPRESS_STORAGE = 'compressor.storage.GzipCompressorFileStorage' + COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage" COMPRESS_OFFLINE = True # AWS S3 # http://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html -USE_AWS = bool_value(os.environ.get('USE_AWS')) +USE_AWS = bool_value(os.environ.get("USE_AWS")) AWS_QUERYSTRING_AUTH = False -AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID') -AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY') -AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME') +AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") +AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") +AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME") AWS_S3_FILE_OVERWRITE = False # use with cloudfront -AWS_S3_CUSTOM_DOMAIN = '{}.s3-{}.amazonaws.com'.format(AWS_STORAGE_BUCKET_NAME, - os.environ.get('AWS_REGION', 'eu-west-1')) +AWS_S3_CUSTOM_DOMAIN = "{}.s3-{}.amazonaws.com".format( + AWS_STORAGE_BUCKET_NAME, os.environ.get("AWS_REGION", "eu-west-1") +) if USE_AWS: DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" # use with cloudfront MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN else: - MEDIA_URL = '/media/' - MEDIA_ROOT = os.environ.get('DJANGO_MEDIAFILES', os.path.join(BASE_DIR, 'media')) + MEDIA_URL = "/media/" + MEDIA_ROOT = os.environ.get("DJANGO_MEDIAFILES", os.path.join(BASE_DIR, "media")) AWS_S3_OBJECT_PARAMETERS = { - 'CacheControl': 'max-age=86400', + "CacheControl": "max-age=86400", } # Media Files -USE_404_FALLBACK_IMAGE = bool_value(os.environ.get('USE_404_FALLBACK_IMAGE', 'True')) +USE_404_FALLBACK_IMAGE = bool_value(os.environ.get("USE_404_FALLBACK_IMAGE", "True")) # Logging Conf LOGGING = { - 'version': 1, - 'formatters': { - 'verbose_format': { - 'format': '%(levelname)s %(asctime)s %(module)s %(name)s (%(process)d): %(message)s' - }, - 'simple_format': { - 'format': '%(levelname)s %(name)s: %(message)s' + "version": 1, + "formatters": { + "verbose_format": { + "format": "%(levelname)s %(asctime)s %(module)s %(name)s (%(process)d): %(message)s" }, + "simple_format": {"format": "%(levelname)s %(name)s: %(message)s"}, }, - 'disable_existing_loggers': not DEBUG, - 'handlers': { - 'mail_admins': { - 'level': 'CRITICAL', - 'class': 'django.utils.log.AdminEmailHandler' + "disable_existing_loggers": not DEBUG, + "handlers": { + "mail_admins": { + "level": "CRITICAL", + "class": "django.utils.log.AdminEmailHandler", }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'stream': sys.stdout, - 'formatter': 'simple_format' + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "stream": sys.stdout, + "formatter": "simple_format", }, # for automatic papertrail logging - 'SysLog': { - 'level': 'INFO', - 'class': 'logging.handlers.SysLogHandler', - 'formatter': 'simple_format', - + "SysLog": { + "level": "INFO", + "class": "logging.handlers.SysLogHandler", + "formatter": "simple_format", }, }, - 'loggers': { - '': { - 'handlers': ['console'], - 'level': 'WARNING' + "loggers": { + "": {"handlers": ["console"], "level": "WARNING"}, + "skillbox": { + "handlers": ["console", "SysLog"], + "level": os.environ.get("DJANGO_LOG_LEVEL", "INFO"), + "propagate": False, }, - 'skillbox': { - 'handlers': ['console', 'SysLog'], - 'level': os.environ.get('DJANGO_LOG_LEVEL', 'INFO'), - 'propagate': False + "graphql": {"handlers": ["console"], "level": "WARNING", "propagate": False}, + "django": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, }, - 'graphql': { - 'handlers': ['console'], - 'level': 'WARNING', - 'propagate': False + "django.server": { + "handlers": ["console"], + "level": "WARNING", + "propagate": False, }, - 'django': { - 'handlers': ['console'], - 'level': 'INFO', - 'propagate': False, - }, - 'django.server': { - 'handlers': ['console'], - 'level': 'WARNING', - 'propagate': False, - }, - } + }, } -if ENABLE_SENTRY and os.environ.get('SENTRY_DSN'): +if ENABLE_SENTRY and os.environ.get("SENTRY_DSN"): import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration - + from sentry_sdk.integrations.logging import ignore_logger def before_send(event, hint): - user = event['user'] - id = user['id'] - event['user'] = {'id': id} + user = event["user"] + id = user["id"] + event["user"] = {"id": id} return event - - environment = os.environ.get('SENTRY_ENV', 'localhost') + environment = os.environ.get("SENTRY_ENV", "localhost") + sample_rate = os.environ.get("SENTRY_SAMPLE_RATE", 0.01) sentry_sdk.init( - dsn=os.environ.get('SENTRY_DSN'), + dsn=os.environ.get("SENTRY_DSN"), integrations=[DjangoIntegration()], send_default_pii=True, before_send=before_send, - environment=environment + before_send_transaction=before_send, + environment=environment, + traces_sample_rate=sample_rate, ) -RAVEN_DSN_JS = os.environ.get('RAVEN_DSN_JS', '') + # ignore 'Traceback' error messages, they don't give enough information + # from https://jerrynsh.com/how-to-monitor-python-graphql-api-with-sentry/ + ignore_logger("graphql.execution.utils") + +RAVEN_DSN_JS = os.environ.get("RAVEN_DSN_JS", "") GRAPHENE = { - 'SCHEMA': 'api.schema.schema', - 'SCHEMA_OUTPUT': 'schema.graphql' + "SCHEMA": "api.schema.schema", + "SCHEMA_OUTPUT": "schema.graphql", + "MIDDLEWARE": ["core.middleware.SentryMiddleware"], } # if DEBUG: @@ -363,25 +353,27 @@ GRAPHENE = { # ] # http://docs.wagtail.io/en/v2.1/advanced_topics/settings.html?highlight=urls -WAGTAIL_SITE_NAME = 'skillbox' +WAGTAIL_SITE_NAME = "skillbox" WAGTAILSEARCH_BACKENDS = { - 'default': { - 'BACKEND': 'wagtail.search.backends.database', + "default": { + "BACKEND": "wagtail.search.backends.database", } } -WAGTAILDOCS_DOCUMENT_MODEL = 'books.CustomDocument' +WAGTAILDOCS_DOCUMENT_MODEL = "books.CustomDocument" -GRAPHQL_QUERIES_DIR = os.path.join(BASE_DIR, '..', 'client', 'src', 'graphql', 'gql', 'queries') -GRAPHQL_MUTATIONS_DIR = os.path.join(GRAPHQL_QUERIES_DIR, '../mutations') +GRAPHQL_QUERIES_DIR = os.path.join( + BASE_DIR, "..", "client", "src", "graphql", "gql", "queries" +) +GRAPHQL_MUTATIONS_DIR = os.path.join(GRAPHQL_QUERIES_DIR, "../mutations") -DEFAULT_FROM_EMAIL = 'myskillbox ' +DEFAULT_FROM_EMAIL = "myskillbox " # Metanet Config if DEBUG: - EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" else: - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" ALLOW_BETA_LOGIN = True @@ -390,25 +382,25 @@ HEP_URL = os.environ.get("HEP_URL") # HEP Oauth AUTHLIB_OAUTH_CLIENTS = { - 'hep': { - 'client_id': os.environ.get("OAUTH_CLIENT_ID"), - 'client_secret': os.environ.get("OAUTH_CLIENT_SECRET"), - 'request_token_url': None, - 'request_token_params': None, - 'access_token_url': os.environ.get("OAUTH_ACCESS_TOKEN_URL"), - 'access_token_params': None, - 'refresh_token_url': None, - 'authorize_url': os.environ.get("OAUTH_AUTHORIZE_URL"), - 'api_base_url': os.environ.get("OAUTH_API_BASE_URL"), - 'client_kwargs': { - 'scope': 'orders', - 'token_endpoint_auth_method': 'client_secret_post', - 'token_placement': 'header', - } + "hep": { + "client_id": os.environ.get("OAUTH_CLIENT_ID"), + "client_secret": os.environ.get("OAUTH_CLIENT_SECRET"), + "request_token_url": None, + "request_token_params": None, + "access_token_url": os.environ.get("OAUTH_ACCESS_TOKEN_URL"), + "access_token_params": None, + "refresh_token_url": None, + "authorize_url": os.environ.get("OAUTH_AUTHORIZE_URL"), + "api_base_url": os.environ.get("OAUTH_API_BASE_URL"), + "client_kwargs": { + "scope": "orders", + "token_endpoint_auth_method": "client_secret_post", + "token_placement": "header", + }, } } -PLATFORM = os.environ.get('APP_FLAVOR', 'myskillbox') +PLATFORM = os.environ.get("APP_FLAVOR", "myskillbox") OAUTH_LOCAL_REDIRECT_URI = os.environ.get("OAUTH_LOCAL_REDIRECT_URI") @@ -419,12 +411,12 @@ TASKBASE_SUPERPASSWORD = os.environ.get("TASKBASE_SUPERPASSWORD") TASKBASE_BASEURL = os.environ.get("TASKBASE_BASEURL") ENABLE_SPELLCHECK = True if TASKBASE_BASEURL else False -TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' -TEST_OUTPUT_DIR = './test-reports/' +TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner" +TEST_OUTPUT_DIR = "./test-reports/" TEST_OUTPUT_VERBOSE = 1 # new default in Django 3.0, making it explicit to facilitate bug hunting -X_FRAME_OPTIONS = 'DENY' +X_FRAME_OPTIONS = "DENY" SECURE_CONTENT_TYPE_NOSNIFF = True # Django 3.2 uses BitAutoField by default, we keep things the old way -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/server/core/views.py b/server/core/views.py index 8855e5ff..06b86544 100644 --- a/server/core/views.py +++ b/server/core/views.py @@ -1,3 +1,4 @@ +from django.http.request import HttpRequest import requests from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin @@ -5,10 +6,34 @@ from django.http.response import HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.views.decorators.csrf import ensure_csrf_cookie from graphene_django.views import GraphQLView +from sentry_sdk.api import start_transaction from wagtail.admin.views.pages.listing import index as wagtailadmin_explore -class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): +# For sentry perfomance monitoring +# taken from https://jerrynsh.com/how-to-monitor-python-graphql-api-with-sentry/ +class SentryGraphQLView(GraphQLView): + def execute_graphql_request( + self, + request: HttpRequest, + data, + query, + variables, + operation_name, + show_graphiql, + ): + operation_type = ( + self.get_backend(request) + .document_from_string(self.schema, query) + .get_operation_type(operation_name) + ) + with start_transaction(op=operation_type, name=operation_name): + return super().execute_graphql_request( + request, data, query, variables, operation_name, show_graphiql + ) + + +class PrivateGraphQLView(LoginRequiredMixin, SentryGraphQLView): pass @@ -16,22 +41,24 @@ class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): def home(request): if settings.SERVE_VIA_WEBPACK: try: - res = requests.get('http://localhost:8080{}'.format(request.get_full_path())) + res = requests.get( + "http://localhost:8080{}".format(request.get_full_path()) + ) headers = res.headers - content_type = headers.get('content-type', 'text/html') + content_type = headers.get("content-type", "text/html") return HttpResponse(res.text, content_type=content_type) except Exception as e: - print('Can not connect to dev server at http://localhost:8080:', e) + print("Can not connect to dev server at http://localhost:8080:", e) - return render(request, 'index.html', {}) + return render(request, "index.html", {}) def override_wagtailadmin_explore_default_ordering(request, parent_page_id): """ Wrap Wagtail's explore view to change the default ordering """ - if request.method == 'GET' and 'ordering' not in request.GET: + if request.method == "GET" and "ordering" not in request.GET: # Display reordering handles by default for children of all Page types. - return HttpResponseRedirect(request.path_info + '?ordering=ord') + return HttpResponseRedirect(request.path_info + "?ordering=ord") return wagtailadmin_explore(request, parent_page_id=parent_page_id)