diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts
index 6bba4eb66a0a..eec3edf45083 100644
--- a/packages/replay/src/replay.ts
+++ b/packages/replay/src/replay.ts
@@ -18,7 +18,8 @@ import { handleKeyboardEvent } from './coreHandlers/handleKeyboardEvent';
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
import { createEventBuffer } from './eventBuffer';
import { clearSession } from './session/clearSession';
-import { getSession } from './session/getSession';
+import { loadOrCreateSession } from './session/loadOrCreateSession';
+import { maybeRefreshSession } from './session/maybeRefreshSession';
import { saveSession } from './session/saveSession';
import type {
AddEventResult,
@@ -228,13 +229,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// Otherwise if there is _any_ sample rate set, try to load an existing
// session, or create a new one.
- const isSessionSampled = this._loadAndCheckSession();
-
- if (!isSessionSampled) {
- // This should only occur if `errorSampleRate` is 0 and was unsampled for
- // session-based replay. In this case there is nothing to do.
- return;
- }
+ this._initializeSessionForSampling();
if (!this.session) {
// This should not happen, something wrong has occurred
@@ -242,14 +237,16 @@ export class ReplayContainer implements ReplayContainerInterface {
return;
}
- if (this.session.sampled && this.session.sampled !== 'session') {
- // If not sampled as session-based, then recording mode will be `buffer`
- // Note that we don't explicitly check if `sampled === 'buffer'` because we
- // could have sessions from Session storage that are still `error` from
- // prior SDK version.
- this.recordingMode = 'buffer';
+ if (this.session.sampled === false) {
+ // This should only occur if `errorSampleRate` is 0 and was unsampled for
+ // session-based replay. In this case there is nothing to do.
+ return;
}
+ // If segmentId > 0, it means we've previously already captured this session
+ // In this case, we still want to continue in `session` recording mode
+ this.recordingMode = this.session.sampled === 'buffer' && this.session.segmentId === 0 ? 'buffer' : 'session';
+
logInfoNextTick(
`[Replay] Starting replay in ${this.recordingMode} mode`,
this._options._experiments.traceInternals,
@@ -276,19 +273,20 @@ export class ReplayContainer implements ReplayContainerInterface {
logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
- const previousSessionId = this.session && this.session.id;
-
- const { session } = getSession({
- timeouts: this.timeouts,
- stickySession: Boolean(this._options.stickySession),
- currentSession: this.session,
- // This is intentional: create a new session-based replay when calling `start()`
- sessionSampleRate: 1,
- allowBuffering: false,
- traceInternals: this._options._experiments.traceInternals,
- });
+ const session = loadOrCreateSession(
+ this.session,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: this._options.stickySession,
+ // This is intentional: create a new session-based replay when calling `start()`
+ sessionSampleRate: 1,
+ allowBuffering: false,
+ },
+ );
- session.previousSessionId = previousSessionId;
this.session = session;
this._initializeRecording();
@@ -305,18 +303,19 @@ export class ReplayContainer implements ReplayContainerInterface {
logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
- const previousSessionId = this.session && this.session.id;
-
- const { session } = getSession({
- timeouts: this.timeouts,
- stickySession: Boolean(this._options.stickySession),
- currentSession: this.session,
- sessionSampleRate: 0,
- allowBuffering: true,
- traceInternals: this._options._experiments.traceInternals,
- });
+ const session = loadOrCreateSession(
+ this.session,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: this._options.stickySession,
+ sessionSampleRate: 0,
+ allowBuffering: true,
+ },
+ );
- session.previousSessionId = previousSessionId;
this.session = session;
this.recordingMode = 'buffer';
@@ -427,7 +426,7 @@ export class ReplayContainer implements ReplayContainerInterface {
* new DOM checkout.`
*/
public resume(): void {
- if (!this._isPaused || !this._loadAndCheckSession()) {
+ if (!this._isPaused || !this._checkSession()) {
return;
}
@@ -535,7 +534,7 @@ export class ReplayContainer implements ReplayContainerInterface {
if (!this._stopRecording) {
// Create a new session, otherwise when the user action is flushed, it
// will get rejected due to an expired session.
- if (!this._loadAndCheckSession()) {
+ if (!this._checkSession()) {
return;
}
@@ -634,7 +633,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// --- There is recent user activity --- //
// This will create a new session if expired, based on expiry length
- if (!this._loadAndCheckSession()) {
+ if (!this._checkSession()) {
return;
}
@@ -751,31 +750,63 @@ export class ReplayContainer implements ReplayContainerInterface {
/**
* Loads (or refreshes) the current session.
+ */
+ private _initializeSessionForSampling(): void {
+ // Whenever there is _any_ error sample rate, we always allow buffering
+ // Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
+ const allowBuffering = this._options.errorSampleRate > 0;
+
+ const session = loadOrCreateSession(
+ this.session,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: this._options.stickySession,
+ sessionSampleRate: this._options.sessionSampleRate,
+ allowBuffering,
+ },
+ );
+
+ this.session = session;
+ }
+
+ /**
+ * Checks and potentially refreshes the current session.
* Returns false if session is not recorded.
*/
- private _loadAndCheckSession(): boolean {
- const { type, session } = getSession({
- timeouts: this.timeouts,
- stickySession: Boolean(this._options.stickySession),
- currentSession: this.session,
- sessionSampleRate: this._options.sessionSampleRate,
- allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
- traceInternals: this._options._experiments.traceInternals,
- });
+ private _checkSession(): boolean {
+ // If there is no session yet, we do not want to refresh anything
+ // This should generally not happen, but to be safe....
+ if (!this.session) {
+ return false;
+ }
+
+ const currentSession = this.session;
+
+ const newSession = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: Boolean(this._options.stickySession),
+ sessionSampleRate: this._options.sessionSampleRate,
+ allowBuffering: this._options.errorSampleRate > 0,
+ },
+ );
+
+ const isNew = newSession.id !== currentSession.id;
// If session was newly created (i.e. was not loaded from storage), then
// enable flag to create the root replay
- if (type === 'new') {
+ if (isNew) {
this.setInitialState();
+ this.session = newSession;
}
- const currentSessionId = this.getSessionId();
- if (session.id !== currentSessionId) {
- session.previousSessionId = currentSessionId;
- }
-
- this.session = session;
-
if (!this.session.sampled) {
void this.stop({ reason: 'session not refreshed' });
return false;
diff --git a/packages/replay/src/session/Session.ts b/packages/replay/src/session/Session.ts
index e373d50dfaa2..80b32aed345a 100644
--- a/packages/replay/src/session/Session.ts
+++ b/packages/replay/src/session/Session.ts
@@ -14,6 +14,7 @@ export function makeSession(session: Partial
& { sampled: Sampled }): S
const segmentId = session.segmentId || 0;
const sampled = session.sampled;
const shouldRefresh = typeof session.shouldRefresh === 'boolean' ? session.shouldRefresh : true;
+ const previousSessionId = session.previousSessionId;
return {
id,
@@ -22,5 +23,6 @@ export function makeSession(session: Partial & { sampled: Sampled }): S
segmentId,
sampled,
shouldRefresh,
+ previousSessionId,
};
}
diff --git a/packages/replay/src/session/createSession.ts b/packages/replay/src/session/createSession.ts
index 0ecd940a4dae..2cb9c0853b09 100644
--- a/packages/replay/src/session/createSession.ts
+++ b/packages/replay/src/session/createSession.ts
@@ -15,10 +15,14 @@ export function getSessionSampleType(sessionSampleRate: number, allowBuffering:
* that all replays will be saved to as attachments. Currently, we only expect
* one of these Sentry events per "replay session".
*/
-export function createSession({ sessionSampleRate, allowBuffering, stickySession = false }: SessionOptions): Session {
+export function createSession(
+ { sessionSampleRate, allowBuffering, stickySession = false }: SessionOptions,
+ { previousSessionId }: { previousSessionId?: string } = {},
+): Session {
const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
const session = makeSession({
sampled,
+ previousSessionId,
});
if (stickySession) {
diff --git a/packages/replay/src/session/getSession.ts b/packages/replay/src/session/getSession.ts
deleted file mode 100644
index da3184f05296..000000000000
--- a/packages/replay/src/session/getSession.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import type { Session, SessionOptions, Timeouts } from '../types';
-import { isSessionExpired } from '../util/isSessionExpired';
-import { logInfoNextTick } from '../util/log';
-import { createSession } from './createSession';
-import { fetchSession } from './fetchSession';
-import { makeSession } from './Session';
-
-interface GetSessionParams extends SessionOptions {
- timeouts: Timeouts;
-
- /**
- * The current session (e.g. if stickySession is off)
- */
- currentSession?: Session;
-
- traceInternals?: boolean;
-}
-
-/**
- * Get or create a session
- */
-export function getSession({
- timeouts,
- currentSession,
- stickySession,
- sessionSampleRate,
- allowBuffering,
- traceInternals,
-}: GetSessionParams): { type: 'new' | 'saved'; session: Session } {
- // If session exists and is passed, use it instead of always hitting session storage
- const session = currentSession || (stickySession && fetchSession(traceInternals));
-
- if (session) {
- // If there is a session, check if it is valid (e.g. "last activity" time
- // should be within the "session idle time", and "session started" time is
- // within "max session time").
- const isExpired = isSessionExpired(session, timeouts);
-
- if (!isExpired || (allowBuffering && session.shouldRefresh)) {
- return { type: 'saved', session };
- } else if (!session.shouldRefresh) {
- // This is the case if we have an error session that is completed
- // (=triggered an error). Session will continue as session-based replay,
- // and when this session is expired, it will not be renewed until user
- // reloads.
- const discardedSession = makeSession({ sampled: false });
- logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
- return { type: 'new', session: discardedSession };
- } else {
- logInfoNextTick('[Replay] Session has expired', traceInternals);
- }
- // Otherwise continue to create a new session
- }
-
- const newSession = createSession({
- stickySession,
- sessionSampleRate,
- allowBuffering,
- });
- logInfoNextTick('[Replay] Created new session', traceInternals);
-
- return { type: 'new', session: newSession };
-}
diff --git a/packages/replay/src/session/loadOrCreateSession.ts b/packages/replay/src/session/loadOrCreateSession.ts
new file mode 100644
index 000000000000..9695eef56102
--- /dev/null
+++ b/packages/replay/src/session/loadOrCreateSession.ts
@@ -0,0 +1,32 @@
+import type { Session, SessionOptions, Timeouts } from '../types';
+import { logInfoNextTick } from '../util/log';
+import { createSession } from './createSession';
+import { fetchSession } from './fetchSession';
+import { maybeRefreshSession } from './maybeRefreshSession';
+
+/**
+ * Get or create a session, when initializing the replay.
+ * Returns a session that may be unsampled.
+ */
+export function loadOrCreateSession(
+ currentSession: Session | undefined,
+ {
+ timeouts,
+ traceInternals,
+ }: {
+ timeouts: Timeouts;
+ traceInternals?: boolean;
+ },
+ sessionOptions: SessionOptions,
+): Session {
+ // If session exists and is passed, use it instead of always hitting session storage
+ const existingSession = currentSession || (sessionOptions.stickySession && fetchSession(traceInternals));
+
+ // No session exists yet, just create a new one
+ if (!existingSession) {
+ logInfoNextTick('[Replay] Created new session', traceInternals);
+ return createSession(sessionOptions);
+ }
+
+ return maybeRefreshSession(existingSession, { timeouts, traceInternals }, sessionOptions);
+}
diff --git a/packages/replay/src/session/maybeRefreshSession.ts b/packages/replay/src/session/maybeRefreshSession.ts
new file mode 100644
index 000000000000..51e4925d074d
--- /dev/null
+++ b/packages/replay/src/session/maybeRefreshSession.ts
@@ -0,0 +1,48 @@
+import type { Session, SessionOptions, Timeouts } from '../types';
+import { isSessionExpired } from '../util/isSessionExpired';
+import { logInfoNextTick } from '../util/log';
+import { createSession } from './createSession';
+import { makeSession } from './Session';
+
+/**
+ * Check a session, and either return it or a refreshed version of it.
+ * The refreshed version may be unsampled.
+ * You can check if the session has changed by comparing the session IDs.
+ */
+export function maybeRefreshSession(
+ session: Session,
+ {
+ timeouts,
+ traceInternals,
+ }: {
+ timeouts: Timeouts;
+ traceInternals?: boolean;
+ },
+ sessionOptions: SessionOptions,
+): Session {
+ // If not expired, all good, just keep the session
+ if (!isSessionExpired(session, timeouts)) {
+ return session;
+ }
+
+ const isBuffering = session.sampled === 'buffer';
+
+ // If we are buffering & the session may be refreshed, just return it
+ if (isBuffering && session.shouldRefresh) {
+ return session;
+ }
+
+ // If we are buffering & the session may not be refreshed (=it was converted to session previously already)
+ // We return an unsampled new session
+ if (isBuffering) {
+ logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
+ return makeSession({ sampled: false });
+ }
+
+ // Else, we are not buffering, and the session is expired, so we need to create a new one
+ logInfoNextTick('[Replay] Session has expired, creating new one...', traceInternals);
+
+ const newSession = createSession(sessionOptions, { previousSessionId: session.id });
+
+ return newSession;
+}
diff --git a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
index 6bf33f182e66..4059b71fe195 100644
--- a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
+++ b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
@@ -84,7 +84,8 @@ describe('Integration | beforeAddRecordingEvent', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockSendReplayRequest.mockClear();
});
@@ -94,7 +95,6 @@ describe('Integration | beforeAddRecordingEvent', () => {
await new Promise(process.nextTick);
jest.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
- replay['_loadAndCheckSession']();
});
afterAll(() => {
diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts
index 777cb437f7e3..e56edae0f723 100644
--- a/packages/replay/test/integration/errorSampleRate.test.ts
+++ b/packages/replay/test/integration/errorSampleRate.test.ts
@@ -295,10 +295,11 @@ describe('Integration | errorSampleRate', () => {
it('does not upload a replay event if error is not sampled', async () => {
// We are trying to replicate the case where error rate is 0 and session
// rate is > 0, we can't set them both to 0 otherwise
- // `_loadAndCheckSession` is not called when initializing the plugin.
+ // `_initializeSessionForSampling` is not called when initializing the plugin.
replay.stop();
replay['_options']['errorSampleRate'] = 0;
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
jest.runAllTimers();
await new Promise(process.nextTick);
diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts
index b95faffa59da..c90f8ceed125 100644
--- a/packages/replay/test/integration/events.test.ts
+++ b/packages/replay/test/integration/events.test.ts
@@ -40,7 +40,8 @@ describe('Integration | events', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockTransportSend.mockClear();
});
@@ -93,7 +94,8 @@ describe('Integration | events', () => {
it('has correct timestamps when there are events earlier than initial timestamp', async function () {
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockTransportSend.mockClear();
Object.defineProperty(document, 'visibilityState', {
configurable: true,
diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts
index 29ce2ba527fd..6f2d3b7d8ccd 100644
--- a/packages/replay/test/integration/flush.test.ts
+++ b/packages/replay/test/integration/flush.test.ts
@@ -85,7 +85,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
if (replay.eventBuffer) {
jest.spyOn(replay.eventBuffer, 'finish');
@@ -276,7 +277,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
// click happens first
domHandler({
@@ -307,10 +309,12 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
- // No-op _loadAndCheckSession to avoid us resetting the session for this test
- const _tmp = replay['_loadAndCheckSession'];
- replay['_loadAndCheckSession'] = () => {
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
+
+ // No-op _checkSession to avoid us resetting the session for this test
+ const _tmp = replay['_checkSession'];
+ replay['_checkSession'] = () => {
return true;
};
@@ -331,7 +335,7 @@ describe('Integration | flush', () => {
expect(mockSendReplay).toHaveBeenCalledTimes(0);
replay.timeouts.maxSessionLife = MAX_SESSION_LIFE;
- replay['_loadAndCheckSession'] = _tmp;
+ replay['_checkSession'] = _tmp;
});
it('logs warning if flushing initial segment without checkout', async () => {
@@ -339,7 +343,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
await new Promise(process.nextTick);
jest.setSystemTime(BASE_TIMESTAMP);
@@ -399,7 +404,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
await new Promise(process.nextTick);
jest.setSystemTime(BASE_TIMESTAMP);
@@ -454,7 +460,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
await new Promise(process.nextTick);
jest.setSystemTime(BASE_TIMESTAMP);
diff --git a/packages/replay/test/integration/rateLimiting.test.ts b/packages/replay/test/integration/rateLimiting.test.ts
index 723dc682d100..291a95c4f94e 100644
--- a/packages/replay/test/integration/rateLimiting.test.ts
+++ b/packages/replay/test/integration/rateLimiting.test.ts
@@ -46,7 +46,8 @@ describe('Integration | rate-limiting behaviour', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockSendReplayRequest.mockClear();
});
@@ -57,7 +58,6 @@ describe('Integration | rate-limiting behaviour', () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
jest.clearAllMocks();
- replay['_loadAndCheckSession']();
});
afterAll(() => {
diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts
index d7a9974bcaa9..d6f26db6653c 100644
--- a/packages/replay/test/integration/sendReplayEvent.test.ts
+++ b/packages/replay/test/integration/sendReplayEvent.test.ts
@@ -59,7 +59,8 @@ describe('Integration | sendReplayEvent', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockSendReplayRequest.mockClear();
});
@@ -69,7 +70,6 @@ describe('Integration | sendReplayEvent', () => {
await new Promise(process.nextTick);
jest.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
- replay['_loadAndCheckSession']();
});
afterAll(() => {
diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts
index 304059659078..6e62b71ca09c 100644
--- a/packages/replay/test/integration/session.test.ts
+++ b/packages/replay/test/integration/session.test.ts
@@ -424,7 +424,8 @@ describe('Integration | session', () => {
it('increases segment id after each event', async () => {
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
Object.defineProperty(document, 'visibilityState', {
configurable: true,
diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts
index cc0e28195244..a88c5de6a839 100644
--- a/packages/replay/test/integration/stop.test.ts
+++ b/packages/replay/test/integration/stop.test.ts
@@ -52,7 +52,8 @@ describe('Integration | stop', () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockRecord.takeFullSnapshot.mockClear();
mockAddInstrumentationHandler.mockClear();
Object.defineProperty(WINDOW, 'location', {
diff --git a/packages/replay/test/unit/session/getSession.test.ts b/packages/replay/test/unit/session/getSession.test.ts
deleted file mode 100644
index aa3110d114f2..000000000000
--- a/packages/replay/test/unit/session/getSession.test.ts
+++ /dev/null
@@ -1,291 +0,0 @@
-import {
- MAX_SESSION_LIFE,
- SESSION_IDLE_EXPIRE_DURATION,
- SESSION_IDLE_PAUSE_DURATION,
- WINDOW,
-} from '../../../src/constants';
-import * as CreateSession from '../../../src/session/createSession';
-import * as FetchSession from '../../../src/session/fetchSession';
-import { getSession } from '../../../src/session/getSession';
-import { saveSession } from '../../../src/session/saveSession';
-import { makeSession } from '../../../src/session/Session';
-
-jest.mock('@sentry/utils', () => {
- return {
- ...(jest.requireActual('@sentry/utils') as { string: unknown }),
- uuid4: jest.fn(() => 'test_session_uuid'),
- };
-});
-
-const SAMPLE_OPTIONS = {
- sessionSampleRate: 1.0,
- allowBuffering: false,
-};
-
-function createMockSession(when: number = Date.now()) {
- return makeSession({
- id: 'test_session_id',
- segmentId: 0,
- lastActivity: when,
- started: when,
- sampled: 'session',
- shouldRefresh: true,
- });
-}
-
-describe('Unit | session | getSession', () => {
- beforeAll(() => {
- jest.spyOn(CreateSession, 'createSession');
- jest.spyOn(FetchSession, 'fetchSession');
- WINDOW.sessionStorage.clear();
- });
-
- afterEach(() => {
- WINDOW.sessionStorage.clear();
- (CreateSession.createSession as jest.MockedFunction).mockClear();
- (FetchSession.fetchSession as jest.MockedFunction).mockClear();
- });
-
- it('creates a non-sticky session when one does not exist', function () {
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toEqual({
- id: 'test_session_uuid',
- segmentId: 0,
- lastActivity: expect.any(Number),
- sampled: 'session',
- started: expect.any(Number),
- shouldRefresh: true,
- });
-
- // Should not have anything in storage
- expect(FetchSession.fetchSession()).toBe(null);
- });
-
- it('creates a non-sticky session, regardless of session existing in sessionStorage', function () {
- saveSession(createMockSession(Date.now() - 10000));
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toBeDefined();
- });
-
- it('creates a non-sticky session, when one is expired', function () {
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession: makeSession({
- id: 'old_session_id',
- lastActivity: Date.now() - 1001,
- started: Date.now() - 1001,
- segmentId: 0,
- sampled: 'session',
- }),
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toBeDefined();
- expect(session.id).not.toBe('old_session_id');
- });
-
- it('creates a sticky session when one does not exist', function () {
- expect(FetchSession.fetchSession()).toBe(null);
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: true,
- sessionSampleRate: 1.0,
- allowBuffering: false,
- });
-
- expect(FetchSession.fetchSession).toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toEqual({
- id: 'test_session_uuid',
- segmentId: 0,
- lastActivity: expect.any(Number),
- sampled: 'session',
- started: expect.any(Number),
- shouldRefresh: true,
- });
-
- // Should not have anything in storage
- expect(FetchSession.fetchSession()).toEqual({
- id: 'test_session_uuid',
- segmentId: 0,
- lastActivity: expect.any(Number),
- sampled: 'session',
- started: expect.any(Number),
- shouldRefresh: true,
- });
- });
-
- it('fetches an existing sticky session', function () {
- const now = Date.now();
- saveSession(createMockSession(now));
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: true,
- sessionSampleRate: 1.0,
- allowBuffering: false,
- });
-
- expect(FetchSession.fetchSession).toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(session).toEqual({
- id: 'test_session_id',
- segmentId: 0,
- lastActivity: now,
- sampled: 'session',
- started: now,
- shouldRefresh: true,
- });
- });
-
- it('fetches an expired sticky session', function () {
- const now = Date.now();
- saveSession(createMockSession(Date.now() - 2000));
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: true,
- ...SAMPLE_OPTIONS,
- });
-
- expect(FetchSession.fetchSession).toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session.id).toBe('test_session_uuid');
- expect(session.lastActivity).toBeGreaterThanOrEqual(now);
- expect(session.started).toBeGreaterThanOrEqual(now);
- expect(session.segmentId).toBe(0);
- });
-
- it('fetches a non-expired non-sticky session', function () {
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession: makeSession({
- id: 'test_session_uuid_2',
- lastActivity: +new Date() - 500,
- started: +new Date() - 500,
- segmentId: 0,
- sampled: 'session',
- }),
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(session.id).toBe('test_session_uuid_2');
- expect(session.segmentId).toBe(0);
- });
-
- it('re-uses the same "buffer" session if it is expired and has never sent a buffered replay', function () {
- const { type, session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession: makeSession({
- id: 'test_session_uuid_2',
- lastActivity: +new Date() - MAX_SESSION_LIFE - 1,
- started: +new Date() - MAX_SESSION_LIFE - 1,
- segmentId: 0,
- sampled: 'buffer',
- }),
- allowBuffering: true,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(type).toBe('saved');
- expect(session.id).toBe('test_session_uuid_2');
- expect(session.sampled).toBe('buffer');
- expect(session.segmentId).toBe(0);
- });
-
- it('creates a new session if it is expired and it was a "buffer" session that has sent a replay', function () {
- const currentSession = makeSession({
- id: 'test_session_uuid_2',
- lastActivity: +new Date() - MAX_SESSION_LIFE - 1,
- started: +new Date() - MAX_SESSION_LIFE - 1,
- segmentId: 0,
- sampled: 'buffer',
- });
- currentSession.shouldRefresh = false;
-
- const { type, session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession,
- allowBuffering: true,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(type).toBe('new');
- expect(session.id).not.toBe('test_session_uuid_2');
- expect(session.sampled).toBe(false);
- expect(session.segmentId).toBe(0);
- });
-});
diff --git a/packages/replay/test/unit/session/loadOrCreateSession.test.ts b/packages/replay/test/unit/session/loadOrCreateSession.test.ts
new file mode 100644
index 000000000000..907e078c75d3
--- /dev/null
+++ b/packages/replay/test/unit/session/loadOrCreateSession.test.ts
@@ -0,0 +1,396 @@
+import {
+ MAX_SESSION_LIFE,
+ SESSION_IDLE_EXPIRE_DURATION,
+ SESSION_IDLE_PAUSE_DURATION,
+ WINDOW,
+} from '../../../src/constants';
+import * as CreateSession from '../../../src/session/createSession';
+import * as FetchSession from '../../../src/session/fetchSession';
+import { loadOrCreateSession } from '../../../src/session/loadOrCreateSession';
+import { saveSession } from '../../../src/session/saveSession';
+import { makeSession } from '../../../src/session/Session';
+import type { SessionOptions, Timeouts } from '../../../src/types';
+
+jest.mock('@sentry/utils', () => {
+ return {
+ ...(jest.requireActual('@sentry/utils') as { string: unknown }),
+ uuid4: jest.fn(() => 'test_session_uuid'),
+ };
+});
+
+const SAMPLE_OPTIONS: SessionOptions = {
+ stickySession: false,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+};
+
+const timeouts: Timeouts = {
+ sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
+ sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
+ maxSessionLife: MAX_SESSION_LIFE,
+};
+
+function createMockSession(when: number = Date.now(), id = 'test_session_id') {
+ return makeSession({
+ id,
+ segmentId: 0,
+ lastActivity: when,
+ started: when,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+}
+
+describe('Unit | session | loadOrCreateSession', () => {
+ beforeAll(() => {
+ jest.spyOn(CreateSession, 'createSession');
+ jest.spyOn(FetchSession, 'fetchSession');
+ WINDOW.sessionStorage.clear();
+ });
+
+ afterEach(() => {
+ WINDOW.sessionStorage.clear();
+ (CreateSession.createSession as jest.MockedFunction).mockClear();
+ (FetchSession.fetchSession as jest.MockedFunction).mockClear();
+ });
+
+ describe('stickySession: false', () => {
+ it('creates new session if none is passed in', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: false,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ expect(session).toEqual({
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ });
+
+ // Should not have anything in storage
+ expect(FetchSession.fetchSession()).toBe(null);
+ });
+
+ it('creates new session, even if something is in sessionStorage', function () {
+ const sessionInStorage = createMockSession(Date.now() - 10000, 'test_old_session_uuid');
+ saveSession(sessionInStorage);
+
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: false,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ expect(session).toEqual({
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ });
+
+ // Should not have anything in storage
+ expect(FetchSession.fetchSession()).toEqual(sessionInStorage);
+ });
+
+ it('uses passed in session', function () {
+ const now = Date.now();
+ const currentSession = createMockSession(now - 2000);
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: false,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('stickySession: true', () => {
+ it('creates new session if none exists', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ };
+ expect(session).toEqual(expectedSession);
+
+ // Should also be stored in storage
+ expect(FetchSession.fetchSession()).toEqual(expectedSession);
+ });
+
+ it('creates new session if session in sessionStorage is expired', function () {
+ const now = Date.now();
+ const date = now - 2000;
+ saveSession(createMockSession(date, 'test_old_session_uuid'));
+
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ previousSessionId: 'test_old_session_uuid',
+ };
+ expect(session).toEqual(expectedSession);
+ expect(session.lastActivity).toBeGreaterThanOrEqual(now);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ expect(FetchSession.fetchSession()).toEqual(expectedSession);
+ });
+
+ it('returns session from sessionStorage if not expired', function () {
+ const date = Date.now() - 2000;
+ saveSession(createMockSession(date, 'test_old_session_uuid'));
+
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 5000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual({
+ id: 'test_old_session_uuid',
+ segmentId: 0,
+ lastActivity: date,
+ sampled: 'session',
+ started: date,
+ shouldRefresh: true,
+ });
+ });
+
+ it('uses passed in session instead of fetching from sessionStorage', function () {
+ const now = Date.now();
+ saveSession(createMockSession(now - 10000, 'test_storage_session_uuid'));
+ const currentSession = createMockSession(now - 2000);
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('buffering', () => {
+ it('returns current session when buffering, even if expired', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: true,
+ });
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+
+ it('returns new unsampled session when buffering & expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).not.toEqual(currentSession);
+ expect(session.sampled).toBe(false);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ });
+
+ it('returns existing session when buffering & not expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 5000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('sampling', () => {
+ it('returns unsampled session if sample rates are 0', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0,
+ allowBuffering: false,
+ },
+ );
+
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: false,
+ started: expect.any(Number),
+ shouldRefresh: true,
+ };
+ expect(session).toEqual(expectedSession);
+ });
+
+ it('returns `session` session if sessionSampleRate===1', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+ },
+ );
+
+ expect(session.sampled).toBe('session');
+ });
+
+ it('returns `buffer` session if allowBuffering===true', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0.0,
+ allowBuffering: true,
+ },
+ );
+
+ expect(session.sampled).toBe('buffer');
+ });
+ });
+});
diff --git a/packages/replay/test/unit/session/maybeRefreshSession.test.ts b/packages/replay/test/unit/session/maybeRefreshSession.test.ts
new file mode 100644
index 000000000000..5bcc8bf4481c
--- /dev/null
+++ b/packages/replay/test/unit/session/maybeRefreshSession.test.ts
@@ -0,0 +1,266 @@
+import {
+ MAX_SESSION_LIFE,
+ SESSION_IDLE_EXPIRE_DURATION,
+ SESSION_IDLE_PAUSE_DURATION,
+ WINDOW,
+} from '../../../src/constants';
+import * as CreateSession from '../../../src/session/createSession';
+import { maybeRefreshSession } from '../../../src/session/maybeRefreshSession';
+import { makeSession } from '../../../src/session/Session';
+import type { SessionOptions, Timeouts } from '../../../src/types';
+
+jest.mock('@sentry/utils', () => {
+ return {
+ ...(jest.requireActual('@sentry/utils') as { string: unknown }),
+ uuid4: jest.fn(() => 'test_session_uuid'),
+ };
+});
+
+const SAMPLE_OPTIONS: SessionOptions = {
+ stickySession: false,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+};
+
+const timeouts: Timeouts = {
+ sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
+ sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
+ maxSessionLife: MAX_SESSION_LIFE,
+};
+
+function createMockSession(when: number = Date.now(), id = 'test_session_id') {
+ return makeSession({
+ id,
+ segmentId: 0,
+ lastActivity: when,
+ started: when,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+}
+
+describe('Unit | session | maybeRefreshSession', () => {
+ beforeAll(() => {
+ jest.spyOn(CreateSession, 'createSession');
+ });
+
+ afterEach(() => {
+ WINDOW.sessionStorage.clear();
+ (CreateSession.createSession as jest.MockedFunction).mockClear();
+ });
+
+ it('returns session if not expired', function () {
+ const now = Date.now();
+ const currentSession = createMockSession(now - 2000);
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+
+ it('creates new session if expired', function () {
+ const now = Date.now();
+ const currentSession = createMockSession(now - 2000, 'test_old_session_uuid');
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ expect(session).not.toEqual(currentSession);
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ previousSessionId: 'test_old_session_uuid',
+ };
+ expect(session).toEqual(expectedSession);
+ expect(session.lastActivity).toBeGreaterThanOrEqual(now);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ });
+
+ describe('buffering', () => {
+ it('returns session when buffering, even if expired', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+
+ it('returns new unsampled session when buffering & expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).not.toEqual(currentSession);
+ expect(session.sampled).toBe(false);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ });
+
+ it('returns existing session when buffering & not expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 5000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('sampling', () => {
+ it('creates unsampled session if sample rates are 0', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0,
+ allowBuffering: false,
+ },
+ );
+
+ expect(session.id).toBe('test_session_uuid');
+ expect(session.sampled).toBe(false);
+ });
+
+ it('creates `session` session if sessionSampleRate===1', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+ },
+ );
+
+ expect(session.id).toBe('test_session_uuid');
+ expect(session.sampled).toBe('session');
+ });
+
+ it('creates `buffer` session if allowBuffering===true', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0.0,
+ allowBuffering: true,
+ },
+ );
+
+ expect(session.id).toBe('test_session_uuid');
+ expect(session.sampled).toBe('buffer');
+ });
+ });
+});
diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts
index 02a965b7d9c2..cb70c85bbe54 100644
--- a/packages/replay/test/utils/setupReplayContainer.ts
+++ b/packages/replay/test/utils/setupReplayContainer.ts
@@ -42,8 +42,8 @@ export function setupReplayContainer({
});
clearSession(replay);
+ replay['_initializeSessionForSampling']();
replay.setInitialState();
- replay['_loadAndCheckSession']();
replay['_isEnabled'] = true;
replay.eventBuffer = createEventBuffer({
useCompression: options?.useCompression || false,
diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts
index 99730ac8dac1..f7a195aba4e8 100644
--- a/packages/serverless/src/index.ts
+++ b/packages/serverless/src/index.ts
@@ -50,4 +50,7 @@ export {
Handlers,
Integrations,
setMeasurement,
+ getActiveSpan,
+ startActiveSpan,
+ startSpan,
} from '@sentry/node';
diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json
index a8841d5a02c2..7a62ede5ed0f 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -27,7 +27,7 @@
"@sentry/svelte": "7.64.0",
"@sentry/types": "7.64.0",
"@sentry/utils": "7.64.0",
- "@sentry/vite-plugin": "^0.6.0",
+ "@sentry/vite-plugin": "^0.6.1",
"magicast": "0.2.8",
"sorcery": "0.11.0"
},
@@ -44,7 +44,7 @@
"build:types": "tsc -p tsconfig.types.json",
"build:watch": "run-p build:transpile:watch build:types:watch",
"build:dev:watch": "yarn build:watch",
- "build:transpile:watch": "rollup -c rollup.npm.config.js --watch",
+ "build:transpile:watch": "rollup -c rollup.npm.config.js --bundleConfigAsCjs --watch",
"build:types:watch": "tsc -p tsconfig.types.json --watch",
"build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build",
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts",
diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts
index e12ed19a6cae..44430a3b9b1c 100644
--- a/packages/sveltekit/src/client/load.ts
+++ b/packages/sveltekit/src/client/load.ts
@@ -1,23 +1,10 @@
-import { addTracingHeadersToFetchRequest } from '@sentry-internal/tracing';
-import type { BaseClient } from '@sentry/core';
-import { getCurrentHub, trace } from '@sentry/core';
-import type { Breadcrumbs, BrowserTracing } from '@sentry/svelte';
+import { trace } from '@sentry/core';
import { captureException } from '@sentry/svelte';
-import type { Client, ClientOptions, SanitizedRequestData, Span } from '@sentry/types';
-import {
- addExceptionMechanism,
- addNonEnumerableProperty,
- getSanitizedUrlString,
- objectify,
- parseFetchArgs,
- parseUrl,
- stringMatchesSomePattern,
-} from '@sentry/utils';
+import { addExceptionMechanism, addNonEnumerableProperty, objectify } from '@sentry/utils';
import type { LoadEvent } from '@sveltejs/kit';
import type { SentryWrappedFlag } from '../common/utils';
import { isRedirect } from '../common/utils';
-import { isRequestCached } from './vendor/lookUpCache';
type PatchedLoadEvent = LoadEvent & Partial;
@@ -80,7 +67,6 @@ export function wrapLoadWithSentry any>(origLoad: T)
const patchedEvent: PatchedLoadEvent = {
...event,
- fetch: instrumentSvelteKitFetch(event.fetch),
};
addNonEnumerableProperty(patchedEvent as unknown as Record, '__sentry_wrapped__', true);
@@ -101,182 +87,3 @@ export function wrapLoadWithSentry any>(origLoad: T)
},
});
}
-
-type SvelteKitFetch = LoadEvent['fetch'];
-
-/**
- * Instruments SvelteKit's client `fetch` implementation which is passed to the client-side universal `load` functions.
- *
- * We need to instrument this in addition to the native fetch we instrument in BrowserTracing because SvelteKit
- * stores the native fetch implementation before our SDK is initialized.
- *
- * see: https://github.com/sveltejs/kit/blob/master/packages/kit/src/runtime/client/fetcher.js
- *
- * This instrumentation takes the fetch-related options from `BrowserTracing` to determine if we should
- * instrument fetch for perfomance monitoring, create a span for or attach our tracing headers to the given request.
- *
- * To dertermine if breadcrumbs should be recorded, this instrumentation relies on the availability of and the options
- * set in the `BreadCrumbs` integration.
- *
- * @param originalFetch SvelteKit's original fetch implemenetation
- *
- * @returns a proxy of SvelteKit's fetch implementation
- */
-function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch {
- const client = getCurrentHub().getClient();
-
- if (!isValidClient(client)) {
- return originalFetch;
- }
-
- const options = client.getOptions();
-
- const browserTracingIntegration = client.getIntegrationById('BrowserTracing') as BrowserTracing | undefined;
- const breadcrumbsIntegration = client.getIntegrationById('Breadcrumbs') as Breadcrumbs | undefined;
-
- const browserTracingOptions = browserTracingIntegration && browserTracingIntegration.options;
-
- const shouldTraceFetch = browserTracingOptions && browserTracingOptions.traceFetch;
- const shouldAddFetchBreadcrumb = breadcrumbsIntegration && breadcrumbsIntegration.options.fetch;
-
- /* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */
- const shouldCreateSpan =
- browserTracingOptions && typeof browserTracingOptions.shouldCreateSpanForRequest === 'function'
- ? browserTracingOptions.shouldCreateSpanForRequest
- : (_: string) => shouldTraceFetch;
-
- /* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */
- const shouldAttachHeaders: (url: string) => boolean = url => {
- return (
- !!shouldTraceFetch &&
- stringMatchesSomePattern(
- url,
- options.tracePropagationTargets || browserTracingOptions.tracePropagationTargets || ['localhost', /^\//],
- )
- );
- };
-
- return new Proxy(originalFetch, {
- apply: (wrappingTarget, thisArg, args: Parameters) => {
- const [input, init] = args;
-
- if (isRequestCached(input, init)) {
- return wrappingTarget.apply(thisArg, args);
- }
-
- const { url: rawUrl, method } = parseFetchArgs(args);
-
- // TODO: extract this to a util function (and use it in breadcrumbs integration as well)
- if (rawUrl.match(/sentry_key/)) {
- // We don't create spans or breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests)
- return wrappingTarget.apply(thisArg, args);
- }
-
- const urlObject = parseUrl(rawUrl);
-
- const requestData: SanitizedRequestData = {
- url: getSanitizedUrlString(urlObject),
- 'http.method': method,
- ...(urlObject.search && { 'http.query': urlObject.search.substring(1) }),
- ...(urlObject.hash && { 'http.hash': urlObject.hash.substring(1) }),
- };
-
- const patchedInit: RequestInit = { ...init };
- const hub = getCurrentHub();
- const scope = hub.getScope();
- const client = hub.getClient();
-
- let fetchPromise: Promise;
-
- function callFetchTarget(span?: Span): Promise {
- if (client && shouldAttachHeaders(rawUrl)) {
- const headers = addTracingHeadersToFetchRequest(input as string | Request, client, scope, patchedInit, span);
- patchedInit.headers = headers as HeadersInit;
- }
- const patchedFetchArgs = [input, patchedInit];
- return wrappingTarget.apply(thisArg, patchedFetchArgs);
- }
-
- if (shouldCreateSpan(rawUrl)) {
- fetchPromise = trace(
- {
- name: `${method} ${requestData.url}`, // this will become the description of the span
- op: 'http.client',
- data: requestData,
- },
- span => {
- const promise = callFetchTarget(span);
- if (span) {
- promise.then(res => span.setHttpStatus(res.status)).catch(_ => span.setStatus('internal_error'));
- }
- return promise;
- },
- );
- } else {
- fetchPromise = callFetchTarget();
- }
-
- if (shouldAddFetchBreadcrumb) {
- addFetchBreadcrumb(fetchPromise, requestData, args);
- }
-
- return fetchPromise;
- },
- });
-}
-
-/* Adds a breadcrumb for the given fetch result */
-function addFetchBreadcrumb(
- fetchResult: Promise,
- requestData: SanitizedRequestData,
- args: Parameters,
-): void {
- const breadcrumbStartTimestamp = Date.now();
- fetchResult.then(
- response => {
- getCurrentHub().addBreadcrumb(
- {
- type: 'http',
- category: 'fetch',
- data: {
- ...requestData,
- status_code: response.status,
- },
- },
- {
- input: args,
- response,
- startTimestamp: breadcrumbStartTimestamp,
- endTimestamp: Date.now(),
- },
- );
- },
- error => {
- getCurrentHub().addBreadcrumb(
- {
- type: 'http',
- category: 'fetch',
- level: 'error',
- data: requestData,
- },
- {
- input: args,
- data: error,
- startTimestamp: breadcrumbStartTimestamp,
- endTimestamp: Date.now(),
- },
- );
- },
- );
-}
-
-type MaybeClientWithGetIntegrationsById =
- | (Client & { getIntegrationById?: BaseClient['getIntegrationById'] })
- | undefined;
-
-type ClientWithGetIntegrationById = Required &
- Exclude;
-
-function isValidClient(client: MaybeClientWithGetIntegrationsById): client is ClientWithGetIntegrationById {
- return !!client && typeof client.getIntegrationById === 'function';
-}
diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts
index 9bf1d2cb140b..c399a4a2ad02 100644
--- a/packages/sveltekit/src/client/sdk.ts
+++ b/packages/sveltekit/src/client/sdk.ts
@@ -1,11 +1,15 @@
import { hasTracingEnabled } from '@sentry/core';
import type { BrowserOptions } from '@sentry/svelte';
-import { BrowserTracing, configureScope, init as initSvelteSdk } from '@sentry/svelte';
+import { BrowserTracing, configureScope, init as initSvelteSdk, WINDOW } from '@sentry/svelte';
import { addOrUpdateIntegration } from '@sentry/utils';
import { applySdkMetadata } from '../common/metadata';
import { svelteKitRoutingInstrumentation } from './router';
+type WindowWithSentryFetchProxy = typeof WINDOW & {
+ _sentryFetchProxy?: typeof fetch;
+};
+
// Treeshakable guard to remove all code related to tracing
declare const __SENTRY_TRACING__: boolean;
@@ -19,8 +23,17 @@ export function init(options: BrowserOptions): void {
addClientIntegrations(options);
+ // 1. Switch window.fetch to our fetch proxy we injected earlier
+ const actualFetch = switchToFetchProxy();
+
+ // 2. Initialize the SDK which will instrument our proxy
initSvelteSdk(options);
+ // 3. Restore the original fetch now that our proxy is instrumented
+ if (actualFetch) {
+ restoreFetch(actualFetch);
+ }
+
configureScope(scope => {
scope.setTag('runtime', 'browser');
});
@@ -45,3 +58,43 @@ function addClientIntegrations(options: BrowserOptions): void {
options.integrations = integrations;
}
+
+/**
+ * During server-side page load, we injected a
+ `;
return html.replace('', content);
}
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index 96f43cc9f7f9..f7c0b99f6301 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -45,6 +45,9 @@ export {
Integrations,
Handlers,
setMeasurement,
+ getActiveSpan,
+ startActiveSpan,
+ startSpan,
} from '@sentry/node';
// We can still leave this for the carrier init and type exports
diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts
index 0ad28e1cb4eb..04d0137062c6 100644
--- a/packages/sveltekit/src/server/load.ts
+++ b/packages/sveltekit/src/server/load.ts
@@ -119,7 +119,11 @@ export function wrapServerLoadWithSentry any>(origSe
addNonEnumerableProperty(event as unknown as Record, '__sentry_wrapped__', true);
- const routeId = event.route && event.route.id;
+ // Accessing any member of `event.route` causes SvelteKit to invalidate the
+ // server `load` function's data on every route change.
+ // To work around this, we use `Object.getOwnPropertyDescriptor` which doesn't invoke the proxy.
+ // https://github.com/sveltejs/kit/blob/e133aba479fa9ba0e7f9e71512f5f937f0247e2c/packages/kit/src/runtime/server/page/load_data.js#L111C3-L124
+ const routeId = event.route && (Object.getOwnPropertyDescriptor(event.route, 'id')?.value as string | undefined);
const { dynamicSamplingContext, traceparentData, propagationContext } = getTracePropagationData(event);
getCurrentHub().getScope().setPropagationContext(propagationContext);
diff --git a/packages/sveltekit/test/client/fetch.test.ts b/packages/sveltekit/test/client/fetch.test.ts
new file mode 100644
index 000000000000..a97478cc86e8
--- /dev/null
+++ b/packages/sveltekit/test/client/fetch.test.ts
@@ -0,0 +1,55 @@
+import { init } from '../../src/client/index';
+
+describe('instruments fetch', () => {
+ beforeEach(() => {
+ // For the happy path, we can assume that both fetch and the fetch proxy are set
+ // We test the edge cases in the other tests below
+
+ // @ts-expect-error this fine just for the test
+ globalThis.fetch = () => Promise.resolve('fetch');
+
+ globalThis._sentryFetchProxy = () => Promise.resolve('_sentryFetchProxy');
+ // small hack to make `supportsNativeFetch` return true
+ globalThis._sentryFetchProxy.toString = () => 'function fetch() { [native code] }';
+ });
+
+ it('correctly swaps and instruments window._sentryFetchProxy', async () => {
+ // We expect init to swap window.fetch with our fetch proxy so that the proxy is instrumented
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ enableTracing: true,
+ });
+
+ // fetch proxy was instrumented
+ expect(globalThis._sentryFetchProxy['__sentry_original__']).toBeDefined();
+
+ // in the end, fetch and fetch proxy were restored correctly
+ expect(await globalThis.fetch('')).toEqual('fetch');
+ expect(await globalThis._sentryFetchProxy()).toEqual('_sentryFetchProxy');
+ });
+
+ it("doesn't swap fetch if the fetch proxy doesn't exist", async () => {
+ delete globalThis._sentryFetchProxy;
+
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ enableTracing: true,
+ });
+
+ expect(await globalThis.fetch('')).toEqual('fetch');
+ expect(globalThis._sentryFetchProxy).toBeUndefined();
+ });
+
+ it("doesn't swap fetch if global fetch doesn't exist", async () => {
+ // @ts-expect-error this fine just for the test
+ delete globalThis.fetch;
+
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ enableTracing: true,
+ });
+
+ expect(await globalThis._sentryFetchProxy()).toEqual('_sentryFetchProxy');
+ expect(globalThis._sentryFetchProxy['__sentry_original__']).toBeUndefined();
+ });
+});
diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts
index 6373ea1ff571..01c2377ddbf2 100644
--- a/packages/sveltekit/test/client/load.test.ts
+++ b/packages/sveltekit/test/client/load.test.ts
@@ -1,17 +1,10 @@
import { addTracingExtensions, Scope } from '@sentry/svelte';
-import { baggageHeaderToDynamicSamplingContext } from '@sentry/utils';
import type { Load } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { vi } from 'vitest';
import { wrapLoadWithSentry } from '../../src/client/load';
-const SENTRY_TRACE_HEADER = '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
-const BAGGAGE_HEADER =
- 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
- 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
- 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1';
-
const mockCaptureException = vi.fn();
let mockScope = new Scope();
@@ -27,44 +20,8 @@ vi.mock('@sentry/svelte', async () => {
};
});
-vi.mock('../../src/client/vendor/lookUpCache', () => {
- return {
- isRequestCached: () => false,
- };
-});
-
const mockTrace = vi.fn();
-const mockedBrowserTracing = {
- options: {
- tracePropagationTargets: ['example.com', /^\\/],
- traceFetch: true,
- shouldCreateSpanForRequest: undefined as undefined | (() => boolean),
- },
-};
-
-const mockedBreadcrumbs = {
- options: {
- fetch: true,
- },
-};
-
-const mockedGetIntegrationById = vi.fn(id => {
- if (id === 'BrowserTracing') {
- return mockedBrowserTracing;
- } else if (id === 'Breadcrumbs') {
- return mockedBreadcrumbs;
- }
- return undefined;
-});
-
-const mockedGetClient = vi.fn(() => {
- return {
- getIntegrationById: mockedGetIntegrationById,
- getOptions: () => ({}),
- };
-});
-
vi.mock('@sentry/core', async () => {
const original = (await vi.importActual('@sentry/core')) as any;
return {
@@ -73,38 +30,10 @@ vi.mock('@sentry/core', async () => {
mockTrace(...args);
return original.trace(...args);
},
- getCurrentHub: () => {
- return {
- getClient: mockedGetClient,
- getScope: () => {
- return {
- getPropagationContext: () => ({
- traceId: '1234567890abcdef1234567890abcdef',
- spanId: '1234567890abcdef',
- sampled: false,
- }),
- getSpan: () => {
- return {
- transaction: {
- getDynamicSamplingContext: () => {
- return baggageHeaderToDynamicSamplingContext(BAGGAGE_HEADER);
- },
- },
- toTraceparent: () => {
- return SENTRY_TRACE_HEADER;
- },
- };
- },
- };
- },
- addBreadcrumb: mockedAddBreadcrumb,
- };
- },
};
});
const mockAddExceptionMechanism = vi.fn();
-const mockedAddBreadcrumb = vi.fn();
vi.mock('@sentry/utils', async () => {
const original = (await vi.importActual('@sentry/utils')) as any;
@@ -118,30 +47,12 @@ function getById(_id?: string) {
throw new Error('error');
}
-const mockedSveltekitFetch = vi.fn().mockReturnValue(Promise.resolve({ status: 200 }));
-
const MOCK_LOAD_ARGS: any = {
params: { id: '123' },
route: {
id: '/users/[id]',
},
url: new URL('http://localhost:3000/users/123'),
- request: {
- headers: {
- get: (key: string) => {
- if (key === 'sentry-trace') {
- return SENTRY_TRACE_HEADER;
- }
-
- if (key === 'baggage') {
- return BAGGAGE_HEADER;
- }
-
- return null;
- },
- },
- },
- fetch: mockedSveltekitFetch,
};
beforeAll(() => {
@@ -153,9 +64,6 @@ describe('wrapLoadWithSentry', () => {
mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
mockTrace.mockClear();
- mockedGetIntegrationById.mockClear();
- mockedSveltekitFetch.mockClear();
- mockedAddBreadcrumb.mockClear();
mockScope = new Scope();
});
@@ -211,253 +119,35 @@ describe('wrapLoadWithSentry', () => {
);
});
- describe.each([
- [
- 'fetch call with fragment and params',
- ['example.com/api/users/?id=123#testfragment'],
- {
- op: 'http.client',
- name: 'GET example.com/api/users/',
- data: {
- 'http.method': 'GET',
- url: 'example.com/api/users/',
- 'http.hash': 'testfragment',
- 'http.query': 'id=123',
- },
- },
- ],
- [
- 'fetch call with options object',
- ['example.com/api/users/?id=123#testfragment', { method: 'POST' }],
- {
- op: 'http.client',
- name: 'POST example.com/api/users/',
- data: {
- 'http.method': 'POST',
- url: 'example.com/api/users/',
- 'http.hash': 'testfragment',
- 'http.query': 'id=123',
- },
- },
- ],
- [
- 'fetch call with custom headers in options ',
- ['example.com/api/users/?id=123#testfragment', { method: 'POST', headers: { 'x-my-header': 'some value' } }],
- {
- op: 'http.client',
- name: 'POST example.com/api/users/',
- data: {
- 'http.method': 'POST',
- url: 'example.com/api/users/',
- 'http.hash': 'testfragment',
- 'http.query': 'id=123',
- },
- },
- ],
- [
- 'fetch call with a Request object ',
- [{ url: '/api/users?id=123', headers: { 'x-my-header': 'value' } } as unknown as Request],
- {
- op: 'http.client',
- name: 'GET /api/users',
- data: {
- 'http.method': 'GET',
- url: '/api/users',
- 'http.query': 'id=123',
- },
- },
- ],
- ])('instruments fetch (%s)', (_, originalFetchArgs, spanCtx) => {
- beforeEach(() => {
- mockedBrowserTracing.options = {
- tracePropagationTargets: ['example.com', /^\//],
- traceFetch: true,
- shouldCreateSpanForRequest: undefined,
- };
- });
-
- const load = async ({ params, fetch }) => {
- await fetch(...originalFetchArgs);
+ it("falls back to the raw URL if `even.route.id` isn't available", async () => {
+ async function load({ params }: Parameters[0]): Promise> {
return {
post: params.id,
};
- };
-
- it('creates a fetch span and attaches tracing headers by default when event.fetch was called', async () => {
- const wrappedLoad = wrapLoadWithSentry(load);
- await wrappedLoad(MOCK_LOAD_ARGS);
-
- expect(mockTrace).toHaveBeenCalledTimes(2);
- expect(mockTrace).toHaveBeenNthCalledWith(
- 1,
- {
- op: 'function.sveltekit.load',
- name: '/users/[id]',
- status: 'ok',
- metadata: {
- source: 'route',
- },
- },
- expect.any(Function),
- expect.any(Function),
- );
- expect(mockTrace).toHaveBeenNthCalledWith(2, spanCtx, expect.any(Function));
-
- const hasSecondArg = originalFetchArgs.length > 1;
- const expectedFetchArgs = [
- originalFetchArgs[0],
- {
- ...(hasSecondArg && (originalFetchArgs[1] as RequestInit)),
- headers: {
- // @ts-ignore that's fine
- ...(hasSecondArg && (originalFetchArgs[1].headers as RequestInit['headers'])),
- baggage: expect.any(String),
- 'sentry-trace': expect.any(String),
- },
- },
- ];
-
- expect(mockedSveltekitFetch).toHaveBeenCalledWith(...expectedFetchArgs);
- });
-
- it("only creates a span but doesn't propagate headers if traceProgagationTargets don't match", async () => {
- const previousPropagationTargets = mockedBrowserTracing.options.tracePropagationTargets;
- mockedBrowserTracing.options.tracePropagationTargets = [];
-
- const wrappedLoad = wrapLoadWithSentry(load);
- await wrappedLoad(MOCK_LOAD_ARGS);
-
- expect(mockTrace).toHaveBeenCalledTimes(2);
- expect(mockTrace).toHaveBeenNthCalledWith(
- 1,
- {
- op: 'function.sveltekit.load',
- name: '/users/[id]',
- status: 'ok',
- metadata: {
- source: 'route',
- },
- },
- expect.any(Function),
- expect.any(Function),
- );
- expect(mockTrace).toHaveBeenNthCalledWith(2, spanCtx, expect.any(Function));
-
- expect(mockedSveltekitFetch).toHaveBeenCalledWith(
- ...[originalFetchArgs[0], originalFetchArgs.length === 2 ? originalFetchArgs[1] : {}],
- );
-
- mockedBrowserTracing.options.tracePropagationTargets = previousPropagationTargets;
- });
-
- it("doesn't create a span nor propagate headers, if `Browsertracing.options.traceFetch` is false", async () => {
- mockedBrowserTracing.options.traceFetch = false;
-
- const wrappedLoad = wrapLoadWithSentry(load);
- await wrappedLoad(MOCK_LOAD_ARGS);
-
- expect(mockTrace).toHaveBeenCalledTimes(1);
- expect(mockTrace).toHaveBeenCalledWith(
- {
- op: 'function.sveltekit.load',
- name: '/users/[id]',
- status: 'ok',
- metadata: {
- source: 'route',
- },
- },
- expect.any(Function),
- expect.any(Function),
- );
-
- expect(mockedSveltekitFetch).toHaveBeenCalledWith(
- ...[originalFetchArgs[0], originalFetchArgs.length === 2 ? originalFetchArgs[1] : {}],
- );
-
- mockedBrowserTracing.options.traceFetch = true;
- });
-
- it("doesn't create a span if `shouldCreateSpanForRequest` returns false", async () => {
- mockedBrowserTracing.options.shouldCreateSpanForRequest = () => false;
-
- const wrappedLoad = wrapLoadWithSentry(load);
- await wrappedLoad(MOCK_LOAD_ARGS);
-
- expect(mockTrace).toHaveBeenCalledTimes(1);
- expect(mockTrace).toHaveBeenCalledWith(
- {
- op: 'function.sveltekit.load',
- name: '/users/[id]',
- status: 'ok',
- metadata: {
- source: 'route',
- },
- },
- expect.any(Function),
- expect.any(Function),
- );
+ }
+ const wrappedLoad = wrapLoadWithSentry(load);
- mockedBrowserTracing.options.shouldCreateSpanForRequest = () => true;
- });
+ const event = { ...MOCK_LOAD_ARGS };
+ delete event.route.id;
- it('adds a breadcrumb for the fetch call', async () => {
- const wrappedLoad = wrapLoadWithSentry(load);
- await wrappedLoad(MOCK_LOAD_ARGS);
+ await wrappedLoad(MOCK_LOAD_ARGS);
- expect(mockedAddBreadcrumb).toHaveBeenCalledWith(
- {
- category: 'fetch',
- data: {
- ...spanCtx.data,
- status_code: 200,
- },
- type: 'http',
- },
- {
- endTimestamp: expect.any(Number),
- input: [...originalFetchArgs],
- response: {
- status: 200,
- },
- startTimestamp: expect.any(Number),
+ expect(mockTrace).toHaveBeenCalledTimes(1);
+ expect(mockTrace).toHaveBeenCalledWith(
+ {
+ op: 'function.sveltekit.load',
+ name: '/users/123',
+ status: 'ok',
+ metadata: {
+ source: 'url',
},
- );
- });
-
- it("doesn't add a breadcrumb if fetch breadcrumbs are deactivated in the integration", async () => {
- mockedBreadcrumbs.options.fetch = false;
-
- const wrappedLoad = wrapLoadWithSentry(load);
- await wrappedLoad(MOCK_LOAD_ARGS);
-
- expect(mockedAddBreadcrumb).not.toHaveBeenCalled();
-
- mockedBreadcrumbs.options.fetch = true;
- });
+ },
+ expect.any(Function),
+ expect.any(Function),
+ );
});
});
- it.each([
- ['is undefined', undefined],
- ["doesn't have a `getClientById` method", {}],
- ])("doesn't instrument fetch if the client %s", async (_, client) => {
- mockedGetClient.mockImplementationOnce(() => client);
-
- async function load(_event: Parameters[0]): Promise> {
- return {
- msg: 'hi',
- };
- }
- const wrappedLoad = wrapLoadWithSentry(load);
-
- const originalFetch = MOCK_LOAD_ARGS.fetch;
- await wrappedLoad(MOCK_LOAD_ARGS);
-
- expect(MOCK_LOAD_ARGS.fetch).toStrictEqual(originalFetch);
-
- expect(mockTrace).toHaveBeenCalledTimes(1);
- });
-
it('adds an exception mechanism', async () => {
const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
void callback({}, { event_id: 'fake-event-id' });
diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts
index a8353a73df3e..5ff3b9f9e846 100644
--- a/packages/sveltekit/test/client/sdk.test.ts
+++ b/packages/sveltekit/test/client/sdk.test.ts
@@ -109,9 +109,7 @@ describe('Sentry client SDK', () => {
it('Merges a user-provided BrowserTracing integration with the automatically added one', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
- integrations: [
- new BrowserTracing({ tracePropagationTargets: ['myDomain.com'], startTransactionOnLocationChange: false }),
- ],
+ integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })],
enableTracing: true,
});
@@ -126,8 +124,7 @@ describe('Sentry client SDK', () => {
expect(browserTracing).toBeDefined();
// This shows that the user-configured options are still here
- expect(options.tracePropagationTargets).toEqual(['myDomain.com']);
- expect(options.startTransactionOnLocationChange).toBe(false);
+ expect(options.finalTimeout).toEqual(10);
// But we force the routing instrumentation to be ours
expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation);
diff --git a/packages/sveltekit/test/client/vendor/lookUpCache.test.ts b/packages/sveltekit/test/client/vendor/lookUpCache.test.ts
deleted file mode 100644
index 29b13494be12..000000000000
--- a/packages/sveltekit/test/client/vendor/lookUpCache.test.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { JSDOM } from 'jsdom';
-import { vi } from 'vitest';
-
-import { isRequestCached } from '../../../src/client/vendor/lookUpCache';
-
-globalThis.document = new JSDOM().window.document;
-
-vi.useFakeTimers().setSystemTime(new Date('2023-06-22'));
-vi.spyOn(performance, 'now').mockReturnValue(1000);
-
-describe('isRequestCached', () => {
- it('should return true if a script tag with the same selector as the constructed request selector is found', () => {
- globalThis.document.body.innerHTML =
- '';
-
- expect(isRequestCached('/api/todos/1', undefined)).toBe(true);
- });
-
- it('should return false if a script with the same selector as the constructed request selector is not found', () => {
- globalThis.document.body.innerHTML = '';
-
- expect(isRequestCached('/api/todos/1', undefined)).toBe(false);
- });
-
- it('should return true if a script with the same selector as the constructed request selector is found and its TTL is valid', () => {
- globalThis.document.body.innerHTML =
- '';
-
- expect(isRequestCached('/api/todos/1', undefined)).toBe(true);
- });
-
- it('should return false if a script with the same selector as the constructed request selector is found and its TTL is expired', () => {
- globalThis.document.body.innerHTML =
- '';
-
- expect(isRequestCached('/api/todos/1', undefined)).toBe(false);
- });
-
- it("should return false if the TTL is set but can't be parsed as a number", () => {
- globalThis.document.body.innerHTML =
- '';
-
- expect(isRequestCached('/api/todos/1', undefined)).toBe(false);
- });
-});
diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts
index fa57dc6b7c46..90980204cf68 100644
--- a/packages/sveltekit/test/server/load.test.ts
+++ b/packages/sveltekit/test/server/load.test.ts
@@ -400,4 +400,33 @@ describe('wrapServerLoadWithSentry calls trace', () => {
expect(mockTrace).toHaveBeenCalledTimes(1);
});
+
+ it("doesn't invoke the proxy set on `event.route`", async () => {
+ const event = getServerOnlyArgs();
+
+ // simulates SvelteKit adding a proxy to `event.route`
+ // https://github.com/sveltejs/kit/blob/e133aba479fa9ba0e7f9e71512f5f937f0247e2c/packages/kit/src/runtime/server/page/load_data.js#L111C3-L124
+ const proxyFn = vi.fn((target: { id: string }, key: string | symbol): any => {
+ return target[key];
+ });
+
+ event.route = new Proxy(event.route, {
+ get: proxyFn,
+ });
+
+ const wrappedLoad = wrapServerLoadWithSentry(serverLoad);
+ await wrappedLoad(event);
+
+ expect(mockTrace).toHaveBeenCalledTimes(1);
+ expect(mockTrace).toHaveBeenCalledWith(
+ expect.objectContaining({
+ op: 'function.sveltekit.server.load',
+ name: '/users/[id]', // <-- this shows that the route was still accessed
+ }),
+ expect.any(Function),
+ expect.any(Function),
+ );
+
+ expect(proxyFn).not.toHaveBeenCalled();
+ });
});
diff --git a/packages/sveltekit/test/vitest.setup.ts b/packages/sveltekit/test/vitest.setup.ts
index af2810a98a96..57fdb8baef87 100644
--- a/packages/sveltekit/test/vitest.setup.ts
+++ b/packages/sveltekit/test/vitest.setup.ts
@@ -13,6 +13,8 @@ export function setup() {
}
if (!globalThis.fetch) {
- // @ts-ignore - Needed for vitest to work with SvelteKit fetch instrumentation
+ // @ts-ignore - Needed for vitest to work with our fetch instrumentation
globalThis.Request = class Request {};
+ // @ts-ignore - Needed for vitest to work with our fetch instrumentation
+ globalThis.Response = class Response {};
}
diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts
index aae66bee3358..d01c837d26c2 100644
--- a/packages/tracing-internal/src/browser/browsertracing.ts
+++ b/packages/tracing-internal/src/browser/browsertracing.ts
@@ -248,6 +248,7 @@ export class BrowserTracing implements Integration {
// This is done as it minimizes bundle size (we don't have to have undefined checks).
//
// If both 1 and either one of 2 or 3 are set (from above), we log out a warning.
+ // eslint-disable-next-line deprecation/deprecation
const tracePropagationTargets = clientOptionsTracePropagationTargets || this.options.tracePropagationTargets;
if (__DEBUG_BUILD__ && this._hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) {
logger.warn(
diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts
index e24c726ada5f..7c64484ce54b 100644
--- a/packages/tracing-internal/src/browser/request.ts
+++ b/packages/tracing-internal/src/browser/request.ts
@@ -27,7 +27,10 @@ export interface RequestInstrumentationOptions {
* List of strings and/or regexes used to determine which outgoing requests will have `sentry-trace` and `baggage`
* headers attached.
*
- * Default: ['localhost', /^\//] {@see DEFAULT_TRACE_PROPAGATION_TARGETS}
+ * @deprecated Use the top-level `tracePropagationTargets` option in `Sentry.init` instead.
+ * This option will be removed in v8.
+ *
+ * Default: ['localhost', /^\//] @see {DEFAULT_TRACE_PROPAGATION_TARGETS}
*/
tracePropagationTargets: Array;
@@ -125,6 +128,7 @@ export function instrumentOutgoingRequests(_options?: Partial {
- const entries = list.getEntries() as PerformanceResourceTiming[];
+ const entries = list.getEntries();
entries.forEach(entry => {
- if ((entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') && entry.name.endsWith(url)) {
+ if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) {
const spanData = resourceTimingEntryToSpanData(entry);
spanData.forEach(data => span.setData(...data));
observer.disconnect();
@@ -220,7 +233,7 @@ export function extractNetworkProtocol(nextHopProtocol: string): { name: string;
return { name, version };
}
-function getAbsoluteTime(time: number): number {
+function getAbsoluteTime(time: number = 0): number {
return ((browserPerformanceTimeOrigin || performance.timeOrigin) + time) / 1000;
}
diff --git a/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts b/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts
deleted file mode 100644
index 2193609e64f7..000000000000
--- a/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f
-//
-// The MIT License (MIT)
-//
-// Copyright (c) 2012-2018 various contributors (see AUTHORS)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import type { GenericObject } from './types';
-
-declare const exports: GenericObject;
-
-/**
- * Copy a property from the given object into `exports`, under the given name.
- *
- * Adapted from Sucrase (https://github.com/alangpierce/sucrase)
- *
- * @param obj The object containing the property to copy.
- * @param localName The name under which to export the property
- * @param importedName The name under which the property lives in `obj`
- */
-export function _createNamedExportFrom(obj: GenericObject, localName: string, importedName: string): void {
- exports[localName] = obj[importedName];
-}
-
-// Sucrase version:
-// function _createNamedExportFrom(obj, localName, importedName) {
-// Object.defineProperty(exports, localName, {enumerable: true, get: () => obj[importedName]});
-// }
diff --git a/packages/utils/src/buildPolyfills/_createStarExport.ts b/packages/utils/src/buildPolyfills/_createStarExport.ts
deleted file mode 100644
index 377d51e10a84..000000000000
--- a/packages/utils/src/buildPolyfills/_createStarExport.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f
-//
-// The MIT License (MIT)
-//
-// Copyright (c) 2012-2018 various contributors (see AUTHORS)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import type { GenericObject } from './types';
-
-declare const exports: GenericObject;
-
-/**
- * Copy properties from an object into `exports`.
- *
- * Adapted from Sucrase (https://github.com/alangpierce/sucrase)
- *
- * @param obj The object containing the properties to copy.
- */
-export function _createStarExport(obj: GenericObject): void {
- Object.keys(obj)
- .filter(key => key !== 'default' && key !== '__esModule' && !(key in exports))
- .forEach(key => (exports[key] = obj[key]));
-}
-
-// Sucrase version:
-// function _createStarExport(obj) {
-// Object.keys(obj)
-// .filter(key => key !== 'default' && key !== '__esModule')
-// .forEach(key => {
-// if (exports.hasOwnProperty(key)) {
-// return;
-// }
-// Object.defineProperty(exports, key, { enumerable: true, get: () => obj[key] });
-// });
-// }
diff --git a/packages/utils/src/buildPolyfills/_interopDefault.ts b/packages/utils/src/buildPolyfills/_interopDefault.ts
deleted file mode 100644
index 3a8c29d1bbaf..000000000000
--- a/packages/utils/src/buildPolyfills/_interopDefault.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-// https://github.com/rollup/rollup/tree/c2cda424e69686671ba010d628c0f70c43a563f8
-// The MIT License (MIT)
-//
-// Copyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software,
-// and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions
-// of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
-// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
-// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import type { RequireResult } from './types';
-
-/**
- * Unwraps a module if it has been wrapped in an object under the key `default`.
- *
- * Adapted from Rollup (https://github.com/rollup/rollup)
- *
- * @param requireResult The result of calling `require` on a module
- * @returns The full module, unwrapped if necessary.
- */
-export function _interopDefault(requireResult: RequireResult): RequireResult {
- return requireResult.__esModule ? (requireResult.default as RequireResult) : requireResult;
-}
-
-// Rollup version:
-// function _interopDefault(e) {
-// return e && e.__esModule ? e['default'] : e;
-// }
diff --git a/packages/utils/src/buildPolyfills/_interopNamespace.ts b/packages/utils/src/buildPolyfills/_interopNamespace.ts
deleted file mode 100644
index ed596090ff73..000000000000
--- a/packages/utils/src/buildPolyfills/_interopNamespace.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// https://github.com/rollup/rollup/tree/c2cda424e69686671ba010d628c0f70c43a563f8
-// The MIT License (MIT)
-//
-// Copyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software,
-// and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions
-// of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
-// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
-// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import type { RequireResult } from './types';
-
-/**
- * Adds a self-referential `default` property to CJS modules which aren't the result of transpilation from ESM modules.
- *
- * Adapted from Rollup (https://github.com/rollup/rollup)
- *
- * @param requireResult The result of calling `require` on a module
- * @returns Either `requireResult` or a copy of `requireResult` with an added self-referential `default` property
- */
-export function _interopNamespace(requireResult: RequireResult): RequireResult {
- return requireResult.__esModule ? requireResult : { ...requireResult, default: requireResult };
-}
-
-// Rollup version (with `output.externalLiveBindings` and `output.freeze` both set to false)
-// function _interopNamespace(e) {
-// if (e && e.__esModule) return e;
-// var n = Object.create(null);
-// if (e) {
-// for (var k in e) {
-// n[k] = e[k];
-// }
-// }
-// n["default"] = e;
-// return n;
-// }
diff --git a/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts b/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts
deleted file mode 100644
index a3b1de3ab3b5..000000000000
--- a/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-// https://github.com/rollup/rollup/tree/c2cda424e69686671ba010d628c0f70c43a563f8
-// The MIT License (MIT)
-//
-// Copyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software,
-// and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions
-// of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
-// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
-// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import type { RequireResult } from './types';
-
-/**
- * Wrap a module in an object, as the value under the key `default`.
- *
- * Adapted from Rollup (https://github.com/rollup/rollup)
- *
- * @param requireResult The result of calling `require` on a module
- * @returns An object containing the key-value pair (`default`, `requireResult`)
- */
-export function _interopNamespaceDefaultOnly(requireResult: RequireResult): RequireResult {
- return {
- __proto__: null,
- default: requireResult,
- };
-}
-
-// Rollup version
-// function _interopNamespaceDefaultOnly(e) {
-// return {
-// __proto__: null,
-// 'default': e
-// };
-// }
diff --git a/packages/utils/src/buildPolyfills/_interopRequireDefault.ts b/packages/utils/src/buildPolyfills/_interopRequireDefault.ts
deleted file mode 100644
index 74122265c07e..000000000000
--- a/packages/utils/src/buildPolyfills/_interopRequireDefault.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f
-//
-// The MIT License (MIT)
-//
-// Copyright (c) 2012-2018 various contributors (see AUTHORS)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import type { RequireResult } from './types';
-
-/**
- * Wraps modules which aren't the result of transpiling an ESM module in an object under the key `default`
- *
- * Adapted from Sucrase (https://github.com/alangpierce/sucrase)
- *
- * @param requireResult The result of calling `require` on a module
- * @returns `requireResult` or `requireResult` wrapped in an object, keyed as `default`
- */
-export function _interopRequireDefault(requireResult: RequireResult): RequireResult {
- return requireResult.__esModule ? requireResult : { default: requireResult };
-}
-
-// Sucrase version
-// function _interopRequireDefault(obj) {
-// return obj && obj.__esModule ? obj : { default: obj };
-// }
diff --git a/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts b/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts
deleted file mode 100644
index 5be829e3e48a..000000000000
--- a/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f
-//
-// The MIT License (MIT)
-//
-// Copyright (c) 2012-2018 various contributors (see AUTHORS)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import type { RequireResult } from './types';
-
-/**
- * Adds a `default` property to CJS modules which aren't the result of transpilation from ESM modules.
- *
- * Adapted from Sucrase (https://github.com/alangpierce/sucrase)
- *
- * @param requireResult The result of calling `require` on a module
- * @returns Either `requireResult` or a copy of `requireResult` with an added self-referential `default` property
- */
-export function _interopRequireWildcard(requireResult: RequireResult): RequireResult {
- return requireResult.__esModule ? requireResult : { ...requireResult, default: requireResult };
-}
-
-// Sucrase version
-// function _interopRequireWildcard(obj) {
-// if (obj && obj.__esModule) {
-// return obj;
-// } else {
-// var newObj = {};
-// if (obj != null) {
-// for (var key in obj) {
-// if (Object.prototype.hasOwnProperty.call(obj, key)) {
-// newObj[key] = obj[key];
-// }
-// }
-// }
-// newObj.default = obj;
-// return newObj;
-// }
-// }
diff --git a/packages/utils/src/buildPolyfills/index.ts b/packages/utils/src/buildPolyfills/index.ts
index 9717453e98fa..2017dcbd9592 100644
--- a/packages/utils/src/buildPolyfills/index.ts
+++ b/packages/utils/src/buildPolyfills/index.ts
@@ -1,13 +1,6 @@
export { _asyncNullishCoalesce } from './_asyncNullishCoalesce';
export { _asyncOptionalChain } from './_asyncOptionalChain';
export { _asyncOptionalChainDelete } from './_asyncOptionalChainDelete';
-export { _createNamedExportFrom } from './_createNamedExportFrom';
-export { _createStarExport } from './_createStarExport';
-export { _interopDefault } from './_interopDefault';
-export { _interopNamespace } from './_interopNamespace';
-export { _interopNamespaceDefaultOnly } from './_interopNamespaceDefaultOnly';
-export { _interopRequireDefault } from './_interopRequireDefault';
-export { _interopRequireWildcard } from './_interopRequireWildcard';
export { _nullishCoalesce } from './_nullishCoalesce';
export { _optionalChain } from './_optionalChain';
export { _optionalChainDelete } from './_optionalChainDelete';
diff --git a/packages/utils/test/buildPolyfills/interop.test.ts b/packages/utils/test/buildPolyfills/interop.test.ts
deleted file mode 100644
index a53c64eb0979..000000000000
--- a/packages/utils/test/buildPolyfills/interop.test.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import {
- _interopDefault,
- _interopNamespace,
- _interopNamespaceDefaultOnly,
- _interopRequireDefault,
- _interopRequireWildcard,
-} from '../../src/buildPolyfills';
-import type { RequireResult } from '../../src/buildPolyfills/types';
-import {
- _interopDefault as _interopDefaultOrig,
- _interopNamespace as _interopNamespaceOrig,
- _interopNamespaceDefaultOnly as _interopNamespaceDefaultOnlyOrig,
- _interopRequireDefault as _interopRequireDefaultOrig,
- _interopRequireWildcard as _interopRequireWildcardOrig,
-} from './originals';
-
-// This file tests five different functions against a range of test cases. Though the inputs are the same for each
-// function's test cases, the expected output differs. The testcases for each function are therefore built from separate
-// collections of expected inputs and expected outputs. Further, for readability purposes, the tests labels have also
-// been split into their own object. It's also worth noting that in real life, there are some test-case/function
-// pairings which would never happen, but by testing all combinations, we're guaranteed to have tested the ones which
-// show up in the wild.
-
-const dogStr = 'dogs are great!';
-const dogFunc = () => dogStr;
-const dogAdjectives = { maisey: 'silly', charlie: 'goofy' };
-
-const withESModuleFlag = { __esModule: true, ...dogAdjectives };
-const withESModuleFlagAndDefault = { __esModule: true, default: dogFunc, ...dogAdjectives };
-const namedExports = { ...dogAdjectives };
-const withNonEnumerableProp = { ...dogAdjectives };
-// Properties added using `Object.defineProperty` are non-enumerable by default
-Object.defineProperty(withNonEnumerableProp, 'hiddenProp', { value: 'shhhhhhhh' });
-const withDefaultExport = { default: dogFunc, ...dogAdjectives };
-const withOnlyDefaultExport = { default: dogFunc };
-const exportsEquals = dogFunc as RequireResult;
-const exportsEqualsWithDefault = dogFunc as RequireResult;
-exportsEqualsWithDefault.default = exportsEqualsWithDefault;
-
-const mockRequireResults: Record = {
- withESModuleFlag,
- withESModuleFlagAndDefault,
- namedExports,
- withNonEnumerableProp,
- withDefaultExport,
- withOnlyDefaultExport,
- exportsEquals: exportsEquals,
- exportsEqualsWithDefault: exportsEqualsWithDefault as unknown as RequireResult,
-};
-
-const testLabels: Record = {
- withESModuleFlag: 'module with `__esModule` flag',
- withESModuleFlagAndDefault: 'module with `__esModule` flag and default export',
- namedExports: 'module with named exports',
- withNonEnumerableProp: 'module with named exports and non-enumerable prop',
- withDefaultExport: 'module with default export',
- withOnlyDefaultExport: 'module with only default export',
- exportsEquals: 'module using `exports =`',
- exportsEqualsWithDefault: 'module using `exports =` with default export',
-};
-
-function makeTestCases(expectedOutputs: Record): Array<[string, RequireResult, RequireResult]> {
- return Object.keys(mockRequireResults).map(key => [testLabels[key], mockRequireResults[key], expectedOutputs[key]]);
-}
-
-describe('_interopNamespace', () => {
- describe('returns the same result as the original', () => {
- const expectedOutputs: Record = {
- withESModuleFlag: withESModuleFlag,
- withESModuleFlagAndDefault: withESModuleFlagAndDefault,
- namedExports: { ...namedExports, default: namedExports },
- withNonEnumerableProp: {
- ...withNonEnumerableProp,
- default: withNonEnumerableProp,
- },
- withDefaultExport: { ...withDefaultExport, default: withDefaultExport },
- withOnlyDefaultExport: { default: withOnlyDefaultExport },
- exportsEquals: { default: exportsEquals },
- exportsEqualsWithDefault: { default: exportsEqualsWithDefault },
- };
-
- const testCases = makeTestCases(expectedOutputs);
-
- it.each(testCases)('%s', (_, requireResult, expectedOutput) => {
- expect(_interopNamespace(requireResult)).toEqual(_interopNamespaceOrig(requireResult));
- expect(_interopNamespace(requireResult)).toEqual(expectedOutput);
- });
- });
-});
-
-describe('_interopNamespaceDefaultOnly', () => {
- describe('returns the same result as the original', () => {
- const expectedOutputs: Record = {
- withESModuleFlag: { default: withESModuleFlag },
- withESModuleFlagAndDefault: { default: withESModuleFlagAndDefault },
- namedExports: { default: namedExports },
- withNonEnumerableProp: { default: withNonEnumerableProp },
- withDefaultExport: { default: withDefaultExport },
- withOnlyDefaultExport: { default: withOnlyDefaultExport },
- exportsEquals: { default: exportsEquals },
- exportsEqualsWithDefault: { default: exportsEqualsWithDefault },
- };
-
- const testCases = makeTestCases(expectedOutputs);
-
- it.each(testCases)('%s', (_, requireResult, expectedOutput) => {
- expect(_interopNamespaceDefaultOnly(requireResult)).toEqual(_interopNamespaceDefaultOnlyOrig(requireResult));
- expect(_interopNamespaceDefaultOnly(requireResult)).toEqual(expectedOutput);
- });
- });
-});
-
-describe('_interopRequireWildcard', () => {
- describe('returns the same result as the original', () => {
- const expectedOutputs: Record = {
- withESModuleFlag: withESModuleFlag,
- withESModuleFlagAndDefault: withESModuleFlagAndDefault,
- namedExports: { ...namedExports, default: namedExports },
- withNonEnumerableProp: {
- ...withNonEnumerableProp,
- default: withNonEnumerableProp,
- },
- withDefaultExport: { ...withDefaultExport, default: withDefaultExport },
- withOnlyDefaultExport: { default: withOnlyDefaultExport },
- exportsEquals: { default: exportsEquals },
- exportsEqualsWithDefault: { default: exportsEqualsWithDefault },
- };
-
- const testCases = makeTestCases(expectedOutputs);
-
- it.each(testCases)('%s', (_, requireResult, expectedOutput) => {
- expect(_interopRequireWildcard(requireResult)).toEqual(_interopRequireWildcardOrig(requireResult));
- expect(_interopRequireWildcard(requireResult)).toEqual(expectedOutput);
- });
- });
-});
-
-describe('_interopDefault', () => {
- describe('returns the same result as the original', () => {
- const expectedOutputs: Record = {
- withESModuleFlag: undefined as unknown as RequireResult,
- withESModuleFlagAndDefault: withESModuleFlagAndDefault.default as RequireResult,
- namedExports: namedExports,
- withNonEnumerableProp: withNonEnumerableProp,
- withDefaultExport: withDefaultExport,
- withOnlyDefaultExport: withOnlyDefaultExport,
- exportsEquals: exportsEquals,
- exportsEqualsWithDefault: exportsEqualsWithDefault,
- };
-
- const testCases = makeTestCases(expectedOutputs);
-
- it.each(testCases)('%s', (_, requireResult, expectedOutput) => {
- expect(_interopDefault(requireResult)).toEqual(_interopDefaultOrig(requireResult));
- expect(_interopDefault(requireResult)).toEqual(expectedOutput);
- });
- });
-});
-
-describe('_interopRequireDefault', () => {
- describe('returns the same result as the original', () => {
- const expectedOutputs: Record = {
- withESModuleFlag: withESModuleFlag,
- withESModuleFlagAndDefault: withESModuleFlagAndDefault,
- namedExports: { default: namedExports },
- withNonEnumerableProp: { default: withNonEnumerableProp },
- withDefaultExport: { default: withDefaultExport },
- withOnlyDefaultExport: { default: withOnlyDefaultExport },
- exportsEquals: { default: exportsEquals },
- exportsEqualsWithDefault: { default: exportsEqualsWithDefault },
- };
-
- const testCases = makeTestCases(expectedOutputs);
-
- it.each(testCases)('%s', (_, requireResult, expectedOutput) => {
- expect(_interopRequireDefault(requireResult)).toEqual(_interopRequireDefaultOrig(requireResult));
- expect(_interopRequireDefault(requireResult)).toEqual(expectedOutput);
- });
- });
-});
diff --git a/packages/utils/test/buildPolyfills/originals.d.ts b/packages/utils/test/buildPolyfills/originals.d.ts
index 323d6f26e93c..c2032b265476 100644
--- a/packages/utils/test/buildPolyfills/originals.d.ts
+++ b/packages/utils/test/buildPolyfills/originals.d.ts
@@ -8,16 +8,6 @@
export function _asyncNullishCoalesce(lhs: any, rhsFn: any): Promise;
export function _asyncOptionalChain(ops: any): Promise;
export function _asyncOptionalChainDelete(ops: any): Promise;
-export function _createNamedExportFrom(obj: any, localName: any, importedName: any): void;
-export function _createStarExport(obj: any): void;
-export function _interopDefault(e: any): any;
-export function _interopNamespace(e: any): any;
-export function _interopNamespaceDefaultOnly(e: any): {
- __proto__: any;
- default: any;
-};
-export function _interopRequireDefault(obj: any): any;
-export function _interopRequireWildcard(obj: any): any;
export function _nullishCoalesce(lhs: any, rhsFn: any): any;
export function _optionalChain(ops: any): any;
export function _optionalChainDelete(ops: any): any;
diff --git a/packages/utils/test/buildPolyfills/originals.js b/packages/utils/test/buildPolyfills/originals.js
index 969591755367..5ec688de93ac 100644
--- a/packages/utils/test/buildPolyfills/originals.js
+++ b/packages/utils/test/buildPolyfills/originals.js
@@ -40,73 +40,6 @@ export async function _asyncOptionalChainDelete(ops) {
return result == null ? true : result;
}
-// From Sucrase
-export function _createNamedExportFrom(obj, localName, importedName) {
- Object.defineProperty(exports, localName, { enumerable: true, get: () => obj[importedName] });
-}
-
-// From Sucrase
-export function _createStarExport(obj) {
- Object.keys(obj)
- .filter(key => key !== 'default' && key !== '__esModule')
- .forEach(key => {
- // eslint-disable-next-line no-prototype-builtins
- if (exports.hasOwnProperty(key)) {
- return;
- }
- Object.defineProperty(exports, key, { enumerable: true, get: () => obj[key] });
- });
-}
-
-// From Rollup
-export function _interopDefault(e) {
- return e && e.__esModule ? e['default'] : e;
-}
-
-// From Rollup
-export function _interopNamespace(e) {
- if (e && e.__esModule) return e;
- var n = Object.create(null);
- if (e) {
- // eslint-disable-next-line guard-for-in
- for (var k in e) {
- n[k] = e[k];
- }
- }
- n['default'] = e;
- return n;
-}
-
-export function _interopNamespaceDefaultOnly(e) {
- return {
- __proto__: null,
- default: e,
- };
-}
-
-// From Sucrase
-export function _interopRequireDefault(obj) {
- return obj && obj.__esModule ? obj : { default: obj };
-}
-
-// From Sucrase
-export function _interopRequireWildcard(obj) {
- if (obj && obj.__esModule) {
- return obj;
- } else {
- var newObj = {};
- if (obj != null) {
- for (var key in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
- newObj[key] = obj[key];
- }
- }
- }
- newObj.default = obj;
- return newObj;
- }
-}
-
// From Sucrase
export function _nullishCoalesce(lhs, rhsFn) {
if (lhs != null) {
diff --git a/rollup/plugins/extractPolyfillsPlugin.js b/rollup/plugins/extractPolyfillsPlugin.js
index e7b83b23dd35..134f39c64bdb 100644
--- a/rollup/plugins/extractPolyfillsPlugin.js
+++ b/rollup/plugins/extractPolyfillsPlugin.js
@@ -7,13 +7,6 @@ const POLYFILL_NAMES = new Set([
'_asyncNullishCoalesce',
'_asyncOptionalChain',
'_asyncOptionalChainDelete',
- '_createNamedExportFrom',
- '_createStarExport',
- '_interopDefault', // rollup's version
- '_interopNamespace', // rollup's version
- '_interopNamespaceDefaultOnly',
- '_interopRequireDefault', // sucrase's version
- '_interopRequireWildcard', // sucrase's version
'_nullishCoalesce',
'_optionalChain',
'_optionalChainDelete',
diff --git a/yarn.lock b/yarn.lock
index af50dde468cf..2f4c50f00895 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -23094,9 +23094,9 @@ proper-lockfile@^4.1.2:
signal-exit "^3.0.2"
protobufjs@^6.10.2, protobufjs@^6.8.6:
- version "6.11.3"
- resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74"
- integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==
+ version "6.11.4"
+ resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa"
+ integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"