import { HoppModule } from "." import * as Sentry from "@sentry/vue" import { BrowserTracing } from "@sentry/tracing" import { Route } from "@sentry/vue/types/router" import { RouteLocationNormalized, Router } from "vue-router" import { settingsStore } from "~/newstore/settings" import { App } from "vue" import { APP_IS_IN_DEV_MODE } from "~/helpers/dev" import { gqlClientError$ } from "~/helpers/backend/GQLClient" import { platform } from "~/platform" /** * The tag names we allow giving to Sentry */ type SentryTag = "BACKEND_OPERATIONS" interface SentryVueRouter { onError: (fn: (err: Error) => void) => void beforeEach: (fn: (to: Route, from: Route, next: () => void) => void) => void } function normalizedRouteToSentryRoute(route: RouteLocationNormalized): Route { return { matched: route.matched, // route.params' type translates just to a fancy version of this, hence assertion params: route.params as Route["params"], path: route.path, // route.query's type translates just to a fancy version of this, hence assertion query: route.query as Route["query"], name: route.name, } } function getInstrumentationVueRouter(router: Router): SentryVueRouter { return { onError: router.onError, beforeEach(func) { router.beforeEach((to, from, next) => { func( normalizedRouteToSentryRoute(to), normalizedRouteToSentryRoute(from), next ) }) }, } } let sentryActive = false function initSentry(dsn: string, router: Router, app: App) { Sentry.init({ app, dsn, release: import.meta.env.VITE_SENTRY_RELEASE_TAG ?? undefined, environment: APP_IS_IN_DEV_MODE ? "dev" : import.meta.env.VITE_SENTRY_ENVIRONMENT, integrations: [ new BrowserTracing({ routingInstrumentation: Sentry.vueRouterInstrumentation( getInstrumentationVueRouter(router) ), // TODO: We may want to limit this later on tracingOrigins: [new URL(import.meta.env.VITE_BACKEND_GQL_URL).origin], }), ], tracesSampleRate: 0.8, }) sentryActive = true } function deinitSentry() { Sentry.close() sentryActive = false } /** * Reports a set of related errors to Sentry * @param errs The errors to report * @param tag The tag for the errord * @param extraTags Additional tag data to add * @param extras Extra information to attach */ function reportErrors( errs: Error[], tag: SentryTag, extraTags: Record | null = null, extras: any = undefined ) { if (sentryActive) { Sentry.withScope((scope) => { scope.setTag("tag", tag) if (extraTags) { Object.entries(extraTags).forEach(([key, value]) => { scope.setTag(key, value) }) } if (extras !== null && extras === undefined) scope.setExtras(extras) scope.addAttachment({ filename: "extras-dump.json", data: JSON.stringify(extras), contentType: "application/json", }) errs.forEach((err) => Sentry.captureException(err)) }) } } /** * Reports a specific error to Sentry * @param err The error to report * @param tag The tag for the error * @param extraTags Additional tag data to add * @param extras Extra information to attach */ function reportError( err: Error, tag: SentryTag, extraTags: Record | null = null, extras: any = undefined ) { reportErrors([err], tag, extraTags, extras) } /** * Subscribes to events occuring in various subsystems in the app * for personalized error reporting */ function subscribeToAppEventsForReporting() { gqlClientError$.subscribe((ev) => { switch (ev.type) { case "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT": reportErrors(ev.errors, "BACKEND_OPERATIONS", { from: ev.type }) break case "CLIENT_REPORTED_ERROR": reportError( ev.error, "BACKEND_OPERATIONS", { from: ev.type }, { op: ev.op } ) break case "GQL_CLIENT_REPORTED_ERROR": reportError( new Error("Backend Query Failed"), "BACKEND_OPERATIONS", { opType: ev.opType }, { opResult: ev.opResult, } ) break } }) } /** * Subscribe to app system events for adding * additional data tags for the error reporting */ function subscribeForAppDataTags() { const currentUser$ = platform.auth.getCurrentUserStream() currentUser$.subscribe((user) => { if (sentryActive) { Sentry.setTag("user_logged_in", !!user) } }) } export default { onRouterInit(app, router) { if (!import.meta.env.VITE_SENTRY_DSN) { console.log( "Sentry tracing is not enabled because 'VITE_SENTRY_DSN' env is not defined" ) return } if (settingsStore.value.TELEMETRY_ENABLED) { initSentry(import.meta.env.VITE_SENTRY_DSN, router, app) } settingsStore.subject$.subscribe(({ TELEMETRY_ENABLED }) => { if (!TELEMETRY_ENABLED && sentryActive) { deinitSentry() } else if (TELEMETRY_ENABLED && !sentryActive) { initSentry(import.meta.env.VITE_SENTRY_DSN!, router, app) } }) subscribeToAppEventsForReporting() subscribeForAppDataTags() }, }