From e384f2d299e34472e7c0f12bd5c417f6efde2ab4 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 8 Apr 2024 13:12:35 +0200 Subject: [PATCH] fix(fe2): low level fatal server errors not being logged to RUM (#2197) * fix(fe2): low level fatal server errors not being logged to RUM * undo nuxt config changes --- .../frontend-2/lib/core/composables/error.ts | 14 ++++ .../lib/core/helpers/observability.ts | 2 + packages/frontend-2/plugins/001-logger.ts | 78 ++++++++++++++++++- packages/frontend-2/plugins/002-rum.ts | 2 +- .../frontend-2/type-augmentations/nuxt.d.ts | 4 + 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/packages/frontend-2/lib/core/composables/error.ts b/packages/frontend-2/lib/core/composables/error.ts index 6ff9b9bba..9b724902f 100644 --- a/packages/frontend-2/lib/core/composables/error.ts +++ b/packages/frontend-2/lib/core/composables/error.ts @@ -2,6 +2,7 @@ import { useScopedState } from '~~/lib/common/composables/scopedState' import { Observability } from '@speckle/shared' import type { AbstractErrorHandler, + AbstractErrorHandlerParams, AbstractUnhandledErrorHandler } from '~/lib/core/helpers/observability' @@ -57,5 +58,18 @@ export const useCreateErrorLoggingTransport = () => { export const useGetErrorLoggingTransports = () => { const { transports } = useErrorLoggingTransportState() + return transports } + +export const useLogToErrorLoggingTransports = () => { + const transports = useGetErrorLoggingTransports() + const invokeTransportsWithPayload = (payload: AbstractErrorHandlerParams) => { + transports.forEach((handler) => handler.onError(payload)) + } + + return { + transports, + invokeTransportsWithPayload + } +} diff --git a/packages/frontend-2/lib/core/helpers/observability.ts b/packages/frontend-2/lib/core/helpers/observability.ts index 02ded6348..3b77ded57 100644 --- a/packages/frontend-2/lib/core/helpers/observability.ts +++ b/packages/frontend-2/lib/core/helpers/observability.ts @@ -135,6 +135,8 @@ export type AbstractUnhandledErrorHandler = (params: { message: string }) => void +export type AbstractErrorHandlerParams = Parameters[0] + /** * Adds proxy that intercepts error log calls so that they can be sent to any transport */ diff --git a/packages/frontend-2/plugins/001-logger.ts b/packages/frontend-2/plugins/001-logger.ts index 652f89914..cd11d02a1 100644 --- a/packages/frontend-2/plugins/001-logger.ts +++ b/packages/frontend-2/plugins/001-logger.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ -import { omit } from 'lodash-es' +import type { Optional } from '@speckle/shared' +import { get, omit } from 'lodash-es' import type { SetRequired } from 'type-fest' import { useReadUserId } from '~/lib/auth/composables/activeUser' import { useCreateErrorLoggingTransport, - useGetErrorLoggingTransports + useGetErrorLoggingTransports, + useLogToErrorLoggingTransports } from '~/lib/core/composables/error' import { useRequestId, @@ -16,9 +18,20 @@ import { buildFakePinoLogger, enableCustomErrorHandling, type AbstractErrorHandler, + type AbstractErrorHandlerParams, type AbstractUnhandledErrorHandler } from '~~/lib/core/helpers/observability' +class CustomDeserializedError extends Error { + code: string + + constructor(params: { code: string; message: string; stack: string }) { + super(params.message) + this.code = params.code + this.stack = params.stack + } +} + /** * - Setting up Pino logger in SSR, basic console.log fallback in CSR * - Also sets up ability to add extra transport for other observability tools @@ -43,6 +56,7 @@ export default defineNuxtPlugin(async (nuxtApp) => { const getUserId = useReadUserId() const country = useUserCountry() const registerErrorTransport = useCreateErrorLoggingTransport() + const { invokeTransportsWithPayload } = useLogToErrorLoggingTransports() const collectMainInfo = (params: { isBrowser: boolean }) => { const info = { @@ -207,9 +221,16 @@ export default defineNuxtPlugin(async (nuxtApp) => { // Global error handler - handle all transports const transports = useGetErrorLoggingTransports() + let serverFatalError: Optional = undefined logger = enableCustomErrorHandling({ logger, onError: (params) => { + const { otherData } = params + + if (process.server && otherData?.isAppError) { + serverFatalError = params + } + transports.forEach((handler) => handler.onError(params)) } }) @@ -243,7 +264,8 @@ export default defineNuxtPlugin(async (nuxtApp) => { logger.error(err, 'Unhandled error in routing', { to: to.path, - from: from?.path + from: from?.path, + isAppError: !!process.server }) }) @@ -257,6 +279,56 @@ export default defineNuxtPlugin(async (nuxtApp) => { }) }) + // Hydrate server fatal error to CSR + if (process.server) { + nuxtApp.hook('app:rendered', () => { + let serializableError: Optional = undefined + try { + serializableError = serverFatalError + ? (JSON.parse(JSON.stringify(serverFatalError)) as AbstractErrorHandlerParams) + : undefined + if (serializableError && serverFatalError?.firstError) { + serializableError.firstError = { + code: get(serverFatalError.firstError, 'code', 'unknown') as string, + message: get(serverFatalError.firstError, 'message', 'unknown') as string, + stack: get(serverFatalError.firstError, 'stack', 'unknown') as string + } as unknown as Error // fakin it + } + } catch (e) { + serializableError = { + args: [], + firstString: 'Failed to serialize serverFatalError', + firstError: e as Error, + otherData: {}, + nonObjectOtherData: [] + } + } + + nuxtApp.ssrContext!.payload.serverFatalError = serializableError + }) + } else { + nuxtApp.hook('app:mounted', () => { + const serverFatalError = nuxtApp.payload.serverFatalError + if (serverFatalError) { + if (serverFatalError.firstError) { + serverFatalError.firstError = new CustomDeserializedError({ + message: get(serverFatalError.firstError, 'message', 'unknown') as string, + code: get(serverFatalError.firstError, 'code', 'unknown') as string, + stack: get(serverFatalError.firstError, 'stack', 'unknown') as string + }) + } + + invokeTransportsWithPayload(serverFatalError) + + if (process.dev) { + // intentionally skipping error pipeline: + // eslint-disable-next-line no-console + console.error('Fatal error occurred on server:', serverFatalError) + } + } + }) + } + return { provide: { logger diff --git a/packages/frontend-2/plugins/002-rum.ts b/packages/frontend-2/plugins/002-rum.ts index 41a7c2917..80d292781 100644 --- a/packages/frontend-2/plugins/002-rum.ts +++ b/packages/frontend-2/plugins/002-rum.ts @@ -105,7 +105,7 @@ async function initRumClient(app: PluginNuxtApp) { router.beforeEach((to) => { const pathDefinition = getRouteDefinition(to) - const routeName = to.meta.datadogName || pathDefinition + const routeName = (to.meta.datadogName || pathDefinition) as string const realPath = to.path window.DD_RUM_START_VIEW?.(realPath, routeName) diff --git a/packages/frontend-2/type-augmentations/nuxt.d.ts b/packages/frontend-2/type-augmentations/nuxt.d.ts index bfb74b632..5f9a577eb 100644 --- a/packages/frontend-2/type-augmentations/nuxt.d.ts +++ b/packages/frontend-2/type-augmentations/nuxt.d.ts @@ -13,6 +13,10 @@ declare module '#app' { */ __scopedStates?: Record } + + interface NuxtPayload { + serverFatalError?: import('~~/lib/core/helpers/observability').AbstractErrorHandlerParams + } } declare module 'vue' {