root: improve sentry distributed tracing (#14468)

* core: include all sentry headers

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove spotlight patch we dont need anymore

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* always trace in debug

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* init sentry earlier

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* re-add light interface

https://github.com/goauthentik/authentik/pull/14331

removes 2 unneeded API calls

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* sentry integrated router

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use new Sentry middleware to propagate headers

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing baggage

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* cleanup logs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use sanitized URLs for logging/tracing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2025-05-11 02:40:31 +02:00
committed by GitHub
parent 7d2aa43364
commit f11ba94603
17 changed files with 203 additions and 154 deletions

View File

@ -5,10 +5,10 @@ from typing import Any
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Value as V from django.db.models import Value as V
from django.http.request import HttpRequest from django.http.request import HttpRequest
from sentry_sdk import get_current_span
from authentik import get_full_version from authentik import get_full_version
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.lib.sentry import get_http_meta
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
_q_default = Q(default=True) _q_default = Q(default=True)
@ -32,13 +32,9 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects brand object into every template""" """Context Processor that injects brand object into every template"""
brand = getattr(request, "brand", DEFAULT_BRAND) brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant()) tenant = getattr(request, "tenant", Tenant())
trace = ""
span = get_current_span()
if span:
trace = span.to_traceparent()
return { return {
"brand": brand, "brand": brand,
"footer_links": tenant.footer_links, "footer_links": tenant.footer_links,
"sentry_trace": trace, "html_meta": {**get_http_meta()},
"version": get_full_version(), "version": get_full_version(),
} }

View File

@ -21,7 +21,9 @@
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" /> {% for key, value in html_meta.items %}
<meta name="{{key}}" content="{{ value }}" />
{% endfor %}
</head> </head>
<body> <body>
{% block body %} {% block body %}

View File

@ -17,7 +17,7 @@ from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from sentry_sdk import HttpTransport from sentry_sdk import HttpTransport, get_current_scope
from sentry_sdk import init as sentry_sdk_init from sentry_sdk import init as sentry_sdk_init
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from sentry_sdk.integrations.argv import ArgvIntegration from sentry_sdk.integrations.argv import ArgvIntegration
@ -27,6 +27,7 @@ from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.socket import SocketIntegration from sentry_sdk.integrations.socket import SocketIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException from websockets.exceptions import WebSocketException
@ -95,6 +96,8 @@ def traces_sampler(sampling_context: dict) -> float:
return 0 return 0
if _type == "websocket": if _type == "websocket":
return 0 return 0
if CONFIG.get_bool("debug"):
return 1
return float(CONFIG.get("error_reporting.sample_rate", 0.1)) return float(CONFIG.get("error_reporting.sample_rate", 0.1))
@ -167,3 +170,14 @@ def before_send(event: dict, hint: dict) -> dict | None:
if settings.DEBUG: if settings.DEBUG:
return None return None
return event return event
def get_http_meta():
"""Get sentry-related meta key-values"""
scope = get_current_scope()
meta = {
SENTRY_TRACE_HEADER_NAME: scope.get_traceparent() or "",
}
if bag := scope.get_baggage():
meta[BAGGAGE_HEADER_NAME] = bag.serialize()
return meta

1
web/package-lock.json generated
View File

@ -7,7 +7,6 @@
"": { "": {
"name": "@goauthentik/web", "name": "@goauthentik/web",
"version": "0.0.0", "version": "0.0.0",
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
".", ".",

View File

@ -19,7 +19,6 @@
"lint:precommit": "wireit", "lint:precommit": "wireit",
"lint:types": "wireit", "lint:types": "wireit",
"lit-analyse": "wireit", "lit-analyse": "wireit",
"postinstall": "bash scripts/patch-spotlight.sh",
"precommit": "wireit", "precommit": "wireit",
"prettier": "wireit", "prettier": "wireit",
"prettier-check": "wireit", "prettier-check": "wireit",

View File

@ -6,7 +6,7 @@
* @import { Message as ESBuildMessage } from "esbuild"; * @import { Message as ESBuildMessage } from "esbuild";
*/ */
const logPrefix = "👷 [ESBuild]"; const logPrefix = "authentik/dev/web: ";
const log = console.debug.bind(console, logPrefix); const log = console.debug.bind(console, logPrefix);
/** /**

View File

@ -1,33 +0,0 @@
#!/usr/bin/env bash
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2> /dev/null) ]]; then
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
if ! grep -GL 'QX2 = ' "$TARGET" > /dev/null ; then
patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
>>>>>>> main
--- a/index-5682ce90.js 2024-06-13 16:19:28
+++ b/index-5682ce90.js 2024-06-13 16:20:23
@@ -4958,11 +4958,10 @@
}
);
}
-const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m));
+const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m)), QX2 = () => {};
function Gp({
data: n,
- onUpdateData: a = () => {
- },
+ onUpdateData: a = QX2,
editingEnabled: s = !1,
clipboardEnabled: o = !1,
displayDataTypes: c = !1,
EOF
else
echo "spotlight overlay.js patch already applied"
fi

View File

@ -131,9 +131,9 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Lifecycle //#region Lifecycle
constructor() { constructor() {
configureSentry(true);
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)"); this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches; this.sidebarOpen = this.#sidebarMatcher.matches;
} }
@ -167,7 +167,6 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
configureSentry(true);
this.user = await me(); this.user = await me();
const canAccessAdmin = const canAccessAdmin =

View File

@ -6,6 +6,7 @@ import {
} from "@goauthentik/common/api/middleware"; } from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants"; import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { SentryMiddleware } from "@goauthentik/common/sentry";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api"; import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
@ -66,21 +67,13 @@ export function brand(): Promise<CurrentBrand> {
return globalBrandPromise; return globalBrandPromise;
} }
export function getMetaContent(key: string): string {
const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`);
if (!metaEl) return "";
return metaEl.content;
}
export const DEFAULT_CONFIG = new Configuration({ export const DEFAULT_CONFIG = new Configuration({
basePath: `${globalAK().api.base}api/v3`, basePath: `${globalAK().api.base}api/v3`,
headers: {
"sentry-trace": getMetaContent("sentry-trace"),
},
middleware: [ middleware: [
new CSRFMiddleware(), new CSRFMiddleware(),
new EventMiddleware(), new EventMiddleware(),
new LoggingMiddleware(globalAK().brand), new LoggingMiddleware(globalAK().brand),
new SentryMiddleware(),
], ],
}); });

View File

@ -1,5 +1,5 @@
import { config } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants"; import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils"; import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
import { import {
@ -10,8 +10,16 @@ import {
setTag, setTag,
setUser, setUser,
} from "@sentry/browser"; } from "@sentry/browser";
import { getTraceData } from "@sentry/core";
import * as Spotlight from "@spotlightjs/spotlight";
import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api"; import {
CapabilitiesEnum,
FetchParams,
Middleware,
RequestContext,
ResponseError,
} from "@goauthentik/api";
/** /**
* A generic error that can be thrown without triggering Sentry's reporting. * A generic error that can be thrown without triggering Sentry's reporting.
@ -21,10 +29,14 @@ export class SentryIgnoredError extends Error {}
export const TAG_SENTRY_COMPONENT = "authentik.component"; export const TAG_SENTRY_COMPONENT = "authentik.component";
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities"; export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
export async function configureSentry(canDoPpi = false): Promise<Config> { let _sentryConfigured = false;
const cfg = await config();
if (cfg.errorReporting.enabled) { export function configureSentry(canDoPpi = false) {
const cfg = globalAK().config;
const debug = cfg.capabilities.includes(CapabilitiesEnum.CanDebug);
if (!cfg.errorReporting.enabled && !debug) {
return cfg;
}
init({ init({
dsn: cfg.errorReporting.sentryDsn, dsn: cfg.errorReporting.sentryDsn,
ignoreErrors: [ ignoreErrors: [
@ -41,12 +53,14 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
release: `authentik@${VERSION}`, release: `authentik@${VERSION}`,
integrations: [ integrations: [
browserTracingIntegration({ browserTracingIntegration({
shouldCreateSpanForRequest: (url: string) => { // https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing
return url.startsWith(window.location.host); instrumentNavigation: false,
}, instrumentPageLoad: false,
traceFetch: false,
}), }),
], ],
tracesSampleRate: cfg.errorReporting.tracesSampleRate, tracePropagationTargets: [window.location.origin],
tracesSampleRate: debug ? 1.0 : cfg.errorReporting.tracesSampleRate,
environment: cfg.errorReporting.environment, environment: cfg.errorReporting.environment,
beforeSend: ( beforeSend: (
event: ErrorEvent, event: ErrorEvent,
@ -71,10 +85,16 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
if (window.location.pathname.includes("if/")) { if (window.location.pathname.includes("if/")) {
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`); setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
} }
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) { if (debug) {
const Spotlight = await import("@spotlightjs/spotlight"); Spotlight.init({
injectImmediately: true,
Spotlight.init({ injectImmediately: true }); integrations: [
Spotlight.sentry({
injectIntoSDK: true,
}),
],
});
console.debug("authentik/config: Enabled Sentry Spotlight");
} }
if (cfg.errorReporting.sendPii && canDoPpi) { if (cfg.errorReporting.sendPii && canDoPpi) {
me().then((user) => { me().then((user) => {
@ -84,6 +104,19 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
} else { } else {
console.debug("authentik/config: Sentry enabled."); console.debug("authentik/config: Sentry enabled.");
} }
} _sentryConfigured = true;
return cfg; }
export class SentryMiddleware implements Middleware {
pre?(context: RequestContext): Promise<FetchParams | void> {
if (!_sentryConfigured) {
return Promise.resolve(context);
}
const traceData = getTraceData();
// @ts-ignore
context.init.headers["baggage"] = traceData["baggage"];
// @ts-ignore
context.init.headers["sentry-trace"] = traceData["sentry-trace"];
return Promise.resolve(context);
}
} }

View File

@ -23,9 +23,20 @@ const configContext = Symbol("configContext");
const modalController = Symbol("modalController"); const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext"); const versionContext = Symbol("versionContext");
export abstract class Interface extends AKElement implements ThemedElement { export abstract class LightInterface extends AKElement implements ThemedElement {
protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase); protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
constructor() {
super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
}
}
export abstract class Interface extends LightInterface implements ThemedElement {
[configContext]: ConfigContextController; [configContext]: ConfigContextController;
[modalController]: ModalOrchestrationController; [modalController]: ModalOrchestrationController;
@ -38,12 +49,6 @@ export abstract class Interface extends AKElement implements ThemedElement {
constructor() { constructor() {
super(); super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
this.addController(new BrandContextController(this)); this.addController(new BrandContextController(this));
this[configContext] = new ConfigContextController(this); this[configContext] = new ConfigContextController(this);
this[modalController] = new ModalOrchestrationController(this); this[modalController] = new ModalOrchestrationController(this);

View File

@ -1,4 +1,4 @@
import { AuthenticatedInterface, Interface } from "./Interface"; import { AuthenticatedInterface, Interface, LightInterface } from "./Interface";
export { Interface, AuthenticatedInterface }; export { Interface, AuthenticatedInterface, LightInterface };
export default Interface; export default Interface;

View File

@ -6,19 +6,35 @@ import { TemplateResult } from "lit";
export class RouteMatch { export class RouteMatch {
route: Route; route: Route;
arguments: { [key: string]: string }; arguments: { [key: string]: string };
fullUrl?: string; fullURL: string;
constructor(route: Route) { constructor(route: Route, fullUrl: string) {
this.route = route; this.route = route;
this.arguments = {}; this.arguments = {};
this.fullURL = fullUrl;
} }
render(): TemplateResult { render(): TemplateResult {
return this.route.render(this.arguments); return this.route.render(this.arguments);
} }
/**
* Convert the matched Route's URL regex to a sanitized, readable URL by replacing
* all regex values with placeholders according to the name of their regex group.
*
* @returns The sanitized URL for logging/tracing.
*/
sanitizedURL() {
let cleanedURL = this.fullURL;
for (const match of Object.keys(this.arguments)) {
const value = this.arguments[match];
cleanedURL = cleanedURL?.replace(value, `:${match}`);
}
return cleanedURL;
}
toString(): string { toString(): string {
return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify( return `<RouteMatch url=${this.sanitizedURL()} route=${this.route} arguments=${JSON.stringify(
this.arguments, this.arguments,
)}>`; )}>`;
} }

View File

@ -3,8 +3,15 @@ import { AKElement } from "@goauthentik/elements/Base";
import { Route } from "@goauthentik/elements/router/Route"; import { Route } from "@goauthentik/elements/router/Route";
import { RouteMatch } from "@goauthentik/elements/router/RouteMatch"; import { RouteMatch } from "@goauthentik/elements/router/RouteMatch";
import "@goauthentik/elements/router/Router404"; import "@goauthentik/elements/router/Router404";
import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getClient,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
} from "@sentry/browser";
import { Client, Span } from "@sentry/types";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
// Poliyfill for hashchange.newURL, // Poliyfill for hashchange.newURL,
@ -53,6 +60,9 @@ export class RouterOutlet extends AKElement {
@property({ attribute: false }) @property({ attribute: false })
routes: Route[] = []; routes: Route[] = [];
private sentryClient?: Client;
private pageLoadSpan?: Span;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
css` css`
@ -69,6 +79,15 @@ export class RouterOutlet extends AKElement {
constructor() { constructor() {
super(); super();
window.addEventListener("hashchange", (ev: HashChangeEvent) => this.navigate(ev)); window.addEventListener("hashchange", (ev: HashChangeEvent) => this.navigate(ev));
this.sentryClient = getClient();
if (this.sentryClient) {
this.pageLoadSpan = startBrowserTracingPageLoadSpan(this.sentryClient, {
name: window.location.pathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url",
},
});
}
} }
firstUpdated(): void { firstUpdated(): void {
@ -92,9 +111,8 @@ export class RouterOutlet extends AKElement {
this.routes.some((route) => { this.routes.some((route) => {
const match = route.url.exec(activeUrl); const match = route.url.exec(activeUrl);
if (match !== null) { if (match !== null) {
matchedRoute = new RouteMatch(route); matchedRoute = new RouteMatch(route, activeUrl);
matchedRoute.arguments = match.groups || {}; matchedRoute.arguments = match.groups || {};
matchedRoute.fullUrl = activeUrl;
console.debug("authentik/router: found match ", matchedRoute); console.debug("authentik/router: found match ", matchedRoute);
return true; return true;
} }
@ -107,13 +125,31 @@ export class RouterOutlet extends AKElement {
<ak-router-404 url=${activeUrl}></ak-router-404> <ak-router-404 url=${activeUrl}></ak-router-404>
</div>`; </div>`;
}); });
matchedRoute = new RouteMatch(route); matchedRoute = new RouteMatch(route, activeUrl);
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {}; matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
matchedRoute.fullUrl = activeUrl;
} }
this.current = matchedRoute; this.current = matchedRoute;
} }
updated(changedProperties: PropertyValues<this>): void {
if (!changedProperties.has("current") || !this.current) return;
if (!this.sentryClient) return;
// https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing
if (this.pageLoadSpan) {
this.pageLoadSpan.updateName(this.current.sanitizedURL());
this.pageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, "route");
this.pageLoadSpan = undefined;
} else {
startBrowserTracingNavigationSpan(this.sentryClient, {
op: "navigation",
name: this.current.sanitizedURL(),
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route",
},
});
}
}
render(): TemplateResult | undefined { render(): TemplateResult | undefined {
return this.current?.render(); return this.current?.render();
} }

View File

@ -171,6 +171,7 @@ export class FlowExecutor extends Interface implements StageHost {
} }
constructor() { constructor() {
configureSentry();
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
const inspector = new URL(window.location.toString()).searchParams.get("inspector"); const inspector = new URL(window.location.toString()).searchParams.get("inspector");
@ -237,7 +238,6 @@ export class FlowExecutor extends Interface implements StageHost {
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
configureSentry();
if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) { if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) {
this.inspectorAvailable = true; this.inspectorAvailable = true;
} }

View File

@ -1,4 +1,4 @@
import { Interface } from "@goauthentik/elements/Interface"; import { LightInterface } from "@goauthentik/elements/Interface";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
@ -10,7 +10,7 @@ import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-loading") @customElement("ak-loading")
export class Loading extends Interface { export class Loading extends LightInterface {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -25,16 +25,6 @@ export class Loading extends Interface {
]; ];
} }
registerContexts(): void {
// Stub function to avoid making API requests for things we don't need. The `Interface` base class loads
// a bunch of data that is used globally by various things, however this is an interface that is shown
// very briefly and we don't need any of that data.
}
async _initCustomCSS(): Promise<void> {
// Stub function to avoid fetching custom CSS.
}
render(): TemplateResult { render(): TemplateResult {
return html` <section return html` <section
class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl" class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"

View File

@ -281,10 +281,10 @@ export class UserInterface extends AuthenticatedInterface {
me?: SessionUser; me?: SessionUser;
constructor() { constructor() {
configureSentry(true);
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.fetchConfigurationDetails(); this.fetchConfigurationDetails();
configureSentry(true);
this.toggleNotificationDrawer = this.toggleNotificationDrawer.bind(this); this.toggleNotificationDrawer = this.toggleNotificationDrawer.bind(this);
this.toggleApiDrawer = this.toggleApiDrawer.bind(this); this.toggleApiDrawer = this.toggleApiDrawer.bind(this);
this.fetchConfigurationDetails = this.fetchConfigurationDetails.bind(this); this.fetchConfigurationDetails = this.fetchConfigurationDetails.bind(this);