From a074e1ec3b3bd8ef3fe80b4daa5f202c0f7efed4 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Mon, 22 Apr 2024 18:41:38 +0800 Subject: [PATCH] Implement Baselime monitoring and update error handling This commit introduces the integration of Baselime for monitoring, accounting for various error scenarios and improved console error logging. Request handling has been updated to assign unique IDs for each request, aiding in tracing/logs. The environment variable key was updated, and the `MonitoringProvider` was nested in the root providers. In the base monitoring service, a function to format errors for logging was added. The provider logic was updated to create a new instance of service for each request, improving memory efficiency. --- README.md | 2 +- apps/web/components/root-providers.tsx | 43 +++++------ apps/web/middleware.ts | 19 +++++ packages/monitoring/baselime/README.md | 2 +- .../baselime/src/components/provider.tsx | 2 +- .../baselime-server-monitoring.service.ts | 72 ++++++++++++++++++- packages/monitoring/src/components/index.ts | 1 + .../monitoring/src/components/provider.tsx | 45 ++++++++++++ .../monitoring/src/hooks/use-monitoring.ts | 18 ++--- .../services/console-monitoring.service.ts | 4 +- 10 files changed, 172 insertions(+), 36 deletions(-) create mode 100644 packages/monitoring/src/components/provider.tsx diff --git a/README.md b/README.md index 3fd75aa09..085d1adca 100644 --- a/README.md +++ b/README.md @@ -1314,7 +1314,7 @@ NEXT_PUBLIC_MONITORING_PROVIDER=sentry To use Baselime, you need to set the following environment variables: ```bash -BASELIME_KEY=your_key +NEXT_PUBLIC_BASELIME_KEY=your_key NEXT_PUBLIC_MONITORING_PROVIDER=baselime ``` diff --git a/apps/web/components/root-providers.tsx b/apps/web/components/root-providers.tsx index 4b01ee6d5..223d83a1f 100644 --- a/apps/web/components/root-providers.tsx +++ b/apps/web/components/root-providers.tsx @@ -8,6 +8,7 @@ import { ThemeProvider } from 'next-themes'; import { CaptchaProvider } from '@kit/auth/captcha/client'; import { I18nProvider } from '@kit/i18n/provider'; +import { MonitoringProvider } from '@kit/monitoring/components'; import { AuthChangeListener } from '@kit/supabase/components/auth-change-listener'; import appConfig from '~/config/app.config'; @@ -42,26 +43,28 @@ export function RootProviders({ const i18nSettings = getI18nSettings(lang); return ( - - - - - + + + + + + - - - {children} - - - - - - + + + {children} + + + + + + + ); } diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index cb6e4da12..87298412e 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -21,6 +21,10 @@ export const config = { export async function middleware(request: NextRequest) { const response = NextResponse.next(); + // set a unique request ID for each request + // this helps us log and trace requests + setRequestId(request); + // apply CSRF and session middleware const csrfResponse = await withCsrfMiddleware(request, response); @@ -109,6 +113,9 @@ async function adminMiddleware(request: NextRequest, response: NextResponse) { return response; } +/** + * Define URL patterns and their corresponding handlers. + */ function getPatterns() { return [ { @@ -170,6 +177,10 @@ function getPatterns() { ]; } +/** + * Match URL patterns to specific handlers. + * @param url + */ function matchUrlPattern(url: string) { const patterns = getPatterns(); const input = url.split('?')[0]; @@ -182,3 +193,11 @@ function matchUrlPattern(url: string) { } } } + +/** + * Set a unique request ID for each request. + * @param request + */ +function setRequestId(request: Request) { + request.headers.set('x-correlation-id', crypto.randomUUID()); +} diff --git a/packages/monitoring/baselime/README.md b/packages/monitoring/baselime/README.md index d7856fd4e..2e8c528ab 100644 --- a/packages/monitoring/baselime/README.md +++ b/packages/monitoring/baselime/README.md @@ -3,6 +3,6 @@ Please set the following environment variables: ``` -BASELIME_KEY=your_key +NEXT_PUBLIC_BASELIME_KEY=your_key NEXT_PUBLIC_MONITORING_PROVIDER=baselime ``` \ No newline at end of file diff --git a/packages/monitoring/baselime/src/components/provider.tsx b/packages/monitoring/baselime/src/components/provider.tsx index a78ff5d39..e35d14fd7 100644 --- a/packages/monitoring/baselime/src/components/provider.tsx +++ b/packages/monitoring/baselime/src/components/provider.tsx @@ -1,6 +1,6 @@ import { BaselimeRum } from '@baselime/react-rum'; -export function BaselineProvider({ +export function BaselimeProvider({ children, apiKey, enableWebVitals, diff --git a/packages/monitoring/baselime/src/services/baselime-server-monitoring.service.ts b/packages/monitoring/baselime/src/services/baselime-server-monitoring.service.ts index 9599bcfda..313b4d338 100644 --- a/packages/monitoring/baselime/src/services/baselime-server-monitoring.service.ts +++ b/packages/monitoring/baselime/src/services/baselime-server-monitoring.service.ts @@ -1,9 +1,75 @@ +import process from 'node:process'; +import { z } from 'zod'; + import { MonitoringService } from '../../../src/services/monitoring.service'; +const apiKey = z + .string({ + required_error: 'API_KEY is required', + }) + .parse(process.env.BASELIME_API_KEY); + export class BaselimeServerMonitoringService implements MonitoringService { - captureException(error: Error | null) { - console.error(`Caught exception: ${JSON.stringify(error)}`); + userId: string | null = null; + + async captureException( + error: Error | null, + extra?: { + requestId?: string; + sessionId?: string; + namespace?: string; + service?: string; + }, + ) { + const formattedError = error ? getFormattedError(error) : {}; + + const event = { + level: 'error', + data: { error }, + error: { + ...formattedError, + }, + message: error ? `${error.name}: ${error.message}` : `Unknown error`, + }; + + const response = await fetch(`https://events.baselime.io/v1/web`, { + method: 'POST', + headers: { + contentType: 'application/json', + 'x-api-key': apiKey, + 'x-service': extra?.service ?? '', + 'x-namespace': extra?.namespace ?? '', + }, + body: JSON.stringify([ + { + userId: this.userId, + sessionId: extra?.sessionId, + namespace: extra?.namespace, + ...event, + }, + ]), + }); + + if (!response.ok) { + console.error( + { + response, + event, + }, + 'Failed to send event to Baselime', + ); + } } - identifyUser(info: Info) {} + identifyUser(info: Info) { + this.userId = info.id; + } +} + +function getFormattedError(error: Error) { + return { + name: error.name, + message: error.message, + stack: error.stack, + }; } diff --git a/packages/monitoring/src/components/index.ts b/packages/monitoring/src/components/index.ts index 39d945d40..dccefb102 100644 --- a/packages/monitoring/src/components/index.ts +++ b/packages/monitoring/src/components/index.ts @@ -1 +1,2 @@ export * from './error-boundary'; +export * from './provider'; diff --git a/packages/monitoring/src/components/provider.tsx b/packages/monitoring/src/components/provider.tsx new file mode 100644 index 000000000..bc7fea910 --- /dev/null +++ b/packages/monitoring/src/components/provider.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { lazy } from 'react'; + +import { getMonitoringProvider } from '../get-monitoring-provider'; +import { InstrumentationProvider } from '../monitoring-providers.enum'; + +const BaselimeProvider = lazy(async () => { + const { BaselimeProvider } = await import('@kit/baselime/provider'); + + return { + default: BaselimeProvider, + }; +}); + +type Config = { + provider: InstrumentationProvider; + providerToken: string; +}; + +export function MonitoringProvider( + props: React.PropsWithChildren<{ config?: Config }>, +) { + const provider = getMonitoringProvider(); + + if (!props.config) { + return <>{props.children}; + } + + switch (provider) { + case InstrumentationProvider.Baselime: + return ( + + {props.children} + + ); + + // sentry does not require a provider + case InstrumentationProvider.Sentry: + return <>{props.children}; + + default: + return <>{props.children}; + } +} diff --git a/packages/monitoring/src/hooks/use-monitoring.ts b/packages/monitoring/src/hooks/use-monitoring.ts index e660abc71..411d36be0 100644 --- a/packages/monitoring/src/hooks/use-monitoring.ts +++ b/packages/monitoring/src/hooks/use-monitoring.ts @@ -5,7 +5,7 @@ import { MonitoringService } from '../services/monitoring.service'; const MONITORING = getMonitoringProvider(); -let service: MonitoringService; +let serviceFactory: () => MonitoringService; /** * @name useMonitoring @@ -13,22 +13,20 @@ let service: MonitoringService; * Use Suspense to suspend while loading the service. */ export function useMonitoring() { - if (!service) { + if (!serviceFactory) { throw withMonitoringService(); } - console.log(service); - - return service; + return serviceFactory(); } async function withMonitoringService() { - service = await loadMonitoringService(); + serviceFactory = await loadMonitoringService(); } -async function loadMonitoringService() { +async function loadMonitoringService(): Promise<() => MonitoringService> { if (!MONITORING) { - return new ConsoleMonitoringService(); + return Promise.resolve(() => new ConsoleMonitoringService()); } switch (MONITORING) { @@ -45,7 +43,9 @@ async function loadMonitoringService() { } default: { - throw new Error(`Unknown instrumentation provider: ${MONITORING}`); + throw new Error( + `Unknown instrumentation provider: ${MONITORING as string}`, + ); } } } diff --git a/packages/monitoring/src/services/console-monitoring.service.ts b/packages/monitoring/src/services/console-monitoring.service.ts index 854d44cf0..1b77c2026 100644 --- a/packages/monitoring/src/services/console-monitoring.service.ts +++ b/packages/monitoring/src/services/console-monitoring.service.ts @@ -6,6 +6,8 @@ export class ConsoleMonitoringService implements MonitoringService { } captureException(error: Error) { - console.error(`Caught exception: ${JSON.stringify(error)}`); + console.error( + `[Console Monitoring] Caught exception: ${JSON.stringify(error)}`, + ); } }