From 0a797260e30740dfd8976da061a2dba97d233d29 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 2 Feb 2026 15:27:04 +0100 Subject: [PATCH] feat: Introduce `NEXT_PUBLIC_TARGET` build argument and abstract server-side error reporting to a dedicated service. --- .env.example | 5 ++++ .gitea/workflows/deploy.yml | 1 + Dockerfile | 2 ++ lib/config.ts | 4 +-- lib/directus.ts | 35 ++++++++++++++++++-------- lib/env.ts | 6 ++--- lib/services/create-services.server.ts | 11 ++++---- 7 files changed, 43 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index e662e6cd..f09355da 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,11 @@ # ──────────────────────────────────────────────────────────────────────────── NODE_ENV=development NEXT_PUBLIC_BASE_URL=http://localhost:3000 +# TARGET is used to differentiate between environments (testing, staging, production) +# NEXT_PUBLIC_TARGET makes this information available to the frontend +NEXT_PUBLIC_TARGET=development +# TARGET is used server-side +TARGET=development # ──────────────────────────────────────────────────────────────────────────── # Analytics (Umami) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 8c806699..b1c53355 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -234,6 +234,7 @@ jobs: --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \ + --build-arg NEXT_PUBLIC_TARGET="$TARGET" \ --build-arg DIRECTUS_URL="$DIRECTUS_URL" \ -t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \ --cache-from type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache \ diff --git a/Dockerfile b/Dockerfile index e7b20d55..14a70efa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,11 +27,13 @@ ENV NEXT_TELEMETRY_DISABLED=1 ARG NEXT_PUBLIC_BASE_URL ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL +ARG NEXT_PUBLIC_TARGET ARG DIRECTUS_URL ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL +ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET ENV DIRECTUS_URL=$DIRECTUS_URL # Validate environment variables during build diff --git a/lib/config.ts b/lib/config.ts index 4857922d..88d2517d 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -18,10 +18,10 @@ function createConfig() { return { env: env.NODE_ENV, target, - isProduction: target === 'production', + isProduction: target === 'production' || !target, isStaging: target === 'staging', isTesting: target === 'testing', - isDevelopment: target === 'development' || env.NODE_ENV === 'development', + isDevelopment: target === 'development', baseUrl: env.NEXT_PUBLIC_BASE_URL, diff --git a/lib/directus.ts b/lib/directus.ts index 26f36d2c..17ab3ba7 100644 --- a/lib/directus.ts +++ b/lib/directus.ts @@ -1,6 +1,6 @@ import { createDirectus, rest, authentication, readItems, readCollections } from '@directus/sdk'; import { config } from './config'; -import * as Sentry from '@sentry/nextjs'; +import { getServerAppServices } from './services/create-services.server'; const { url, adminEmail, password, token, proxyPath, internalUrl } = config.directus; @@ -19,7 +19,7 @@ const shouldShowDevErrors = config.isTesting || config.isDevelopment; */ function formatError(error: any) { if (shouldShowDevErrors) { - return error.message || 'An unexpected error occurred.'; + return error.errors?.[0]?.message || error.message || 'An unexpected error occurred.'; } return 'A system error occurred. Our team has been notified.'; } @@ -33,7 +33,9 @@ export async function ensureAuthenticated() { try { await client.login(adminEmail, password); } catch (e) { - Sentry.captureException(e); + if (typeof window === 'undefined') { + getServerAppServices().errors.captureException(e, { part: 'directus_auth' }); + } console.error('Failed to authenticate with Directus:', e); } } @@ -78,7 +80,9 @@ export async function getProducts(locale: string = 'de') { ); return items.map((item) => mapDirectusProduct(item, locale)); } catch (error) { - Sentry.captureException(error); + if (typeof window === 'undefined') { + getServerAppServices().errors.captureException(error, { part: 'directus_get_products' }); + } console.error('Error fetching products:', error); return []; } @@ -104,7 +108,12 @@ export async function getProductBySlug(slug: string, locale: string = 'de') { if (!items || items.length === 0) return null; return mapDirectusProduct(items[0], locale); } catch (error) { - Sentry.captureException(error); + if (typeof window === 'undefined') { + getServerAppServices().errors.captureException(error, { + part: 'directus_get_product_by_slug', + slug, + }); + } console.error(`Error fetching product ${slug}:`, error); return null; } @@ -117,7 +126,9 @@ export async function checkHealth() { await ensureAuthenticated(); await client.request(readCollections()); } catch (e: any) { - Sentry.captureException(e); + if (typeof window === 'undefined') { + getServerAppServices().errors.captureException(e, { part: 'directus_health_auth' }); + } console.error('Directus authentication failed during health check:', e); return { status: 'error', @@ -133,7 +144,9 @@ export async function checkHealth() { try { await client.request(readItems('products', { limit: 1 })); } catch (e: any) { - Sentry.captureException(e); + if (typeof window === 'undefined') { + getServerAppServices().errors.captureException(e, { part: 'directus_health_schema' }); + } if ( e.message?.includes('does not exist') || e.code === 'INVALID_PAYLOAD' || @@ -142,7 +155,7 @@ export async function checkHealth() { return { status: 'error', message: shouldShowDevErrors - ? 'The "products" collection is missing or inaccessible. Please sync your data.' + ? `The "products" collection is missing or inaccessible. Error: ${e.message || 'Unknown'}` : 'Required data structures are currently unavailable.', code: 'SCHEMA_MISSING', }; @@ -150,7 +163,7 @@ export async function checkHealth() { return { status: 'error', message: shouldShowDevErrors - ? `Schema error: ${e.message}` + ? `Schema error: ${e.errors?.[0]?.message || e.message || 'Unknown error'}` : 'The data schema is currently misconfigured.', code: 'SCHEMA_ERROR', }; @@ -158,7 +171,9 @@ export async function checkHealth() { return { status: 'ok', message: 'Directus is reachable and responding.' }; } catch (error: any) { - Sentry.captureException(error); + if (typeof window === 'undefined') { + getServerAppServices().errors.captureException(error, { part: 'directus_health_critical' }); + } console.error('Directus health check failed with unexpected error:', error); return { status: 'error', diff --git a/lib/env.ts b/lib/env.ts index 64a22ee7..b90037d9 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -11,9 +11,7 @@ const preprocessEmptyString = (val: unknown) => (val === '' ? undefined : val); export const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), NEXT_PUBLIC_BASE_URL: z.preprocess(preprocessEmptyString, z.string().url()), - NEXT_PUBLIC_TARGET: z - .enum(['development', 'testing', 'staging', 'production']) - .default('development'), + NEXT_PUBLIC_TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(), // Analytics NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()), @@ -50,7 +48,7 @@ export const envSchema = z.object({ INTERNAL_DIRECTUS_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()), // Deploy Target - TARGET: z.enum(['development', 'testing', 'staging', 'production']).default('development'), + TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(), }); export type Env = z.infer; diff --git a/lib/services/create-services.server.ts b/lib/services/create-services.server.ts index a96126ee..2a5a881e 100644 --- a/lib/services/create-services.server.ts +++ b/lib/services/create-services.server.ts @@ -13,7 +13,7 @@ export function getServerAppServices(): AppServices { // Create logger first to log initialization const logger = new PinoLoggerService('server'); - + logger.info('Initializing server application services', { environment: getMaskedConfig(), timestamp: new Date().toISOString(), @@ -40,7 +40,9 @@ export function getServerAppServices(): AppServices { : new NoopErrorReportingService(); if (config.errors.glitchtip.enabled) { - logger.info('GlitchTip error reporting service initialized'); + logger.info('GlitchTip error reporting service initialized', { + dsnPresent: Boolean(config.errors.glitchtip.dsn), + }); } else { logger.info('Noop error reporting service initialized (error reporting disabled)'); } @@ -54,9 +56,8 @@ export function getServerAppServices(): AppServices { }); singleton = new AppServices(analytics, errors, cache, logger); - + logger.info('All application services initialized successfully'); - + return singleton; } -