diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index 8b5f0c76bc17..c733ff636c57 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -73,7 +73,7 @@ export function _INTERNAL_captureLog( } const { _experiments, release, environment } = client.getOptions(); - const { enableLogs = false, beforeSendLog } = _experiments ?? {}; + const { enableLogs = false, beforeSendLog, scopeValuesAppliedToLogs = [] } = _experiments ?? {}; if (!enableLogs) { DEBUG_BUILD && logger.warn('logging option not enabled, log will not be captured.'); return; @@ -118,6 +118,22 @@ export function _INTERNAL_captureLog( }); } + const { tags, user } = scope.getScopeData(); + scopeValuesAppliedToLogs.forEach(scopeAttribute => { + switch (scopeAttribute) { + case 'tags': + Object.entries(tags).forEach(([key, value]) => { + logAttributes[`sentry.tag.${key}`] = value; + }); + break; + case 'user': + logAttributes['user.id'] = user?.id; + logAttributes['user.email'] = user?.email; + logAttributes['user.name'] = user?.name; + break; + } + }); + const span = _getSpanForScope(scope); if (span) { // Add the parent span ID to the log attributes for trace context diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 51ccc7cdab79..66f5dff6e731 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -200,6 +200,14 @@ export interface ClientOptions Log | null; + + /** + * Setting this field to an array of attribute keys will add the corresponding attributes + * from the Sentry Scope to to all outgoing logs as log attributes. + * + * The allowed keys are `user` and `tags`. Defaults to `[]`. + */ + scopeValuesAppliedToLogs?: Array<'user' | 'tags'>; }; /** diff --git a/packages/core/test/lib/logs/exports.test.ts b/packages/core/test/lib/logs/exports.test.ts index c672373df947..c756826a8b3d 100644 --- a/packages/core/test/lib/logs/exports.test.ts +++ b/packages/core/test/lib/logs/exports.test.ts @@ -351,4 +351,102 @@ describe('_INTERNAL_captureLog', () => { expect(beforeCaptureLogSpy).toHaveBeenCalledWith('afterCaptureLog', log); beforeCaptureLogSpy.mockRestore(); }); + + it('includes scope tags in log attributes when scopeValuesAppliedToLogs includes tags', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: true, scopeValuesAppliedToLogs: ['tags'] }, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setTag('service', 'auth-service'); + scope.setTag('region', 'us-east-1'); + scope.setTag('deployment', 'blue'); + + _INTERNAL_captureLog({ level: 'info', message: 'test log with tags' }, client, scope); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: 'sentry.tag.service', value: { stringValue: 'auth-service' } }), + expect.objectContaining({ key: 'sentry.tag.region', value: { stringValue: 'us-east-1' } }), + expect.objectContaining({ key: 'sentry.tag.deployment', value: { stringValue: 'blue' } }), + ]), + ); + }); + + it('includes user data in log attributes when scopeValuesAppliedToLogs includes user', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: true, scopeValuesAppliedToLogs: ['user'] }, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setUser({ + id: '123', + email: 'test@example.com', + name: 'Test User', + }); + + _INTERNAL_captureLog({ level: 'info', message: 'test log with user data' }, client, scope); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: 'user.id', value: { stringValue: '123' } }), + expect.objectContaining({ key: 'user.email', value: { stringValue: 'test@example.com' } }), + expect.objectContaining({ key: 'user.name', value: { stringValue: 'Test User' } }), + ]), + ); + }); + + it('includes both tags and user data when scopeValuesAppliedToLogs includes both', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: true, scopeValuesAppliedToLogs: ['tags', 'user'] }, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setTag('environment', 'production'); + scope.setUser({ + id: '123', + email: 'test@example.com', + }); + + _INTERNAL_captureLog({ level: 'info', message: 'test log with tags and user data' }, client, scope); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: 'sentry.tag.environment', value: { stringValue: 'production' } }), + expect.objectContaining({ key: 'user.id', value: { stringValue: '123' } }), + expect.objectContaining({ key: 'user.email', value: { stringValue: 'test@example.com' } }), + ]), + ); + }); + + it('does not include scope values when scopeValuesAppliedToLogs is empty', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + _experiments: { enableLogs: true, scopeValuesAppliedToLogs: [] }, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setTag('environment', 'production'); + scope.setUser({ + id: '123', + email: 'test@example.com', + }); + + _INTERNAL_captureLog({ level: 'info', message: 'test log without scope values' }, client, scope); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: 'sentry.tag.environment' }), + expect.objectContaining({ key: 'user.id' }), + expect.objectContaining({ key: 'user.email' }), + ]), + ); + }); });