Skip to content

feat(core): Add experimental scopeValuesAppliedToLogs option #16020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
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
18 changes: 17 additions & 1 deletion packages/core/src/logs/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/types-hoist/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
* @returns A new log that will be sent | null.
*/
beforeSendLog?: (log: Log) => 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'>;
};

/**
Expand Down
98 changes: 98 additions & 0 deletions packages/core/test/lib/logs/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]',
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: '[email protected]' } }),
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: '[email protected]',
});

_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: '[email protected]' } }),
]),
);
});

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: '[email protected]',
});

_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' }),
]),
);
});
});
Loading