Skip to content

Commit 64d36a9

Browse files
authored
feat(core)!: Always use session from isolation scope (#14860)
This PR ensures that we always take the session from the isolation scope, never from the current scope. This has the implication that we need to be sure to pass the isolation scope to `_processEvent`, as this is where the session may be marked as errored. For this, I updated the internal method `_processEvent` to take the isolation scope as last argument, as well as streamlining this slightly. I opted to update the signature of the protected `_prepareEvent` method too, and make currentScope/isolationScope required there. We already always pass this in now, so it safes a few bytes to avoid the fallback everywhere. This should not really affect users unless they overwrite the `_processEvent` method, which is internal/private anyhow, so IMHO this should be fine. I added a small note to the migration guide anyhow!
1 parent 797a4dd commit 64d36a9

File tree

11 files changed

+99
-83
lines changed

11 files changed

+99
-83
lines changed

dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import { expect } from '@playwright/test';
22
import type { SessionContext } from '@sentry/core';
33

44
import { sentryTest } from '../../../utils/fixtures';
5-
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
5+
import { getFirstSentryEnvelopeRequest, waitForSession } from '../../../utils/helpers';
66

77
sentryTest('should update session when an error is thrown.', async ({ getLocalTestUrl, page }) => {
88
const url = await getLocalTestUrl({ testDir: __dirname });
9+
910
const pageloadSession = await getFirstSentryEnvelopeRequest<SessionContext>(page, url);
10-
const updatedSession = (
11-
await Promise.all([page.locator('#throw-error').click(), getFirstSentryEnvelopeRequest<SessionContext>(page)])
12-
)[1];
11+
12+
const updatedSessionPromise = waitForSession(page);
13+
await page.locator('#throw-error').click();
14+
const updatedSession = await updatedSessionPromise;
1315

1416
expect(pageloadSession).toBeDefined();
1517
expect(pageloadSession.init).toBe(true);
@@ -25,9 +27,10 @@ sentryTest('should update session when an exception is captured.', async ({ getL
2527
const url = await getLocalTestUrl({ testDir: __dirname });
2628

2729
const pageloadSession = await getFirstSentryEnvelopeRequest<SessionContext>(page, url);
28-
const updatedSession = (
29-
await Promise.all([page.locator('#capture-exception').click(), getFirstSentryEnvelopeRequest<SessionContext>(page)])
30-
)[1];
30+
31+
const updatedSessionPromise = waitForSession(page);
32+
await page.locator('#capture-exception').click();
33+
const updatedSession = await updatedSessionPromise;
3134

3235
expect(pageloadSession).toBeDefined();
3336
expect(pageloadSession.init).toBe(true);

dev-packages/browser-integration-tests/utils/helpers.ts

+24-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
Event,
88
EventEnvelope,
99
EventEnvelopeHeaders,
10+
SessionContext,
1011
TransactionEvent,
1112
} from '@sentry/core';
1213

@@ -157,7 +158,7 @@ export const countEnvelopes = async (
157158
* @param {{ path?: string; content?: string }} impl
158159
* @return {*} {Promise<void>}
159160
*/
160-
async function runScriptInSandbox(
161+
export async function runScriptInSandbox(
161162
page: Page,
162163
impl: {
163164
path?: string;
@@ -178,7 +179,7 @@ async function runScriptInSandbox(
178179
* @param {string} [url]
179180
* @return {*} {Promise<Array<Event>>}
180181
*/
181-
async function getSentryEvents(page: Page, url?: string): Promise<Array<Event>> {
182+
export async function getSentryEvents(page: Page, url?: string): Promise<Array<Event>> {
182183
if (url) {
183184
await page.goto(url);
184185
}
@@ -250,6 +251,25 @@ export function waitForTransactionRequest(
250251
});
251252
}
252253

254+
export async function waitForSession(page: Page): Promise<SessionContext> {
255+
const req = await page.waitForRequest(req => {
256+
const postData = req.postData();
257+
if (!postData) {
258+
return false;
259+
}
260+
261+
try {
262+
const event = envelopeRequestParser<SessionContext>(req);
263+
264+
return typeof event.init === 'boolean' && event.started !== undefined;
265+
} catch {
266+
return false;
267+
}
268+
});
269+
270+
return envelopeRequestParser<SessionContext>(req);
271+
}
272+
253273
/**
254274
* We can only test tracing tests in certain bundles/packages:
255275
* - NPM (ESM, CJS)
@@ -353,7 +373,7 @@ async function getMultipleRequests<T>(
353373
/**
354374
* Wait and get multiple envelope requests at the given URL, or the current page
355375
*/
356-
async function getMultipleSentryEnvelopeRequests<T>(
376+
export async function getMultipleSentryEnvelopeRequests<T>(
357377
page: Page,
358378
count: number,
359379
options?: {
@@ -374,7 +394,7 @@ async function getMultipleSentryEnvelopeRequests<T>(
374394
* @param {string} [url]
375395
* @return {*} {Promise<T>}
376396
*/
377-
async function getFirstSentryEnvelopeRequest<T>(
397+
export async function getFirstSentryEnvelopeRequest<T>(
378398
page: Page,
379399
url?: string,
380400
requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T,
@@ -388,5 +408,3 @@ async function getFirstSentryEnvelopeRequest<T>(
388408

389409
return req;
390410
}
391-
392-
export { runScriptInSandbox, getMultipleSentryEnvelopeRequests, getFirstSentryEnvelopeRequest, getSentryEvents };

docs/migration/v8-to-v9.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,14 @@ Sentry.init({
143143
- The `flatten` export has been removed. There is no replacement.
144144
- The `urlEncode` method has been removed. There is no replacement.
145145
- The `getDomElement` method has been removed. There is no replacement.
146-
- The `Request` type has been removed. Use `RequestEventData` type instead.
147-
- The `TransactionNamingScheme` type has been removed. There is no replacement.
148146
- The `memoBuilder` method has been removed. There is no replacement.
149147

148+
#### Other/Internal Changes
149+
150+
The following changes are unlikely to affect users of the SDK. They are listed here only for completion sake, and to alert users that may be relying on internal behavior.
151+
152+
- `client._prepareEvent()` now requires a currentScope & isolationScope to be passed as last arugments
153+
150154
### `@sentry/browser`
151155

152156
- The `captureUserFeedback` method has been removed. Use `captureFeedback` instead and update the `comments` field to `message`.
@@ -210,6 +214,8 @@ This led to some duplication, where we had to keep an interface in `@sentry/type
210214
Since v9, the types have been merged into `@sentry/core`, which removed some of this duplication. This means that certain things that used to be a separate interface, will not expect an actual instance of the class/concrete implementation. This should not affect most users, unless you relied on passing things with a similar shape to internal methods. The following types are affected:
211215

212216
- `Scope` now always expects the `Scope` class
217+
- The `TransactionNamingScheme` type has been removed. There is no replacement.
218+
- The `Request` type has been removed. Use `RequestEventData` type instead.
213219
- The `IntegrationClass` type is no longer exported - it was not used anymore. Instead, use `Integration` or `IntegrationFn`.
214220

215221
# No Version Support Timeline

packages/browser/src/client.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,13 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {
105105
/**
106106
* @inheritDoc
107107
*/
108-
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
108+
protected _prepareEvent(
109+
event: Event,
110+
hint: EventHint,
111+
currentScope: Scope,
112+
isolationScope: Scope,
113+
): PromiseLike<Event | null> {
109114
event.platform = event.platform || 'javascript';
110-
return super._prepareEvent(event, hint, scope);
115+
return super._prepareEvent(event, hint, currentScope, isolationScope);
111116
}
112117
}

packages/core/src/baseclient.ts

+23-13
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,11 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
216216

217217
const sdkProcessingMetadata = event.sdkProcessingMetadata || {};
218218
const capturedSpanScope: Scope | undefined = sdkProcessingMetadata.capturedSpanScope;
219+
const capturedSpanIsolationScope: Scope | undefined = sdkProcessingMetadata.capturedSpanIsolationScope;
219220

220-
this._process(this._captureEvent(event, hintWithEventId, capturedSpanScope || currentScope));
221+
this._process(
222+
this._captureEvent(event, hintWithEventId, capturedSpanScope || currentScope, capturedSpanIsolationScope),
223+
);
221224

222225
return hintWithEventId.event_id;
223226
}
@@ -676,8 +679,8 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
676679
protected _prepareEvent(
677680
event: Event,
678681
hint: EventHint,
679-
currentScope = getCurrentScope(),
680-
isolationScope = getIsolationScope(),
682+
currentScope: Scope,
683+
isolationScope: Scope,
681684
): PromiseLike<Event | null> {
682685
const options = this.getOptions();
683686
const integrations = Object.keys(this._integrations);
@@ -718,12 +721,17 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
718721
* @param hint
719722
* @param scope
720723
*/
721-
protected _captureEvent(event: Event, hint: EventHint = {}, scope?: Scope): PromiseLike<string | undefined> {
724+
protected _captureEvent(
725+
event: Event,
726+
hint: EventHint = {},
727+
currentScope = getCurrentScope(),
728+
isolationScope = getIsolationScope(),
729+
): PromiseLike<string | undefined> {
722730
if (DEBUG_BUILD && isErrorEvent(event)) {
723731
logger.log(`Captured error event \`${getPossibleEventMessages(event)[0] || '<unknown>'}\``);
724732
}
725733

726-
return this._processEvent(event, hint, scope).then(
734+
return this._processEvent(event, hint, currentScope, isolationScope).then(
727735
finalEvent => {
728736
return finalEvent.event_id;
729737
},
@@ -756,7 +764,12 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
756764
* @param currentScope A scope containing event metadata.
757765
* @returns A SyncPromise that resolves with the event or rejects in case event was/will not be send.
758766
*/
759-
protected _processEvent(event: Event, hint: EventHint, currentScope?: Scope): PromiseLike<Event> {
767+
protected _processEvent(
768+
event: Event,
769+
hint: EventHint,
770+
currentScope: Scope,
771+
isolationScope: Scope,
772+
): PromiseLike<Event> {
760773
const options = this.getOptions();
761774
const { sampleRate } = options;
762775

@@ -779,12 +792,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
779792
);
780793
}
781794

782-
const dataCategory: DataCategory = eventType === 'replay_event' ? 'replay' : eventType;
783-
784-
const sdkProcessingMetadata = event.sdkProcessingMetadata || {};
785-
const capturedSpanIsolationScope: Scope | undefined = sdkProcessingMetadata.capturedSpanIsolationScope;
795+
const dataCategory = (eventType === 'replay_event' ? 'replay' : eventType) satisfies DataCategory;
786796

787-
return this._prepareEvent(event, hint, currentScope, capturedSpanIsolationScope)
797+
return this._prepareEvent(event, hint, currentScope, isolationScope)
788798
.then(prepared => {
789799
if (prepared === null) {
790800
this.recordDroppedEvent('event_processor', dataCategory, event);
@@ -811,8 +821,8 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
811821
throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log');
812822
}
813823

814-
const session = currentScope && currentScope.getSession();
815-
if (!isTransaction && session) {
824+
const session = currentScope.getSession() || isolationScope.getSession();
825+
if (isError && session) {
816826
this._updateSessionFromEvent(session, processedEvent);
817827
}
818828

packages/core/src/exports.ts

+1-12
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,6 @@ export function startSession(context?: SessionContext): Session {
287287
// Afterwards we set the new session on the scope
288288
isolationScope.setSession(session);
289289

290-
// TODO (v8): Remove this and only use the isolation scope(?).
291-
// For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession()
292-
currentScope.setSession(session);
293-
294290
return session;
295291
}
296292

@@ -309,22 +305,15 @@ export function endSession(): void {
309305

310306
// the session is over; take it off of the scope
311307
isolationScope.setSession();
312-
313-
// TODO (v8): Remove this and only use the isolation scope(?).
314-
// For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession()
315-
currentScope.setSession();
316308
}
317309

318310
/**
319311
* Sends the current Session on the scope
320312
*/
321313
function _sendSessionUpdate(): void {
322314
const isolationScope = getIsolationScope();
323-
const currentScope = getCurrentScope();
324315
const client = getClient();
325-
// TODO (v8): Remove currentScope and only use the isolation scope(?).
326-
// For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession()
327-
const session = currentScope.getSession() || isolationScope.getSession();
316+
const session = isolationScope.getSession();
328317
if (session && client) {
329318
client.captureSession(session);
330319
}

packages/core/src/getCurrentHubShim.ts

+2-22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { addBreadcrumb } from './breadcrumbs';
22
import { getClient, getCurrentScope, getIsolationScope, withScope } from './currentScopes';
33
import {
44
captureEvent,
5+
captureSession,
56
endSession,
67
setContext,
78
setExtra,
@@ -54,15 +55,7 @@ export function getCurrentHubShim(): Hub {
5455

5556
startSession,
5657
endSession,
57-
captureSession(end?: boolean): void {
58-
// both send the update and pull the session from the scope
59-
if (end) {
60-
return endSession();
61-
}
62-
63-
// only send the update
64-
_sendSessionUpdate();
65-
},
58+
captureSession,
6659
};
6760
}
6861

@@ -77,16 +70,3 @@ export function getCurrentHubShim(): Hub {
7770
*/
7871
// eslint-disable-next-line deprecation/deprecation
7972
export const getCurrentHub = getCurrentHubShim;
80-
81-
/**
82-
* Sends the current Session on the scope
83-
*/
84-
function _sendSessionUpdate(): void {
85-
const scope = getCurrentScope();
86-
const client = getClient();
87-
88-
const session = scope.getSession();
89-
if (client && session) {
90-
client.captureSession(session);
91-
}
92-
}

packages/core/src/server-runtime-client.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ export class ServerRuntimeClient<
166166
protected _prepareEvent(
167167
event: Event,
168168
hint: EventHint,
169-
scope?: Scope,
170-
isolationScope?: Scope,
169+
currentScope: Scope,
170+
isolationScope: Scope,
171171
): PromiseLike<Event | null> {
172172
if (this._options.platform) {
173173
event.platform = event.platform || this._options.platform;
@@ -184,7 +184,7 @@ export class ServerRuntimeClient<
184184
event.server_name = event.server_name || this._options.serverName;
185185
}
186186

187-
return super._prepareEvent(event, hint, scope, isolationScope);
187+
return super._prepareEvent(event, hint, currentScope, isolationScope);
188188
}
189189

190190
/** Extract trace information from scope */

packages/core/test/lib/serverruntimeclient.test.ts renamed to packages/core/test/lib/server-runtime-client.test.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Event, EventHint } from '../../src/types-hoist';
22

3-
import { createTransport } from '../../src';
3+
import { Scope, createTransport } from '../../src';
44
import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client';
55
import { ServerRuntimeClient } from '../../src/server-runtime-client';
66

@@ -18,14 +18,17 @@ function getDefaultClientOptions(options: Partial<ServerRuntimeClientOptions> =
1818
describe('ServerRuntimeClient', () => {
1919
let client: ServerRuntimeClient;
2020

21+
const currentScope = new Scope();
22+
const isolationScope = new Scope();
23+
2124
describe('_prepareEvent', () => {
2225
test('adds platform to event', () => {
2326
const options = getDefaultClientOptions({ dsn: PUBLIC_DSN });
2427
const client = new ServerRuntimeClient({ ...options, platform: 'blargh' });
2528

2629
const event: Event = {};
2730
const hint: EventHint = {};
28-
(client as any)._prepareEvent(event, hint);
31+
client['_prepareEvent'](event, hint, currentScope, isolationScope);
2932

3033
expect(event.platform).toEqual('blargh');
3134
});
@@ -36,7 +39,7 @@ describe('ServerRuntimeClient', () => {
3639

3740
const event: Event = {};
3841
const hint: EventHint = {};
39-
(client as any)._prepareEvent(event, hint);
42+
client['_prepareEvent'](event, hint, currentScope, isolationScope);
4043

4144
expect(event.server_name).toEqual('server');
4245
});
@@ -47,7 +50,7 @@ describe('ServerRuntimeClient', () => {
4750

4851
const event: Event = {};
4952
const hint: EventHint = {};
50-
(client as any)._prepareEvent(event, hint);
53+
client['_prepareEvent'](event, hint, currentScope, isolationScope);
5154

5255
expect(event.contexts?.runtime).toEqual({
5356
name: 'edge',
@@ -60,7 +63,7 @@ describe('ServerRuntimeClient', () => {
6063

6164
const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } };
6265
const hint: EventHint = {};
63-
(client as any)._prepareEvent(event, hint);
66+
client['_prepareEvent'](event, hint, currentScope, isolationScope);
6467

6568
expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' });
6669
expect(event.contexts?.runtime).not.toEqual({ name: 'edge' });

0 commit comments

Comments
 (0)