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
3 changes: 3 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SITE_CONFIG } from '@/lib/metadata';
import AnalyticsProvider from '@/components/providers/AnalyticsProvider';
import { ShareModalProvider } from '@/contexts/ShareContext';
import ApmProvider from '@/components/ApmProvider';
import { ApmDiagnostics } from '@/components/ApmDiagnostics';
import { GoogleAnalytics } from '@next/third-parties/google';
import { Analytics } from '@vercel/analytics/react';

Expand Down Expand Up @@ -117,6 +118,8 @@ export default async function RootLayout({
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<ApmProvider />
{/* APM Diagnostics - Shows in non-production or with ?apm-debug=true */}
<ApmDiagnostics />
<ClickProvider>
<OnchainProvider>
<NextAuthProvider session={session}>
Expand Down
161 changes: 161 additions & 0 deletions components/ApmDiagnostics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use client';

import { apm } from '@elastic/apm-rum';
import { useEffect, useState } from 'react';

interface ApmDiagnosticInfo {
isInitialized: boolean;
config: any;
serverUrl?: string;
serviceName?: string;
environment?: string;
errors: string[];
}

export function ApmDiagnostics() {
const [diagnosticInfo, setDiagnosticInfo] = useState<ApmDiagnosticInfo>({
isInitialized: false,
config: {},
errors: [],
});

useEffect(() => {
const checkApmStatus = () => {
const errors: string[] = [];

// Check if APM is initialized
const isInitialized = !!(apm as any).serviceFactory;

if (!isInitialized) {
errors.push('APM is not initialized');
}

// Check environment variables
const serverUrl = process.env.NEXT_PUBLIC_ELASTIC_APM_SERVER_URL;
const serviceName = process.env.NEXT_PUBLIC_ELASTIC_APM_SERVICE_NAME;
const environment = process.env.NEXT_PUBLIC_ENV;

if (!serverUrl) {
errors.push('NEXT_PUBLIC_ELASTIC_APM_SERVER_URL is not set');
}

// Try to get APM config
let config = {};
try {
if (isInitialized && (apm as any).serviceFactory) {
const configService = (apm as any).serviceFactory.getService('ConfigService');
if (configService) {
config = configService.config || {};
}
}
} catch (e) {
errors.push(`Failed to retrieve APM config: ${e}`);
}

// Test APM server connectivity
if (serverUrl && isInitialized) {
// Create a test transaction
const testTransaction = apm.startTransaction('diagnostics-test', 'custom');
if (testTransaction) {
testTransaction.end();
console.log('APM Diagnostics: Test transaction created');
} else {
errors.push('Failed to create test transaction');
}

// Test error capture
try {
apm.captureError(new Error('APM Diagnostics Test Error - This is intentional'));
console.log('APM Diagnostics: Test error captured');
} catch (e) {
errors.push(`Failed to capture test error: ${e}`);
}
}

setDiagnosticInfo({
isInitialized,
config,
serverUrl: serverUrl || 'Not set',
serviceName: serviceName || 'Not set',
environment: environment || 'Not set',
errors,
});
};

// Run diagnostics after a short delay to ensure APM is initialized
setTimeout(checkApmStatus, 1000);
}, []);

// Show diagnostics based on environment and errors
const showDiagnostics =
process.env.NEXT_PUBLIC_ENV !== 'production' ||
diagnosticInfo.errors.length > 0 ||
(typeof window !== 'undefined' && window.location.search.includes('apm-debug=true'));

if (!showDiagnostics) {
return null;
}

return (
<div
style={{
position: 'fixed',
bottom: '10px',
right: '10px',
background: diagnosticInfo.errors.length > 0 ? '#fee' : '#efe',
border: `2px solid ${diagnosticInfo.errors.length > 0 ? '#f00' : '#0f0'}`,
padding: '15px',
borderRadius: '8px',
zIndex: 9999,
maxWidth: '500px',
fontFamily: 'monospace',
fontSize: '11px',
}}
>
<h3 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>APM Diagnostics</h3>

<div style={{ marginBottom: '5px' }}>
<strong>Status:</strong>{' '}
{diagnosticInfo.isInitialized ? '✅ Initialized' : '❌ Not Initialized'}
</div>

<div style={{ marginBottom: '5px' }}>
<strong>Server URL:</strong> {diagnosticInfo.serverUrl}
</div>

<div style={{ marginBottom: '5px' }}>
<strong>Service Name:</strong> {diagnosticInfo.serviceName}
</div>

<div style={{ marginBottom: '5px' }}>
<strong>Environment:</strong> {diagnosticInfo.environment}
</div>

{diagnosticInfo.errors.length > 0 && (
<div style={{ marginTop: '10px' }}>
<strong>Errors:</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{diagnosticInfo.errors.map((error, index) => (
<li key={index} style={{ color: 'red' }}>
{error}
</li>
))}
</ul>
</div>
)}

{Object.keys(diagnosticInfo.config).length > 0 && (
<details style={{ marginTop: '10px' }}>
<summary style={{ cursor: 'pointer' }}>Config Details</summary>
<pre style={{ fontSize: '10px', overflow: 'auto', maxHeight: '200px' }}>
{JSON.stringify(diagnosticInfo.config, null, 2)}
</pre>
</details>
)}

<div style={{ marginTop: '10px', fontSize: '10px', color: '#666' }}>
Check browser console for APM logs. This diagnostic will send a test transaction and error.
</div>
</div>
);
}
117 changes: 117 additions & 0 deletions components/ApmTestComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { apm } from '@elastic/apm-rum';
import { useState } from 'react';

/**
* Test component to verify APM is working
* Add this component temporarily to any page to test APM
*/
export function ApmTestComponent() {
const [testResults, setTestResults] = useState<string[]>([]);

const addResult = (result: string) => {
setTestResults((prev) => [...prev, `${new Date().toLocaleTimeString()}: ${result}`]);
};

const testTransaction = () => {
try {
const transaction = apm.startTransaction('manual-test-transaction', 'custom');
if (transaction) {
// Simulate some work
setTimeout(() => {
transaction.end();
addResult('✅ Manual transaction sent successfully');
}, 100);
} else {
addResult('❌ Failed to start transaction - APM may not be initialized');
}
} catch (error) {
addResult(`❌ Transaction error: ${error}`);
}
};

const testError = () => {
try {
apm.captureError(new Error('Test error from APM test component'));
addResult('✅ Test error sent successfully');
} catch (error) {
addResult(`❌ Error capture failed: ${error}`);
}
};

const testSpan = () => {
try {
const span = apm.startSpan('test-span', 'custom');
if (span) {
setTimeout(() => {
span.end();
addResult('✅ Test span sent successfully');
}, 50);
} else {
addResult('❌ Failed to start span');
}
} catch (error) {
addResult(`❌ Span error: ${error}`);
}
};

const clearResults = () => {
setTestResults([]);
};

return (
<div
style={{
position: 'fixed',
top: '10px',
right: '10px',
background: 'white',
border: '2px solid #333',
padding: '15px',
borderRadius: '8px',
zIndex: 9999,
maxWidth: '400px',
fontFamily: 'monospace',
fontSize: '12px',
}}
>
<h3 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>APM Test Component</h3>

<div style={{ marginBottom: '10px' }}>
<button onClick={testTransaction} style={{ marginRight: '5px', padding: '5px 10px' }}>
Test Transaction
</button>
<button onClick={testError} style={{ marginRight: '5px', padding: '5px 10px' }}>
Test Error
</button>
<button onClick={testSpan} style={{ marginRight: '5px', padding: '5px 10px' }}>
Test Span
</button>
<button onClick={clearResults} style={{ padding: '5px 10px' }}>
Clear
</button>
</div>

<div
style={{
maxHeight: '200px',
overflowY: 'auto',
background: '#f5f5f5',
padding: '10px',
borderRadius: '4px',
}}
>
{testResults.length === 0 ? (
<div style={{ color: '#666' }}>No test results yet. Click buttons above to test APM.</div>
) : (
testResults.map((result, index) => (
<div key={index} style={{ marginBottom: '5px' }}>
{result}
</div>
))
)}
</div>
</div>
);
}
66 changes: 61 additions & 5 deletions services/apm.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,68 @@ export function initElasticApm() {
}

const environment = process.env.NEXT_PUBLIC_ENV || 'development';

const serviceName =
process.env.NEXT_PUBLIC_ELASTIC_APM_SERVICE_NAME || 'researchhub-development-web';

apm.init({
environment: environment,
serviceName: serviceName,
serverUrl: serverUrl,
});
try {
const apmInstance = apm.init({
environment: environment,
serviceName: serviceName,
serverUrl: serverUrl,
// Log level for debugging
logLevel: environment === 'production' ? 'warn' : 'debug',
// Disable agent in development if needed
active: true,
// Add page load transaction name
pageLoadTransactionName: window.location.pathname,
// Distributed tracing
distributedTracingOrigins: ['https://researchhub.com', 'https://*.vercel.app'],
// Error threshold
errorThrottleLimit: 20,
errorThrottleInterval: 30000,
// Transaction sample rate (1.0 = 100%)
transactionSampleRate: environment === 'production' ? 0.1 : 1.0,
// Disable instrumenting certain requests
ignoreTransactions: [
// Ignore health checks and analytics
/\/api\/health/,
/google-analytics/,
/analytics\.amplitude/,
],
// Breakdown metrics
breakdownMetrics: true,
});

// Log successful initialization
console.log(
`Elastic APM initialized successfully for ${serviceName} in ${environment} environment`
);

// Set initial user context if available
if (typeof window !== 'undefined' && window.localStorage) {
const userId = localStorage.getItem('userId');
if (userId) {
apm.setUserContext({
id: userId,
});
}
}

// Add global error handler
window.addEventListener('error', (event) => {
console.error('Global error caught by APM:', event.error);
});

// Monitor unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection caught by APM:', event.reason);
apm.captureError(new Error(`Unhandled Promise Rejection: ${event.reason}`));
});

return apmInstance;
} catch (error) {
console.error('Failed to initialize Elastic APM:', error);
return null;
}
}