import { AppServices } from './app-services'; import { NoopAnalyticsService } from './analytics/noop-analytics-service'; import { MemoryCacheService } from './cache/memory-cache-service'; import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service'; import { NoopErrorReportingService } from './errors/noop-error-reporting-service'; import { NoopLoggerService } from './logging/noop-logger-service'; import { PinoLoggerService } from './logging/pino-logger-service'; /** * Singleton instance of AppServices. * * In Next.js, module singletons are per-process (server) and per-tab (client). * This is sufficient for a small service layer and provides better performance * than creating new instances on every request. * * @private */ let singleton: AppServices | undefined; /** * Get the application services singleton. * * This function creates and caches the application services, including: * - Analytics service (Umami or no-op) * - Error reporting service (GlitchTip/Sentry or no-op) * - Cache service (in-memory) * * The services are configured based on environment variables: * - `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Enables Umami analytics * - `NEXT_PUBLIC_SENTRY_DSN` - Enables client-side error reporting * - `SENTRY_DSN` - Enables server-side error reporting * * @returns {AppServices} The application services singleton * * @example * ```typescript * // Get services in a client component * import { getAppServices } from '@/lib/services/create-services'; * * const services = getAppServices(); * services.analytics.track('button_click', { button_id: 'cta' }); * ``` * * @example * ```typescript * // Get services in a server component or API route * import { getAppServices } from '@/lib/services/create-services'; * * const services = getAppServices(); * await services.cache.set('key', 'value'); * ``` * * @example * ```typescript * // Automatic service selection based on environment * // If NEXT_PUBLIC_UMAMI_WEBSITE_ID is set: * // services.analytics = UmamiAnalyticsService * // If not set: * // services.analytics = NoopAnalyticsService (safe no-op) * ``` * * @see {@link UmamiAnalyticsService} for analytics implementation * @see {@link NoopAnalyticsService} for no-op fallback * @see {@link GlitchtipErrorReportingService} for error reporting * @see {@link MemoryCacheService} for caching */ export function getAppServices(): AppServices { // Return cached instance if available if (singleton) return singleton; // Create logger first to log initialization const logger = typeof window === 'undefined' ? new PinoLoggerService('server') : new NoopLoggerService(); // Log environment variables (safely masked) const envLog = { // Mask sensitive values - show only last 4 characters or '***' for empty NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID ? `***${process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID.slice(-4)}` : 'not set', UMAMI_SCRIPT_URL: process.env.UMAMI_SCRIPT_URL ?? 'not set', NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN ? `***${process.env.NEXT_PUBLIC_SENTRY_DSN.slice(-4)}` : 'not set', SENTRY_DSN: process.env.SENTRY_DSN ? `***${process.env.SENTRY_DSN.slice(-4)}` : 'not set', // Safe to show - no sensitive data NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL ?? 'not set', NODE_ENV: process.env.NODE_ENV ?? 'not set', }; // Log initialization if (typeof window === 'undefined') { // Server-side logger.info('Initializing server application services', { environment: envLog, timestamp: new Date().toISOString(), }); } else { // Client-side logger.info('Initializing client application services', { environment: envLog, timestamp: new Date().toISOString(), }); } // Determine which services to enable based on environment variables const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID); const sentryClientEnabled = Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN); const sentryServerEnabled = Boolean(process.env.SENTRY_DSN); logger.info('Service configuration', { umamiEnabled, sentryClientEnabled, sentryServerEnabled, isServer: typeof window === 'undefined', }); // Create analytics service (Umami or no-op) // Use dynamic import to avoid importing server-only code in client components const analytics = umamiEnabled ? (() => { const { UmamiAnalyticsService } = require('./analytics/umami-analytics-service'); return new UmamiAnalyticsService({ enabled: true }); })() : new NoopAnalyticsService(); if (umamiEnabled) { logger.info('Umami analytics service initialized'); } else { logger.info('Noop analytics service initialized (analytics disabled)'); } // Create error reporting service (GlitchTip/Sentry or no-op) // Server-side and client-side have separate DSNs const errors = typeof window === 'undefined' ? sentryServerEnabled ? new GlitchtipErrorReportingService({ enabled: true }) : new NoopErrorReportingService() : sentryClientEnabled ? new GlitchtipErrorReportingService({ enabled: true }) : new NoopErrorReportingService(); if (typeof window === 'undefined' && sentryServerEnabled) { logger.info('GlitchTip error reporting service initialized (server)'); } else if (typeof window !== 'undefined' && sentryClientEnabled) { logger.info('GlitchTip error reporting service initialized (client)'); } else { logger.info('Noop error reporting service initialized (error reporting disabled)'); } // IMPORTANT: This module is imported by client components. // Do not import Node-only modules (like the `redis` client) here. // Use [`getServerAppServices()`](lib/services/create-services.server.ts:1) on the server. const cache = new MemoryCacheService(); logger.info('Memory cache service initialized'); logger.info('Pino logger service initialized', { name: typeof window === 'undefined' ? 'server' : 'client', level: process.env.LOG_LEVEL ?? 'info', }); // Create and cache the singleton singleton = new AppServices(analytics, errors, cache, logger); logger.info('All application services initialized successfully'); return singleton; }