Skip to content

Commit 6a13da7

Browse files
committed
introducing Sentry Logging
1 parent 8af04ab commit 6a13da7

File tree

9 files changed

+1807
-78
lines changed

9 files changed

+1807
-78
lines changed

.env.example

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ATLASCODE_FX3_TIMEOUT="2000"
1111
#
1212

1313
# Syntax: comma separated settings in the format <name>=<boolean>
14-
ATLASCODE_FF_OVERRIDES="feature_flag_1=true, feature_flag_2=false"
14+
ATLASCODE_FF_OVERRIDES="atlascode-sentry-logging=true, feature_flag_2=false"
1515

1616
# This is for experiments that controls a boolean value.
1717
# Syntax: comma separated settings in the format <name>=<boolean>
@@ -20,4 +20,13 @@ ATLASCODE_EXP_OVERRIDES_BOOL="exp1_name=true, exp2_name=false"
2020
# This is for experiments that controls a string value.
2121
# Syntax: comma separated settings in the format <name>=<string>
2222
# NOTE: the string is unquoted, and a comma within the string is not supported.
23-
ATLASCODE_EXP_OVERRIDES_STRING="exp3_name=something, exp4_name=something else"
23+
ATLASCODE_EXP_OVERRIDES_STRING="exp3_name=something, exp4_name=something else"
24+
25+
#
26+
# Sentry Error Tracking Configuration (optional)
27+
# Only set these values if you want to enable Sentry error tracking
28+
#
29+
SENTRY_ENABLED=false
30+
SENTRY_DSN=https://example.com
31+
SENTRY_ENVIRONMENT=production
32+
SENTRY_SAMPLE_RATE=1.0

package-lock.json

Lines changed: 1104 additions & 76 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,6 +1670,8 @@
16701670
"@mui/styles": "^5.18.0",
16711671
"@mui/utils": "^5.17.1",
16721672
"@segment/analytics-node": "^2.1.3",
1673+
"@sentry/browser": "^9.40.0",
1674+
"@sentry/node": "^9.40.0",
16731675
"@speed-highlight/core": "^1.2.7",
16741676
"@vscode/codicons": "^0.0.40",
16751677
"@vscode/webview-ui-toolkit": "^1.4.0",

src/container.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { RovoDevCodeActionProvider } from './rovo-dev/rovoDevCodeActionProvider'
4040
import { RovoDevProcessManager } from './rovo-dev/rovoDevProcessManager';
4141
import { RovoDevWebviewProvider } from './rovo-dev/rovoDevWebviewProvider';
4242
import { RovoDevLogger } from './rovo-dev/util/rovoDevLogger';
43+
import { SentryConfig, SentryService } from './sentry';
4344
import { SiteManager } from './siteManager';
4445
import { AtlascodeUriHandler, SETTINGS_URL } from './uriHandler';
4546
import { Experiments, FeatureFlagClient, FeatureFlagClientInitError, Features } from './util/featureFlags';
@@ -260,6 +261,25 @@ export class Container {
260261
this._onboardingProvider = new OnboardingProvider();
261262

262263
this.refreshRovoDev(context);
264+
265+
// Initialize Sentry for error tracking
266+
try {
267+
const sentryConfig: SentryConfig = {
268+
enabled: process.env.SENTRY_ENABLED === 'true',
269+
dsn: process.env.SENTRY_DSN,
270+
environment: process.env.SENTRY_ENVIRONMENT || 'development',
271+
sampleRate: parseFloat(process.env.SENTRY_SAMPLE_RATE || '1.0'),
272+
atlasCodeVersion: version,
273+
};
274+
275+
if (sentryConfig.enabled && sentryConfig.dsn) {
276+
await SentryService.getInstance().initialize(sentryConfig);
277+
Logger.info('Sentry initialized successfully');
278+
}
279+
} catch (error) {
280+
Logger.error(error as Error, 'Failed to initialize Sentry');
281+
// Continue with extension startup regardless
282+
}
263283
}
264284

265285
private static async initializeFeatureFlagClient() {

src/logger.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ jest.mock('./container', () => ({
3333
},
3434
}));
3535

36+
// Mock SentryService
37+
jest.mock('./sentry', () => ({
38+
SentryService: {
39+
getInstance: jest.fn(),
40+
},
41+
}));
42+
3643
const mockContainerIsDebugging = () => {
3744
(Container.isDebugging as any) = true;
3845
};
@@ -50,6 +57,13 @@ describe('Logger', () => {
5057
// Reset mocks
5158
jest.clearAllMocks();
5259

60+
// Setup default SentryService mock to avoid crashes in other tests
61+
const { SentryService } = require('./sentry');
62+
(SentryService.getInstance as jest.Mock).mockReturnValue({
63+
isInitialized: jest.fn().mockReturnValue(false),
64+
captureException: jest.fn(),
65+
});
66+
5367
mockOutputChannel = expansionCastTo<LogOutputChannel>({
5468
dispose: jest.fn(),
5569
append: jest.fn(),
@@ -323,4 +337,100 @@ describe('Logger', () => {
323337
}
324338
});
325339
});
340+
341+
describe('Sentry integration', () => {
342+
let mockSentryService: any;
343+
344+
beforeEach(() => {
345+
// Set up Logger with Error level
346+
(configuration.initializing as jest.Mock).mockReturnValue(true);
347+
(configuration.get as jest.Mock).mockReturnValue(OutputLevel.Errors);
348+
Logger.configure(expansionCastTo<ExtensionContext>({ subscriptions: [] }));
349+
350+
// Setup the mocked Sentry service to be returned by getInstance
351+
mockSentryService = {
352+
isInitialized: jest.fn(),
353+
captureException: jest.fn(),
354+
};
355+
const { SentryService } = require('./sentry');
356+
(SentryService.getInstance as jest.Mock).mockReturnValue(mockSentryService);
357+
mockSentryService.isInitialized.mockReturnValue(true);
358+
});
359+
360+
it('should not capture exception to Sentry when not initialized', () => {
361+
mockSentryService.isInitialized.mockReturnValue(false);
362+
363+
const testError = new Error('test error');
364+
Logger.error(testError, 'Error message');
365+
366+
expect(mockSentryService.captureException).not.toHaveBeenCalled();
367+
});
368+
369+
it('should include capturedBy in Sentry tags', () => {
370+
mockSentryService.isInitialized.mockReturnValue(true);
371+
372+
const testError = new Error('test error');
373+
Logger.error(testError, 'Error message');
374+
375+
expect(mockSentryService.captureException).toHaveBeenCalledWith(
376+
testError,
377+
expect.objectContaining({
378+
tags: expect.objectContaining({
379+
capturedBy: expect.any(String),
380+
}),
381+
}),
382+
);
383+
});
384+
385+
it('should include params in Sentry extra context', () => {
386+
mockSentryService.isInitialized.mockReturnValue(true);
387+
388+
const testError = new Error('test error');
389+
Logger.error(testError, 'Error message', 'param1', 'param2');
390+
391+
expect(mockSentryService.captureException).toHaveBeenCalledWith(
392+
testError,
393+
expect.objectContaining({
394+
extra: expect.objectContaining({
395+
params: ['param1', 'param2'],
396+
}),
397+
}),
398+
);
399+
});
400+
401+
it('should handle Sentry errors gracefully and continue logging', () => {
402+
mockSentryService.isInitialized.mockReturnValue(true);
403+
mockSentryService.captureException.mockImplementation(() => {
404+
throw new Error('Sentry failed');
405+
});
406+
407+
const testError = new Error('test error');
408+
409+
// Should not throw
410+
expect(() => {
411+
Logger.error(testError, 'Error message');
412+
}).not.toThrow();
413+
414+
// Should still log to output channel
415+
expect(mockOutputChannel.appendLine).toHaveBeenCalled();
416+
});
417+
418+
it('should call Sentry before logging to output channel', () => {
419+
mockSentryService.isInitialized.mockReturnValue(true);
420+
const callOrder: string[] = [];
421+
422+
mockSentryService.captureException.mockImplementation(() => {
423+
callOrder.push('sentry');
424+
});
425+
426+
(mockOutputChannel.appendLine as jest.Mock).mockImplementation(() => {
427+
callOrder.push('output');
428+
});
429+
430+
const testError = new Error('test error');
431+
Logger.error(testError, 'Error message');
432+
433+
expect(callOrder).toEqual(['sentry', 'output']);
434+
});
435+
});
326436
});

src/logger.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,25 @@ export class Logger {
156156
): void {
157157
Logger._onError.fire({ error: ex, errorMessage, capturedBy, params, productArea });
158158

159+
// Capture to Sentry if initialized
160+
try {
161+
const { SentryService } = require('./sentry');
162+
if (SentryService.getInstance().isInitialized()) {
163+
SentryService.getInstance().captureException(ex, {
164+
tags: {
165+
productArea: productArea || 'unknown',
166+
capturedBy: capturedBy || 'unknown',
167+
},
168+
extra: {
169+
errorMessage,
170+
params,
171+
},
172+
});
173+
}
174+
} catch (e) {
175+
console.error('Failed to initialize Sentry:', e);
176+
}
177+
159178
if (this.level === OutputLevel.Silent) {
160179
return;
161180
}

0 commit comments

Comments
 (0)