Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,23 @@ CACHE_EXPIRATION_INTERVAL=3600000
# A DSN tells a Sentry SDK where to send events so the events are associated with the correct project.
# You can find the DSN in your project settings by navigating to [Project] > Settings > Client Keys (DSN) in sentry.io.
PUBLIC_FRONTEND_SENTRY_DSN=https://example.ingest.de.sentry.io/example
BACKEND_SENTRY_DSN=https://example.ingest.de.sentry.io/example
# We recommend adjusting this value in production, or using tracesSampler
# for finer control
PUBLIC_FRONTEND_SENTRY_TRACES_SAMPLE_RATE=1.0
# This sets the sample rate to be 10%. You may want this to be 100% while
# in development and sample at a lower rate in production
PUBLIC_FRONTEND_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.1
# If the entire session is not sampled, use the below sample rate to sample
# sessions when an error occurs.
PUBLIC_FRONTEND_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=1.0

# A DSN tells a Sentry SDK where to send events so the events are associated with the correct project.
# You can find the DSN in your project settings by navigating to [Project] > Settings > Client Keys (DSN) in sentry.io.
BACKEND_SENTRY_DSN=https://example.ingest.de.sentry.io/example

# The auth token to use when uploading source maps to Sentry.
SENTRY_AUTH_TOKEN=auth_token
# The organization slug of your Sentry organization.
SENTRY_ORG=openvaa
# The project slug of your Sentry project.
SENTRY_PROJECT=openvaa-frontend
3 changes: 2 additions & 1 deletion backend/vaa-strapi/config/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const aws = require('@aws-sdk/client-ses');
import { staticSettings } from '@openvaa/app-shared';

export default ({ env }) => {
const isDev = env('NODE_ENV') === 'development';
Expand Down Expand Up @@ -85,7 +86,7 @@ export default ({ env }) => {
resolve: './src/plugins/openvaa-admin-tools'
},
sentry: {
enabled: true,
enabled: staticSettings.sentry.enabled ?? false,
config: {
dsn: env('BACKEND_SENTRY_DSN'),
environment: env('NODE_ENV'),
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ services:
BACKEND_API_TOKEN: ${BACKEND_API_TOKEN}
PUBLIC_DEBUG: ${PUBLIC_DEBUG}
PUBLIC_FRONTEND_SENTRY_DSN: ${PUBLIC_FRONTEND_SENTRY_DSN}
PUBLIC_FRONTEND_SENTRY_TRACES_SAMPLE_RATE: ${PUBLIC_FRONTEND_SENTRY_TRACES_SAMPLE_RATE}
PUBLIC_FRONTEND_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: ${PUBLIC_FRONTEND_SENTRY_REPLAYS_SESSION_SAMPLE_RATE}
PUBLIC_FRONTEND_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: ${PUBLIC_FRONTEND_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE}
SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN}
SENTRY_ORG: ${SENTRY_ORG}
SENTRY_PROJECT: ${SENTRY_PROJECT}
awslocal:
extends:
file: ./backend/vaa-strapi/docker-compose.dev.yml
Expand Down
20 changes: 19 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,22 @@ PUBLIC_BROWSER_FRONTEND_URL=http://localhost:5173
# Used to reach frontend instance from a server (differs from `PUBLIC_BROWSER_FRONTEND_URL` when using Docker)
PUBLIC_SERVER_FRONTEND_URL=http://frontend:5173

PUBLIC_FRONTEND_SENTRY_DSN=https://example.ingest.de.sentry.io/example
# A DSN tells a Sentry SDK where to send events so the events are associated with the correct project.
# You can find the DSN in your project settings by navigating to [Project] > Settings > Client Keys (DSN) in sentry.io.
PUBLIC_FRONTEND_SENTRY_DSN=https://example.ingest.de.sentry.io/example
# We recommend adjusting this value in production, or using tracesSampler
# for finer control
PUBLIC_FRONTEND_SENTRY_TRACES_SAMPLE_RATE=1.0
# This sets the sample rate to be 10%. You may want this to be 100% while
# in development and sample at a lower rate in production
PUBLIC_FRONTEND_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.1
# If the entire session is not sampled, use the below sample rate to sample
# sessions when an error occurs.
PUBLIC_FRONTEND_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=1.0

# The auth token to use when uploading source maps to Sentry.
SENTRY_AUTH_TOKEN=auth_token
# The organization slug of your Sentry organization.
SENTRY_ORG=openvaa
# The project slug of your Sentry project.
SENTRY_PROJECT=openvaa-frontend
33 changes: 20 additions & 13 deletions frontend/src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { staticSettings } from '@openvaa/app-shared';
import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import { constants } from '$lib/utils/constants';
import type { HandleClientError } from '@sveltejs/kit';

Sentry.init({
dsn: constants.PUBLIC_FRONTEND_SENTRY_DSN,
if (staticSettings.sentry.enabled) {
Sentry.init({
enabled: staticSettings.sentry.enabled,

tracesSampleRate: 1.0,
dsn: constants.PUBLIC_FRONTEND_SENTRY_DSN,

// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
tracesSampleRate: Number(constants.PUBLIC_FRONTEND_SENTRY_TRACES_SAMPLE_RATE),
replaysSessionSampleRate: Number(constants.PUBLIC_FRONTEND_SENTRY_REPLAYS_SESSION_SAMPLE_RATE),
replaysOnErrorSampleRate: Number(constants.PUBLIC_FRONTEND_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE),

// If the entire session is not sampled, use the below sample rate to sample
// sessions when an error occurs.
replaysOnErrorSampleRate: 1.0,
// If you don't want to use Session Replay, just remove the line below:
integrations: [replayIntegration()]
});
}

// If you don't want to use Session Replay, just remove the line below:
integrations: [replayIntegration()]
});
// The default behaviour is to log the error:
const handleErrorDefault = (async ({ error }) => {
console.error(error);
}) satisfies HandleClientError;

// If you have a custom error handler, pass it to `handleErrorWithSentry`
export const handleError = handleErrorWithSentry();
export const handleError = staticSettings.sentry.enabled
? handleErrorWithSentry(handleErrorDefault)
: handleErrorDefault;
217 changes: 112 additions & 105 deletions frontend/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { staticSettings } from '@openvaa/app-shared';
import * as Sentry from '@sentry/sveltekit';
import { sequence } from '@sveltejs/kit/hooks';
import { API_ROOT } from '$lib/api/adapters/apiRoute/apiRoutes';
Expand All @@ -7,131 +8,137 @@ import { constants } from '$lib/utils/constants';
import { logDebugError } from '$lib/utils/logger';
import type { Handle, HandleServerError } from '@sveltejs/kit';

Sentry.init({
dsn: constants.PUBLIC_FRONTEND_SENTRY_DSN,
tracesSampleRate: 1
});

// Handle and handleError based on sveltekit-i18n examples: https://github.com/sveltekit-i18n/lib/blob/master/examples/locale-router-advanced/src/hooks.server.js
if (staticSettings.sentry.enabled) {
Sentry.init({
dsn: constants.PUBLIC_FRONTEND_SENTRY_DSN,
tracesSampleRate: 1
});
}

/** Set to `true` to show debug log in console */
const DEBUG = false;

export const handle: Handle = sequence(Sentry.sentryHandle(), (async ({ event, resolve }) => {
const { params, route, url, request, isDataRequest } = event;
const { pathname, search } = url;
const requestedLocale = params.lang;

const supportedLocales = locales.get();
let cleanPath = requestedLocale ? pathname.replace(new RegExp(`^/${requestedLocale}`, 'i'), '') : pathname;
if (cleanPath === '') cleanPath = '/';

debug('Route: START', { params, pathname, isDataRequest, route });

//////////////////////////////////////////////////////////////////////////
// 1. Handle non-route requests
//////////////////////////////////////////////////////////////////////////

// If this request is not a route request, resolve normally
// NB. If defining API routes that should return json, test cleanPath here and resolve
if (route?.id == null || pathname == null || pathname.startsWith(API_ROOT)) {
debug('Route: RESOLVE non-route request');
return resolve(event);
}

//////////////////////////////////////////////////////////////////////////
// 2. Figure out which locale to serve
//////////////////////////////////////////////////////////////////////////

let preferredLocale: string | undefined;
let servedLocale: string | undefined;

// Get preferred locale from request headers
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const preferredLocales = parseAcceptedLanguages(acceptLanguage);
preferredLocale = matchLocale(preferredLocales, supportedLocales);
}

if (supportedLocales.length === 1) {
// No need for locale matching if there's only one locale
servedLocale = defaultLocale;
} else if (requestedLocale) {
// We use soft locale matching for route parameters, so we need to map the param to a supported one
servedLocale = matchLocale(requestedLocale, supportedLocales);
}
// If we still don't have a locale use the preferred one or the default one
servedLocale ??= preferredLocale ?? defaultLocale;
debug(
`Route: LOCALE parsed to ${servedLocale} • PATH to '${cleanPath}' (requested ${requestedLocale}, preferred ${preferredLocale})`
);

//////////////////////////////////////////////////////////////////////////
// 3. Redirect if the locale param is not the same as the served locale
//////////////////////////////////////////////////////////////////////////

if (requestedLocale !== servedLocale) {
debug(`Route: REDIRECT to locale ${servedLocale}`);
return new Response(undefined, {
headers: { location: `/${servedLocale}${cleanPath}${search}` },
status: 301
});
}

//////////////////////////////////////////////////////////////////////////
// 4. Handle candidate requests
//////////////////////////////////////////////////////////////////////////

if (pathname.startsWith(`/${servedLocale}/candidate`)) {
const token = event.cookies.get('token');

if (token && pathname.endsWith('candidate/login')) {
debug('Route: REDIRECT to home page');
return new Response(undefined, {
headers: { location: `/${servedLocale}/candidate` },
status: 303
});
export const handle: Handle = sequence(
staticSettings.sentry.enabled ? Sentry.sentryHandle() : async ({ event, resolve }) => resolve(event), // Use no-op if Sentry disabled.
(async ({ event, resolve }) => {
const { params, route, url, request, isDataRequest } = event;
const { pathname, search } = url;
const requestedLocale = params.lang;

const supportedLocales = locales.get();
let cleanPath = requestedLocale ? pathname.replace(new RegExp(`^/${requestedLocale}`, 'i'), '') : pathname;
if (cleanPath === '') cleanPath = '/';

debug('Route: START', { params, pathname, isDataRequest, route });

//////////////////////////////////////////////////////////////////////////
// 1. Handle non-route requests
//////////////////////////////////////////////////////////////////////////

// If this request is not a route request, resolve normally
// NB. If defining API routes that should return json, test cleanPath here and resolve
if (route?.id == null || pathname == null || pathname.startsWith(API_ROOT)) {
debug('Route: RESOLVE non-route request');
return resolve(event);
}

if (!token && route.id.includes('(protected)')) {
debug('Route: REDIRECT to login page');
//////////////////////////////////////////////////////////////////////////
// 2. Figure out which locale to serve
//////////////////////////////////////////////////////////////////////////

let preferredLocale: string | undefined;
let servedLocale: string | undefined;

// Get preferred locale from request headers
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const preferredLocales = parseAcceptedLanguages(acceptLanguage);
preferredLocale = matchLocale(preferredLocales, supportedLocales);
}

if (supportedLocales.length === 1) {
// No need for locale matching if there's only one locale
servedLocale = defaultLocale;
} else if (requestedLocale) {
// We use soft locale matching for route parameters, so we need to map the param to a supported one
servedLocale = matchLocale(requestedLocale, supportedLocales);
}
// If we still don't have a locale use the preferred one or the default one
servedLocale ??= preferredLocale ?? defaultLocale;
debug(
`Route: LOCALE parsed to ${servedLocale} • PATH to '${cleanPath}' (requested ${requestedLocale}, preferred ${preferredLocale})`
);

//////////////////////////////////////////////////////////////////////////
// 3. Redirect if the locale param is not the same as the served locale
//////////////////////////////////////////////////////////////////////////

if (requestedLocale !== servedLocale) {
debug(`Route: REDIRECT to locale ${servedLocale}`);
return new Response(undefined, {
headers: { location: `/${servedLocale}/candidate/login?redirectTo=${cleanPath.substring(1)}` },
status: 303
headers: { location: `/${servedLocale}${cleanPath}${search}` },
status: 301
});
}
}

//////////////////////////////////////////////////////////////////////////
// 5. Serve content in the requested locale
//////////////////////////////////////////////////////////////////////////

debug(`Route: SERVE with proper locale ${servedLocale}`);
return resolve(
{
...event,
locals: {
currentLocale: servedLocale,
preferredLocale

//////////////////////////////////////////////////////////////////////////
// 4. Handle candidate requests
//////////////////////////////////////////////////////////////////////////

if (pathname.startsWith(`/${servedLocale}/candidate`)) {
const token = event.cookies.get('token');

if (token && pathname.endsWith('candidate/login')) {
debug('Route: REDIRECT to home page');
return new Response(undefined, {
headers: { location: `/${servedLocale}/candidate` },
status: 303
});
}

if (!token && route.id.includes('(protected)')) {
debug('Route: REDIRECT to login page');
return new Response(undefined, {
headers: { location: `/${servedLocale}/candidate/login?redirectTo=${cleanPath.substring(1)}` },
status: 303
});
}
},
{
transformPageChunk: ({ html }) => html.replace('%lang%', `${servedLocale}`)
}
);
}) satisfies Handle);

export const handleError = Sentry.handleErrorWithSentry((async ({ error, event }) => {
//////////////////////////////////////////////////////////////////////////
// 5. Serve content in the requested locale
//////////////////////////////////////////////////////////////////////////

debug(`Route: SERVE with proper locale ${servedLocale}`);
return resolve(
{
...event,
locals: {
currentLocale: servedLocale,
preferredLocale
}
},
{
transformPageChunk: ({ html }) => html.replace('%lang%', `${servedLocale}`)
}
);
}) satisfies Handle
);

const handleErrorDefault = (async ({ error, event }) => {
const { locals } = event;
const currentLocale = locals?.currentLocale;
logDebugError('handleError', error);
if (currentLocale) await loadTranslations(currentLocale, 'error');
return {
message: '500'
};
}) satisfies HandleServerError);
}) satisfies HandleServerError;

export const handleError = staticSettings.sentry.enabled
? Sentry.handleErrorWithSentry(handleErrorDefault)
: handleErrorDefault;

/** Show debug message if `DEBUG` is `true` */
function debug(message: unknown, error?: unknown) {
if (DEBUG) logDebugError(message, error);
}
5 changes: 4 additions & 1 deletion frontend/src/lib/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ export const constants: Record<string, string> = {
PUBLIC_IDENTITY_PROVIDER_CLIENT_ID: env.PUBLIC_IDENTITY_PROVIDER_CLIENT_ID,
PUBLIC_IDENTITY_PROVIDER_AUTHORIZATION_ENDPOINT: env.PUBLIC_IDENTITY_PROVIDER_AUTHORIZATION_ENDPOINT,
PUBLIC_DEBUG: env.PUBLIC_DEBUG,
PUBLIC_FRONTEND_SENTRY_DSN: env.PUBLIC_FRONTEND_SENTRY_DSN
PUBLIC_FRONTEND_SENTRY_DSN: env.PUBLIC_FRONTEND_SENTRY_DSN,
PUBLIC_FRONTEND_SENTRY_TRACES_SAMPLE_RATE: env.PUBLIC_FRONTEND_SENTRY_TRACES_SAMPLE_RATE,
PUBLIC_FRONTEND_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: env.PUBLIC_FRONTEND_SENTRY_REPLAYS_SESSION_SAMPLE_RATE,
PUBLIC_FRONTEND_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: env.PUBLIC_FRONTEND_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
};
Loading