All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m51s
301 lines
8.3 KiB
TypeScript
301 lines
8.3 KiB
TypeScript
// src/services/sentry.client.test.ts
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
// Use vi.hoisted to define mocks that need to be available before vi.mock runs
|
|
const { mockSentry, mockLogger } = vi.hoisted(() => ({
|
|
mockSentry: {
|
|
init: vi.fn(),
|
|
captureException: vi.fn(() => 'mock-event-id'),
|
|
captureMessage: vi.fn(() => 'mock-message-id'),
|
|
setContext: vi.fn(),
|
|
setUser: vi.fn(),
|
|
addBreadcrumb: vi.fn(),
|
|
breadcrumbsIntegration: vi.fn(() => ({})),
|
|
ErrorBoundary: vi.fn(),
|
|
},
|
|
mockLogger: {
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@sentry/react', () => mockSentry);
|
|
|
|
vi.mock('./logger.client', () => ({
|
|
logger: mockLogger,
|
|
default: mockLogger,
|
|
}));
|
|
|
|
describe('sentry.client', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('with Sentry disabled (default test environment)', () => {
|
|
// The test environment has Sentry disabled by default (VITE_SENTRY_DSN not set)
|
|
// Import the module fresh for each test
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
});
|
|
|
|
it('should have isSentryConfigured as false in test environment', async () => {
|
|
const { isSentryConfigured } = await import('./sentry.client');
|
|
expect(isSentryConfigured).toBe(false);
|
|
});
|
|
|
|
it('should not initialize Sentry when not configured', async () => {
|
|
const { initSentry, isSentryConfigured } = await import('./sentry.client');
|
|
|
|
initSentry();
|
|
|
|
// When Sentry is not configured, Sentry.init should NOT be called
|
|
if (!isSentryConfigured) {
|
|
expect(mockSentry.init).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('should return undefined from captureException when not configured', async () => {
|
|
const { captureException } = await import('./sentry.client');
|
|
|
|
const result = captureException(new Error('test error'));
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(mockSentry.captureException).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return undefined from captureMessage when not configured', async () => {
|
|
const { captureMessage } = await import('./sentry.client');
|
|
|
|
const result = captureMessage('test message');
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(mockSentry.captureMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not set user when not configured', async () => {
|
|
const { setUser } = await import('./sentry.client');
|
|
|
|
setUser({ id: '123', email: 'test@example.com' });
|
|
|
|
expect(mockSentry.setUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not add breadcrumb when not configured', async () => {
|
|
const { addBreadcrumb } = await import('./sentry.client');
|
|
|
|
addBreadcrumb({ message: 'test breadcrumb', category: 'test' });
|
|
|
|
expect(mockSentry.addBreadcrumb).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Sentry re-export', () => {
|
|
it('should re-export Sentry object', async () => {
|
|
const { Sentry } = await import('./sentry.client');
|
|
|
|
expect(Sentry).toBeDefined();
|
|
expect(Sentry.init).toBeDefined();
|
|
expect(Sentry.captureException).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('initSentry beforeSend filter logic', () => {
|
|
// Test the beforeSend filter function logic in isolation
|
|
// This tests the filter that's passed to Sentry.init
|
|
|
|
it('should filter out browser extension errors', () => {
|
|
// Simulate the beforeSend logic from the implementation
|
|
const filterExtensionErrors = (event: {
|
|
exception?: {
|
|
values?: Array<{
|
|
stacktrace?: {
|
|
frames?: Array<{ filename?: string }>;
|
|
};
|
|
}>;
|
|
};
|
|
}) => {
|
|
if (
|
|
event.exception?.values?.[0]?.stacktrace?.frames?.some((frame) =>
|
|
frame.filename?.includes('extension://'),
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
return event;
|
|
};
|
|
|
|
const extensionError = {
|
|
exception: {
|
|
values: [
|
|
{
|
|
stacktrace: {
|
|
frames: [{ filename: 'chrome-extension://abc123/script.js' }],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
expect(filterExtensionErrors(extensionError)).toBeNull();
|
|
});
|
|
|
|
it('should allow normal errors through', () => {
|
|
const filterExtensionErrors = (event: {
|
|
exception?: {
|
|
values?: Array<{
|
|
stacktrace?: {
|
|
frames?: Array<{ filename?: string }>;
|
|
};
|
|
}>;
|
|
};
|
|
}) => {
|
|
if (
|
|
event.exception?.values?.[0]?.stacktrace?.frames?.some((frame) =>
|
|
frame.filename?.includes('extension://'),
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
return event;
|
|
};
|
|
|
|
const normalError = {
|
|
exception: {
|
|
values: [
|
|
{
|
|
stacktrace: {
|
|
frames: [{ filename: '/app/src/index.js' }],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
expect(filterExtensionErrors(normalError)).toBe(normalError);
|
|
});
|
|
|
|
it('should handle events without exception property', () => {
|
|
const filterExtensionErrors = (event: {
|
|
exception?: {
|
|
values?: Array<{
|
|
stacktrace?: {
|
|
frames?: Array<{ filename?: string }>;
|
|
};
|
|
}>;
|
|
};
|
|
}) => {
|
|
if (
|
|
event.exception?.values?.[0]?.stacktrace?.frames?.some((frame) =>
|
|
frame.filename?.includes('extension://'),
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
return event;
|
|
};
|
|
|
|
const eventWithoutException = { message: 'test' };
|
|
|
|
expect(filterExtensionErrors(eventWithoutException as any)).toBe(eventWithoutException);
|
|
});
|
|
|
|
it('should handle firefox extension URLs', () => {
|
|
const filterExtensionErrors = (event: {
|
|
exception?: {
|
|
values?: Array<{
|
|
stacktrace?: {
|
|
frames?: Array<{ filename?: string }>;
|
|
};
|
|
}>;
|
|
};
|
|
}) => {
|
|
if (
|
|
event.exception?.values?.[0]?.stacktrace?.frames?.some((frame) =>
|
|
frame.filename?.includes('extension://'),
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
return event;
|
|
};
|
|
|
|
const firefoxExtensionError = {
|
|
exception: {
|
|
values: [
|
|
{
|
|
stacktrace: {
|
|
frames: [{ filename: 'moz-extension://abc123/script.js' }],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
expect(filterExtensionErrors(firefoxExtensionError)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('isSentryConfigured logic', () => {
|
|
// Test the logic that determines if Sentry is configured
|
|
// This mirrors the implementation: !!config.sentry.dsn && config.sentry.enabled
|
|
|
|
it('should return false when DSN is empty', () => {
|
|
const dsn = '';
|
|
const enabled = true;
|
|
const result = !!dsn && enabled;
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when enabled is false', () => {
|
|
const dsn = 'https://test@sentry.io/123';
|
|
const enabled = false;
|
|
const result = !!dsn && enabled;
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when DSN is set and enabled is true', () => {
|
|
const dsn = 'https://test@sentry.io/123';
|
|
const enabled = true;
|
|
const result = !!dsn && enabled;
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when DSN is undefined', () => {
|
|
const dsn = undefined;
|
|
const enabled = true;
|
|
const result = !!dsn && enabled;
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('captureException logic', () => {
|
|
it('should set context before capturing when context is provided', () => {
|
|
// This tests the conditional context setting logic
|
|
const context = { userId: '123' };
|
|
const shouldSetContext = !!context;
|
|
expect(shouldSetContext).toBe(true);
|
|
});
|
|
|
|
it('should not set context when not provided', () => {
|
|
const context = undefined;
|
|
const shouldSetContext = !!context;
|
|
expect(shouldSetContext).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('captureMessage default level', () => {
|
|
it('should default to info level', () => {
|
|
// Test the default parameter behavior
|
|
const defaultLevel = 'info';
|
|
expect(defaultLevel).toBe('info');
|
|
});
|
|
});
|
|
});
|