diff --git a/.env.template b/.env.template index d8aded78..8969789f 100644 --- a/.env.template +++ b/.env.template @@ -1,9 +1,9 @@ -VITE_SENTRY_ENVIRONMENT= # Sentry DSN configuration, no need to be set for local setup # Frontend: SENTRY_ORG= SENTRY_PROJECT= -VITE_SENTRY_DSN= +FRONTEND_SENTRY_DSN= +FRONTEND_SENTRY_ENVIRONMENT= # BFF: BFF_SENTRY_DSN= DYNATRACE_SCRIPT_URL= diff --git a/index.html b/index.html index 6212d53f..9156cbe7 100644 --- a/index.html +++ b/index.html @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/package-lock.json b/package-lock.json index 4484acb0..3ebef690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "globals": "^16.0.0", "prettier": "^3.5.3", "ts-node": "^10.9.2", + "tsx": "^4.20.5", "typescript": "^5.7.3", "typescript-eslint": "^8.26.1", "vite": "^6.3.4", @@ -11212,7 +11213,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -15216,7 +15217,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -16679,6 +16680,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index d8881122..dd348659 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "npm": "^11.0.0" }, "scripts": { - "dev": "node --loader ts-node/esm ./server.ts --local-dev", + "dev": "tsx ./server.ts --local-dev", "start": "node dist/server.js", - "build": "tsc && npm run build:server && vite build", + "build": "npm run build:server && npm run build:client", + "build:client": "vite build", "build:server": "tsc -p tsconfig.server.json", "lint": "eslint ./src --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint ./src --fix", @@ -91,7 +92,7 @@ "fastify-tsconfig": "^3.0.0", "globals": "^16.0.0", "prettier": "^3.5.3", - "ts-node": "^10.9.2", + "tsx": "^4.20.5", "typescript": "^5.7.3", "typescript-eslint": "^8.26.1", "vite": "^6.3.4", diff --git a/server.ts b/server.ts index 8092d868..9c025a82 100644 --- a/server.ts +++ b/server.ts @@ -24,7 +24,7 @@ if (!process.env.BFF_SENTRY_DSN || process.env.BFF_SENTRY_DSN.trim() === '') { } else { Sentry.init({ dsn: process.env.BFF_SENTRY_DSN, - environment: process.env.VITE_SENTRY_ENVIRONMENT, + environment: process.env.FRONTEND_SENTRY_ENVIRONMENT, beforeSend(event) { if (event.request && event.request.cookies) { event.request.cookies = Object.keys(event.request.cookies).reduce((acc, key) => { @@ -72,12 +72,12 @@ await fastify.register(envPlugin); let sentryHost = ''; // @ts-ignore -if (fastify.config.VITE_SENTRY_DSN && fastify.config.VITE_SENTRY_DSN.length > 0) { +if (fastify.config.FRONTEND_SENTRY_DSN && fastify.config.FRONTEND_SENTRY_DSN.length > 0) { try { // @ts-ignore - sentryHost = new URL(fastify.config.VITE_SENTRY_DSN).hostname; + sentryHost = new URL(fastify.config.FRONTEND_SENTRY_DSN).hostname; } catch { - console.log('VITE_SENTRY_DSN is not a valid URL'); + console.log('FRONTEND_SENTRY_DSN is not a valid URL'); sentryHost = ''; } } @@ -95,7 +95,9 @@ fastify.register(helmet, { contentSecurityPolicy: { directives: { 'connect-src': ["'self'", 'sdk.openui5.org', sentryHost, dynatraceOrigin], - 'script-src': isLocalDev ? ["'self'", "'unsafe-inline'", dynatraceOrigin] : ["'self'", dynatraceOrigin], + 'script-src': isLocalDev + ? ["'self'", "'unsafe-inline'", "'unsafe-eval'", sentryHost, dynatraceOrigin] + : ["'self'", sentryHost, dynatraceOrigin], // @ts-ignore 'frame-ancestors': [...fastify.config.FRAME_ANCESTORS.split(',')], }, @@ -112,6 +114,15 @@ await fastify.register(FastifyVite, { spa: true, }); +fastify.get('/sentry', function (req, reply) { + return reply.send({ + // @ts-ignore + FRONTEND_SENTRY_DSN: fastify.config.FRONTEND_SENTRY_DSN, + // @ts-ignore + FRONTEND_SENTRY_ENVIRONMENT: fastify.config.FRONTEND_SENTRY_ENVIRONMENT, + }); +}); + // @ts-ignore fastify.get('/', function (req, reply) { return reply.html(); diff --git a/server/config/env.ts b/server/config/env.ts index 7f063198..1bcfe43a 100644 --- a/server/config/env.ts +++ b/server/config/env.ts @@ -30,8 +30,8 @@ const schema = { FEEDBACK_URL_LINK: { type: 'string' }, FRAME_ANCESTORS: { type: 'string' }, BFF_SENTRY_DSN: { type: 'string' }, - VITE_SENTRY_DSN: { type: 'string' }, - VITE_SENTRY_ENVIRONMENT: { type: 'string' }, + FRONTEND_SENTRY_DSN: { type: 'string' }, + FRONTEND_SENTRY_ENVIRONMENT: { type: 'string' }, // System variables NODE_ENV: { type: 'string', enum: ['development', 'production'] }, diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts new file mode 100644 index 00000000..48d85270 --- /dev/null +++ b/src/lib/sentry.ts @@ -0,0 +1,65 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; +import { Routes, useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from 'react-router-dom'; + +// Define proper typing for Sentry configuration +interface SentryConfig { + FRONTEND_SENTRY_DSN: string; + FRONTEND_SENTRY_ENVIRONMENT: string; +} + +// Validate sentryConfig format +function isValidSentryConfig(config: unknown): config is SentryConfig { + if (typeof config !== 'object' || config === null) { + return false; + } + + const typedConfig = config as Record; + return ( + typeof typedConfig.FRONTEND_SENTRY_DSN === 'string' && + typedConfig.FRONTEND_SENTRY_DSN.length > 0 && + typeof typedConfig.FRONTEND_SENTRY_ENVIRONMENT === 'string' && + typedConfig.FRONTEND_SENTRY_ENVIRONMENT.length > 0 + ); +} + +// Fetch Sentry configuration from server +async function fetchSentryConfig(): Promise { + try { + const response = await fetch('/sentry'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.warn('Failed to fetch Sentry configuration:', error); + return null; + } +} + +// Initialize Sentry and return the wrapped Routes component +export async function initializeSentry(): Promise { + const sentryConfig = await fetchSentryConfig(); + + if (!isValidSentryConfig(sentryConfig)) { + console.warn('Invalid or missing Sentry configuration, continuing without Sentry integration'); + return Routes; + } + + // Initialize Sentry with valid configuration + Sentry.init({ + dsn: sentryConfig.FRONTEND_SENTRY_DSN, + environment: sentryConfig.FRONTEND_SENTRY_ENVIRONMENT, + integrations: [ + Sentry.reactRouterV7BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ], + }); + + return Sentry.withSentryReactRouterV7Routing(Routes); +} diff --git a/src/mount.ts b/src/mount.ts index 74e9e5f5..481d77af 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -1,29 +1,11 @@ import { createRoot } from 'react-dom/client'; import { createApp } from './main.tsx'; -import * as Sentry from '@sentry/react'; -import React from 'react'; -import { Routes, useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from 'react-router-dom'; +import { initializeSentry } from './lib/sentry.ts'; -let sentryRoutes = Routes; -if (import.meta.env.VITE_SENTRY_DSN && import.meta.env.VITE_SENTRY_DSN.length > 0) { - Sentry.init({ - dsn: import.meta.env.VITE_SENTRY_DSN, - environment: import.meta.env.VITE_SENTRY_ENVIRONMENT, - integrations: [ - Sentry.reactRouterV7BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ], - }); +// Initialize Sentry and get the Routes component (with or without Sentry integration) +const SentryRoutes = await initializeSentry(); - sentryRoutes = Sentry.withSentryReactRouterV7Routing(Routes); -} - -export const SentryRoutes = sentryRoutes; +export { SentryRoutes }; const root = createRoot(document.getElementById('root')!); root.render(createApp()); diff --git a/vite.config.js b/vite.config.js index 3239d584..34b19992 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,6 +14,7 @@ export default defineConfig({ sentryVitePlugin({ org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, + telemetry: false, reactComponentAnnotation: { enabled: true, }, @@ -22,5 +23,6 @@ export default defineConfig({ build: { sourcemap: true, + target: 'esnext', // Support top-level await }, });