Skip to content
Closed
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
2 changes: 2 additions & 0 deletions apps/webapp/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "./globals.css";
import { Toaster } from "@/components/ui/toaster";
import { ErrorBoundary } from "@/components/error/ErrorBoundary";
import { ErrorProvider, setupGlobalErrorHandling } from "@/components/error/ErrorProvider";
import { EnvironmentLogger } from "@/components/debug/EnvironmentLogger";

// Set up global error handling and performance monitoring
if (typeof window !== 'undefined') {
Expand Down Expand Up @@ -48,6 +49,7 @@ export default function RootLayout({
return (
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
<body className="min-h-screen bg-background font-sans antialiased">
<EnvironmentLogger enabled={process.env.NODE_ENV === 'development'} />
<ErrorBoundary showError={process.env.NODE_ENV === 'development'}>
<ErrorProvider>
{children}
Expand Down
59 changes: 59 additions & 0 deletions apps/webapp/components/debug/EnvironmentLogger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';

import { useEffect } from 'react';

interface EnvironmentLoggerProps {
enabled?: boolean;
}

export function EnvironmentLogger({ enabled = true }: EnvironmentLoggerProps) {
useEffect(() => {
if (!enabled) {
return;
}

// Get all environment variables accessible on the client side
const clientEnvVars: Record<string, string> = {};

// In Next.js, only environment variables prefixed with NEXT_PUBLIC_ are available on the client
// We'll also check process.env for any other variables that might be exposed
for (const key in process.env) {
if (key.startsWith('NEXT_PUBLIC_') || process.env[key] !== undefined) {
clientEnvVars[key] = process.env[key] || '';
}
}

// Also log some runtime information that might be useful for debugging
const debugInfo = {
timestamp: new Date().toISOString(),
userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'SSR',
currentUrl: typeof window !== 'undefined' ? window.location.href : 'SSR',
nodeEnv: process.env.NODE_ENV,
environmentVariables: clientEnvVars,
};

console.group('🔍 Client-side Environment Debug Info');
console.log('Timestamp:', debugInfo.timestamp);
console.log('Node Environment:', debugInfo.nodeEnv);
console.log('User Agent:', debugInfo.userAgent);
console.log('Current URL:', debugInfo.currentUrl);
console.log('Environment Variables Count:', Object.keys(clientEnvVars).length);

console.group('📋 Environment Variables:');
Object.entries(clientEnvVars).forEach(([key, value]) => {
// Mask sensitive values for security
const maskedValue = key.toLowerCase().includes('key') || key.toLowerCase().includes('secret') || key.toLowerCase().includes('token')
? value.substring(0, 4) + '****' + value.substring(value.length - 4)
: value;
console.log(`${key}:`, maskedValue);
});
console.groupEnd();

console.log('Full debug object:', debugInfo);
console.groupEnd();

}, [enabled]);

// This component doesn't render anything visible
return null;
}
15 changes: 15 additions & 0 deletions apps/webapp/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Next.js instrumentation file
* This runs when the Next.js server starts up
*/

export async function register() {
// Only run instrumentation in development mode
if (process.env.NODE_ENV === 'development') {
// Dynamic import to avoid bundling in production
const { initializeServerEnvironmentLogging } = await import('./lib/debug/ServerEnvironmentLogger');

// Initialize server environment logging
initializeServerEnvironmentLogging();
}
}
156 changes: 156 additions & 0 deletions apps/webapp/lib/debug/ServerEnvironmentLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Server-side environment logger for debugging during development
* Only runs in development mode and logs environment variables at server startup
*/

interface EnvironmentCategory {
name: string;
icon: string;
variables: Record<string, string>;
}

interface ServerEnvironmentInfo {
timestamp: string;
workingDirectory: string;
nodeEnv: string;
categories: EnvironmentCategory[];
totalCount: number;
}

/**
* Masks sensitive values in environment variables
*/
function maskSensitiveValue(key: string, value: string): string {
const sensitiveKeys = ['key', 'secret', 'token', 'password', 'auth'];
const isSensitive = sensitiveKeys.some(sensitiveKey =>
key.toLowerCase().includes(sensitiveKey)
);

if (!isSensitive || !value || value.length < 8) {
return value;
}

return value.substring(0, 4) + '****' + value.substring(value.length - 4);
}

/**
* Categorizes environment variables into logical groups
*/
function categorizeEnvironmentVariables(): EnvironmentCategory[] {
const env = process.env;
const categories: EnvironmentCategory[] = [];

// NEXT_PUBLIC_* variables
const nextPublicVars: Record<string, string> = {};

// Firebase-related variables
const firebaseVars: Record<string, string> = {};

// Other development variables
const otherVars: Record<string, string> = {};

for (const [key, value] of Object.entries(env)) {
if (!value) continue;

const maskedValue = maskSensitiveValue(key, value);

if (key.startsWith('NEXT_PUBLIC_')) {
nextPublicVars[key] = maskedValue;
} else if (key.includes('FIREBASE') || key.includes('FIRE_')) {
firebaseVars[key] = maskedValue;
} else if (['NODE_ENV', 'PORT', 'HOSTNAME', 'PWD'].includes(key)) {
otherVars[key] = maskedValue;
}
}

if (Object.keys(nextPublicVars).length > 0) {
categories.push({
name: 'Next.js Public Variables',
icon: '🌐',
variables: nextPublicVars
});
}

if (Object.keys(firebaseVars).length > 0) {
categories.push({
name: 'Firebase Variables',
icon: '🔥',
variables: firebaseVars
});
}

if (Object.keys(otherVars).length > 0) {
categories.push({
name: 'Development Variables',
icon: '⚙️',
variables: otherVars
});
}

return categories;
}

/**
* Gets comprehensive server environment information
*/
function getServerEnvironmentInfo(): ServerEnvironmentInfo {
const categories = categorizeEnvironmentVariables();
const totalCount = categories.reduce((sum, cat) => sum + Object.keys(cat.variables).length, 0);

return {
timestamp: new Date().toISOString(),
workingDirectory: process.cwd(),
nodeEnv: process.env.NODE_ENV || 'unknown',
categories,
totalCount
};
}

/**
* Logs server environment information to the console
* Only runs in development mode
*/
export function logServerEnvironment(): void {
// Only log in development mode
if (process.env.NODE_ENV !== 'development') {
return;
}

const envInfo = getServerEnvironmentInfo();

console.log('\n' + '='.repeat(60));
console.group('🖥️ Server-Side Environment Debug Info');
console.log('📅 Timestamp:', envInfo.timestamp);
console.log('🌍 Node Environment:', envInfo.nodeEnv);
console.log('📁 Working Directory:', envInfo.workingDirectory);
console.log('📊 Total Environment Variables:', envInfo.totalCount);
console.log('');

// Log each category
envInfo.categories.forEach(category => {
console.group(`${category.icon} ${category.name} (${Object.keys(category.variables).length})`);

Object.entries(category.variables).forEach(([key, value]) => {
console.log(` ${key}:`, value);
});

console.groupEnd();
});

console.groupEnd();
console.log('='.repeat(60) + '\n');
}

/**
* Initialize server environment logging
* Call this at server startup to log environment variables
*/
export function initializeServerEnvironmentLogging(): void {
if (process.env.NODE_ENV === 'development') {
// Log immediately when called
logServerEnvironment();

// Also log a startup message
console.log('🚀 Server-side environment logging initialized for development mode');
}
}
113 changes: 113 additions & 0 deletions apps/webapp/src/__tests__/ServerEnvironmentLogger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

describe('ServerEnvironmentLogger', () => {
let consoleGroupSpy: ReturnType<typeof vi.spyOn>;
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let consoleGroupEndSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
// Mock console methods
consoleGroupSpy = vi.spyOn(console, 'group').mockImplementation(() => {});
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleGroupEndSpy = vi.spyOn(console, 'groupEnd').mockImplementation(() => {});
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('logServerEnvironment', () => {
it('should not log anything when NODE_ENV is not development', async () => {
// Set NODE_ENV to production
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';

const { logServerEnvironment } = await import('../../lib/debug/ServerEnvironmentLogger');

logServerEnvironment();

expect(consoleGroupSpy).not.toHaveBeenCalled();
expect(consoleLogSpy).not.toHaveBeenCalled();

// Restore original NODE_ENV
process.env.NODE_ENV = originalNodeEnv;
});

it('should log environment info when NODE_ENV is development', async () => {
// Set NODE_ENV to development
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';

// Set some test environment variables
process.env.NEXT_PUBLIC_TEST = 'test-value';
process.env.FIREBASE_API_KEY = 'test-firebase-key';

const { logServerEnvironment } = await import('../../lib/debug/ServerEnvironmentLogger');

logServerEnvironment();

expect(consoleGroupSpy).toHaveBeenCalledWith('🖥️ Server-Side Environment Debug Info');
expect(consoleLogSpy).toHaveBeenCalledWith('🌍 Node Environment:', 'development');
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('📁 Working Directory:'), expect.any(String));

// Clean up
delete process.env.NEXT_PUBLIC_TEST;
delete process.env.FIREBASE_API_KEY;
process.env.NODE_ENV = originalNodeEnv;
});

it('should mask sensitive environment variables', async () => {
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';

// Set a sensitive environment variable
process.env.NEXT_PUBLIC_API_KEY = 'very-secret-api-key-value';

const { logServerEnvironment } = await import('../../lib/debug/ServerEnvironmentLogger');

logServerEnvironment();

// Check that the sensitive value was masked
const calls = consoleLogSpy.mock.calls;
const keyCall = calls.find(call => call[0] === ' NEXT_PUBLIC_API_KEY:');
expect(keyCall).toBeDefined();
if (keyCall) {
expect(keyCall[1]).toMatch(/^very\*\*\*\*alue$/);
}

// Clean up
delete process.env.NEXT_PUBLIC_API_KEY;
process.env.NODE_ENV = originalNodeEnv;
});
});

describe('initializeServerEnvironmentLogging', () => {
it('should call logServerEnvironment in development mode', async () => {
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';

const { initializeServerEnvironmentLogging } = await import('../../lib/debug/ServerEnvironmentLogger');

initializeServerEnvironmentLogging();

expect(consoleGroupSpy).toHaveBeenCalled();
expect(consoleLogSpy).toHaveBeenCalledWith('🚀 Server-side environment logging initialized for development mode');

process.env.NODE_ENV = originalNodeEnv;
});

it('should not log anything in non-development mode', async () => {
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';

const { initializeServerEnvironmentLogging } = await import('../../lib/debug/ServerEnvironmentLogger');

initializeServerEnvironmentLogging();

expect(consoleGroupSpy).not.toHaveBeenCalled();
expect(consoleLogSpy).not.toHaveBeenCalled();

process.env.NODE_ENV = originalNodeEnv;
});
});
});
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"packages/*"
],
"scripts": {
"dev:webapp": "mkdir -p logs && cd apps/webapp && FORCE_COLOR=1 pnpm dev 2>&1 | tee >(while IFS= read -r line; do echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $line\"; done > ../../logs/webapp-dev.$(date '+%Y-%m-%d-%H').log)",
"dev:mcp-api": "mkdir -p logs && cd apps/mcp-api && FORCE_COLOR=1 pnpm dev 2>&1 | tee >(while IFS= read -r line; do echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $line\"; done > ../../logs/mcp-api-dev.$(date '+%Y-%m-%d-%H').log)",
"dev:emulators:with-data": "mkdir -p logs && firebase emulators:start --import=.data/emulators/firebase-data --only auth,firestore 2>&1 | tee logs/firebase-emulators.$(date '+%Y-%m-%d-%H').log",
"dev:webapp": "mkdir -p logs && cd apps/webapp && FORCE_COLOR=1 pnpm dev 2>&1 | tee >(while IFS= read -r line; do echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $line\"; done > ../../logs/webapp-dev.$(date '+%Y-%m-%dT%H').log)",
"dev:mcp-api": "mkdir -p logs && cd apps/mcp-api && FORCE_COLOR=1 pnpm dev 2>&1 | tee >(while IFS= read -r line; do echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $line\"; done > ../../logs/mcp-api-dev.$(date '+%Y-%m-%dT%H').log)",
"dev:emulators:with-data": "mkdir -p logs && firebase emulators:start --import=.data/emulators/firebase-data --only auth,firestore 2>&1 | tee logs/firebase-emulators.$(date '+%Y-%m-%dT%H').log",
"build": "turbo build",
"lint": "turbo lint",
"lint:fix": "turbo lint:fix",
Expand Down
Loading
Loading