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)}`, + ); } }