Skip to content

Commit 8184a54

Browse files
authored
feat: Explicit Scope for captureException and captureMessage (#2627)
* feat: Explicit Scope for captureException and captureMessage
1 parent ace2651 commit 8184a54

File tree

13 files changed

+719
-361
lines changed

13 files changed

+719
-361
lines changed

CHANGELOG.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
6+
- [minimal/core] feat: Allow for explicit scope through 2nd argument to `captureException/captureMessage` (#2627)
67

78
## 5.16.0
89

@@ -12,15 +13,15 @@
1213
- [browser] fix: Call wrapped `RequestAnimationFrame` with correct context (#2570)
1314
- [node] fix: Prevent reading the same source file multiple times (#2569)
1415
- [integrations] feat: Vue performance monitoring (#2571)
15-
- [apm] fix: Use proper type name for op #2584
16-
- [core] fix: sent_at for envelope headers to use same clock #2597
17-
- [apm] fix: Improve bundle size by moving span status to @sentry/apm #2589
18-
- [apm] feat: No longer discard transactions instead mark them deadline exceeded #2588
19-
- [apm] feat: Introduce `Sentry.startTransaction` and `Transaction.startChild` #2600
20-
- [apm] feat: Transactions no longer go through `beforeSend` #2600
16+
- [apm] fix: Use proper type name for op (#2584)
17+
- [core] fix: sent_at for envelope headers to use same clock (#2597)
18+
- [apm] fix: Improve bundle size by moving span status to @sentry/apm (#2589)
19+
- [apm] feat: No longer discard transactions instead mark them deadline exceeded (#2588)
20+
- [apm] feat: Introduce `Sentry.startTransaction` and `Transaction.startChild` (#2600)
21+
- [apm] feat: Transactions no longer go through `beforeSend` (#2600)
2122
- [browser] fix: Emit Sentry Request breadcrumbs from inside the client (#2615)
22-
- [apm] fix: No longer debounce IdleTransaction #2618
23-
- [apm] feat: Add pageload transaction option + fixes #2623
23+
- [apm] fix: No longer debounce IdleTransaction (#2618)
24+
- [apm] feat: Add pageload transaction option + fixes (#2623)
2425

2526
## 5.15.5
2627

packages/browser/test/package/test-code.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,19 @@ Sentry.addBreadcrumb({
5050

5151
// Capture methods
5252
Sentry.captureException(new Error('foo'));
53+
Sentry.captureException(new Error('foo'), {
54+
tags: {
55+
foo: 1,
56+
},
57+
});
58+
Sentry.captureException(new Error('foo'), scope => scope);
5359
Sentry.captureMessage('bar');
60+
Sentry.captureMessage('bar', {
61+
tags: {
62+
foo: 1,
63+
},
64+
});
65+
Sentry.captureMessage('bar', scope => scope);
5466

5567
// Scope behavior
5668
Sentry.withScope(scope => {

packages/core/src/baseclient.ts

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Scope } from '@sentry/hub';
2-
import { Client, Event, EventHint, Integration, IntegrationClass, Options, SdkInfo, Severity } from '@sentry/types';
2+
import { Client, Event, EventHint, Integration, IntegrationClass, Options, Severity } from '@sentry/types';
33
import {
44
Dsn,
55
isPrimitive,
@@ -248,54 +248,31 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
248248
* @returns A new event with more information.
249249
*/
250250
protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike<Event | null> {
251-
const { environment, release, dist, maxValueLength = 250, normalizeDepth = 3 } = this.getOptions();
252-
253-
const prepared: Event = { ...event };
254-
255-
if (!prepared.timestamp) {
256-
prepared.timestamp = timestampWithMs();
257-
}
258-
259-
if (prepared.environment === undefined && environment !== undefined) {
260-
prepared.environment = environment;
261-
}
262-
263-
if (prepared.release === undefined && release !== undefined) {
264-
prepared.release = release;
265-
}
266-
267-
if (prepared.dist === undefined && dist !== undefined) {
268-
prepared.dist = dist;
269-
}
270-
271-
if (prepared.message) {
272-
prepared.message = truncate(prepared.message, maxValueLength);
273-
}
274-
275-
const exception = prepared.exception && prepared.exception.values && prepared.exception.values[0];
276-
if (exception && exception.value) {
277-
exception.value = truncate(exception.value, maxValueLength);
278-
}
251+
const { normalizeDepth = 3 } = this.getOptions();
252+
const prepared: Event = {
253+
...event,
254+
event_id: event.event_id || (hint && hint.event_id ? hint.event_id : uuid4()),
255+
timestamp: event.timestamp || timestampWithMs(),
256+
};
279257

280-
const request = prepared.request;
281-
if (request && request.url) {
282-
request.url = truncate(request.url, maxValueLength);
283-
}
258+
this._applyClientOptions(prepared);
259+
this._applyIntegrationsMetadata(prepared);
284260

285-
if (prepared.event_id === undefined) {
286-
prepared.event_id = hint && hint.event_id ? hint.event_id : uuid4();
261+
// If we have scope given to us, use it as the base for further modifications.
262+
// This allows us to prevent unnecessary copying of data if `captureContext` is not provided.
263+
let finalScope = scope;
264+
if (hint && hint.captureContext) {
265+
finalScope = Scope.clone(finalScope).update(hint.captureContext);
287266
}
288267

289-
this._addIntegrations(prepared.sdk);
290-
291268
// We prepare the result here with a resolved Event.
292269
let result = SyncPromise.resolve<Event | null>(prepared);
293270

294271
// This should be the last thing called, since we want that
295272
// {@link Hub.addEventProcessor} gets the finished prepared event.
296-
if (scope) {
273+
if (finalScope) {
297274
// In case we have a hub we reassign it.
298-
result = scope.applyToEvent(prepared, hint);
275+
result = finalScope.applyToEvent(prepared, hint);
299276
}
300277

301278
return result.then(evt => {
@@ -345,11 +322,48 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
345322
};
346323
}
347324

325+
/**
326+
* Enhances event using the client configuration.
327+
* It takes care of all "static" values like environment, release and `dist`,
328+
* as well as truncating overly long values.
329+
* @param event event instance to be enhanced
330+
*/
331+
protected _applyClientOptions(event: Event): void {
332+
const { environment, release, dist, maxValueLength = 250 } = this.getOptions();
333+
334+
if (event.environment === undefined && environment !== undefined) {
335+
event.environment = environment;
336+
}
337+
338+
if (event.release === undefined && release !== undefined) {
339+
event.release = release;
340+
}
341+
342+
if (event.dist === undefined && dist !== undefined) {
343+
event.dist = dist;
344+
}
345+
346+
if (event.message) {
347+
event.message = truncate(event.message, maxValueLength);
348+
}
349+
350+
const exception = event.exception && event.exception.values && event.exception.values[0];
351+
if (exception && exception.value) {
352+
exception.value = truncate(exception.value, maxValueLength);
353+
}
354+
355+
const request = event.request;
356+
if (request && request.url) {
357+
request.url = truncate(request.url, maxValueLength);
358+
}
359+
}
360+
348361
/**
349362
* This function adds all used integrations to the SDK info in the event.
350363
* @param sdkInfo The sdkInfo of the event that will be filled with all integrations.
351364
*/
352-
protected _addIntegrations(sdkInfo?: SdkInfo): void {
365+
protected _applyIntegrationsMetadata(event: Event): void {
366+
const sdkInfo = event.sdk;
353367
const integrationsArray = Object.keys(this._integrations);
354368
if (sdkInfo && integrationsArray.length > 0) {
355369
sdkInfo.integrations = integrationsArray;

packages/core/test/lib/base.test.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Hub, Scope } from '@sentry/hub';
2-
import { Event } from '@sentry/types';
2+
import { Event, Severity } from '@sentry/types';
33
import { SentryError } from '@sentry/utils';
44

55
import { TestBackend } from '../mocks/backend';
@@ -163,9 +163,8 @@ describe('BaseClient', () => {
163163
});
164164
});
165165

166-
describe('captures', () => {
166+
describe('captureException', () => {
167167
test('captures and sends exceptions', () => {
168-
expect.assertions(1);
169168
const client = new TestClient({ dsn: PUBLIC_DSN });
170169
client.captureException(new Error('test exception'));
171170
expect(TestBackend.instance!.event).toEqual({
@@ -182,19 +181,69 @@ describe('BaseClient', () => {
182181
});
183182
});
184183

184+
test('allows for providing explicit scope', () => {
185+
const client = new TestClient({ dsn: PUBLIC_DSN });
186+
const scope = new Scope();
187+
scope.setExtra('foo', 'wat');
188+
client.captureException(
189+
new Error('test exception'),
190+
{
191+
captureContext: {
192+
extra: {
193+
bar: 'wat',
194+
},
195+
},
196+
},
197+
scope,
198+
);
199+
expect(TestBackend.instance!.event).toEqual(
200+
expect.objectContaining({
201+
extra: {
202+
bar: 'wat',
203+
foo: 'wat',
204+
},
205+
}),
206+
);
207+
});
208+
209+
test('allows for clearing data from existing scope if explicit one does so in a callback function', () => {
210+
const client = new TestClient({ dsn: PUBLIC_DSN });
211+
const scope = new Scope();
212+
scope.setExtra('foo', 'wat');
213+
client.captureException(
214+
new Error('test exception'),
215+
{
216+
captureContext: s => {
217+
s.clear();
218+
s.setExtra('bar', 'wat');
219+
return s;
220+
},
221+
},
222+
scope,
223+
);
224+
expect(TestBackend.instance!.event).toEqual(
225+
expect.objectContaining({
226+
extra: {
227+
bar: 'wat',
228+
},
229+
}),
230+
);
231+
});
232+
});
233+
234+
describe('captureMessage', () => {
185235
test('captures and sends messages', () => {
186-
expect.assertions(1);
187236
const client = new TestClient({ dsn: PUBLIC_DSN });
188237
client.captureMessage('test message');
189238
expect(TestBackend.instance!.event).toEqual({
190239
event_id: '42',
240+
level: 'info',
191241
message: 'test message',
192242
timestamp: 2020,
193243
});
194244
});
195245

196246
test('should call eventFromException if input to captureMessage is not a primitive', () => {
197-
expect.assertions(2);
198247
const client = new TestClient({ dsn: PUBLIC_DSN });
199248
const spy = jest.spyOn(TestBackend.instance!, 'eventFromException');
200249

@@ -209,6 +258,33 @@ describe('BaseClient', () => {
209258
client.captureMessage([] as any);
210259
expect(spy.mock.calls.length).toEqual(2);
211260
});
261+
262+
test('allows for providing explicit scope', () => {
263+
const client = new TestClient({ dsn: PUBLIC_DSN });
264+
const scope = new Scope();
265+
scope.setExtra('foo', 'wat');
266+
client.captureMessage(
267+
'test message',
268+
Severity.Warning,
269+
{
270+
captureContext: {
271+
extra: {
272+
bar: 'wat',
273+
},
274+
},
275+
},
276+
scope,
277+
);
278+
expect(TestBackend.instance!.event).toEqual(
279+
expect.objectContaining({
280+
extra: {
281+
bar: 'wat',
282+
foo: 'wat',
283+
},
284+
level: 'warning',
285+
}),
286+
);
287+
});
212288
});
213289

214290
describe('captureEvent() / prepareEvent()', () => {

packages/core/test/mocks/backend.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Event, Options, Transport } from '@sentry/types';
1+
import { Event, Options, Severity, Transport } from '@sentry/types';
22
import { SyncPromise } from '@sentry/utils';
33

44
import { BaseBackend } from '../../src/basebackend';
@@ -50,8 +50,8 @@ export class TestBackend extends BaseBackend<TestOptions> {
5050
});
5151
}
5252

53-
public eventFromMessage(message: string): PromiseLike<Event> {
54-
return SyncPromise.resolve({ message });
53+
public eventFromMessage(message: string, level: Severity = Severity.Info): PromiseLike<Event> {
54+
return SyncPromise.resolve({ message, level });
5555
}
5656

5757
public sendEvent(event: Event): void {

0 commit comments

Comments
 (0)