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.
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryStreamedHydration>
|
||||
<I18nProvider settings={i18nSettings} resolver={i18nResolver}>
|
||||
<CaptchaProvider>
|
||||
<CaptchaTokenSetter siteKey={captchaSiteKey} />
|
||||
<MonitoringProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryStreamedHydration>
|
||||
<I18nProvider settings={i18nSettings} resolver={i18nResolver}>
|
||||
<CaptchaProvider>
|
||||
<CaptchaTokenSetter siteKey={captchaSiteKey} />
|
||||
|
||||
<AuthChangeListener appHomePath={pathsConfig.app.home}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
defaultTheme={theme}
|
||||
enableColorScheme={false}
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</AuthChangeListener>
|
||||
</CaptchaProvider>
|
||||
</I18nProvider>
|
||||
</ReactQueryStreamedHydration>
|
||||
</QueryClientProvider>
|
||||
<AuthChangeListener appHomePath={pathsConfig.app.home}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
defaultTheme={theme}
|
||||
enableColorScheme={false}
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</AuthChangeListener>
|
||||
</CaptchaProvider>
|
||||
</I18nProvider>
|
||||
</ReactQueryStreamedHydration>
|
||||
</QueryClientProvider>
|
||||
</MonitoringProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaselimeRum } from '@baselime/react-rum';
|
||||
|
||||
export function BaselineProvider({
|
||||
export function BaselimeProvider({
|
||||
children,
|
||||
apiKey,
|
||||
enableWebVitals,
|
||||
|
||||
@@ -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 extends { id: string }>(info: Info) {}
|
||||
identifyUser<Info extends { id: string }>(info: Info) {
|
||||
this.userId = info.id;
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedError(error: Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './error-boundary';
|
||||
export * from './provider';
|
||||
|
||||
45
packages/monitoring/src/components/provider.tsx
Normal file
45
packages/monitoring/src/components/provider.tsx
Normal file
@@ -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 (
|
||||
<BaselimeProvider apiKey={props.config?.providerToken} enableWebVitals>
|
||||
{props.children}
|
||||
</BaselimeProvider>
|
||||
);
|
||||
|
||||
// sentry does not require a provider
|
||||
case InstrumentationProvider.Sentry:
|
||||
return <>{props.children}</>;
|
||||
|
||||
default:
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
}
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user