Skip to content

Commit b47c593

Browse files
committed
Merge branch 'develop' into feat-replay-change-sampling-logic
2 parents ff5fc97 + d1b09c6 commit b47c593

File tree

20 files changed

+319
-84
lines changed

20 files changed

+319
-84
lines changed

.github/workflows/label-last-commenter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
github.event.comment.author_association != 'COLLABORATOR'
1717
&& github.event.comment.author_association != 'MEMBER'
1818
&& github.event.comment.author_association != 'OWNER'
19-
&& !github.event.issue.closed
19+
&& github.event.issue.state == 'open'
2020
uses: actions-ecosystem/action-add-labels@v1
2121
with:
2222
labels: 'Waiting for: Team'

packages/browser-integration-tests/suites/replay/sessionExpiry/init.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Sentry.init({
1717
});
1818

1919
window.Replay._replay.timeouts = {
20-
sessionIdle: 2000, // this is usually 5min, but we want to test this with shorter times
20+
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
21+
sessionIdleExpire: 2000, // this is usually 15min, but we want to test this with shorter times
2122
maxSessionLife: 3600000, // default: 60min
2223
};

packages/browser-integration-tests/suites/replay/sessionInactive/init.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Sentry.init({
1717
});
1818

1919
window.Replay._replay.timeouts = {
20-
sessionIdle: 1000, // default: 5min
21-
maxSessionLife: 2000, // this is usually 60min, but we want to test this with shorter times
20+
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
21+
sessionIdleExpire: 900000, // defayult: 15min
22+
maxSessionLife: 3600000, // default: 60min
2223
};

packages/browser-integration-tests/suites/replay/sessionInactive/test.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
waitForReplayRequest,
1212
} from '../../../utils/replayHelpers';
1313

14-
// Session should expire after 2s - keep in sync with init.js
15-
const SESSION_TIMEOUT = 2000;
14+
// Session should be paused after 2s - keep in sync with init.js
15+
const SESSION_PAUSED = 2000;
1616

1717
sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) => {
1818
if (shouldSkipReplayTest()) {
@@ -44,11 +44,8 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) =>
4444

4545
await page.click('#button1');
4646

47-
// We wait for another segment 0
48-
const reqPromise1 = waitForReplayRequest(page, 0);
49-
5047
// Now we wait for the session timeout, nothing should be sent in the meanwhile
51-
await new Promise(resolve => setTimeout(resolve, SESSION_TIMEOUT));
48+
await new Promise(resolve => setTimeout(resolve, SESSION_PAUSED));
5249

5350
// nothing happened because no activity/inactivity was detected
5451
const replay = await getReplaySnapshot(page);
@@ -64,17 +61,17 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) =>
6461
expect(replay2._isEnabled).toEqual(true);
6562
expect(replay2._isPaused).toEqual(true);
6663

67-
// Trigger an action, should re-start the recording
64+
// We wait for next segment to be sent once we resume the session
65+
const reqPromise1 = waitForReplayRequest(page);
66+
67+
// Trigger an action, should resume the recording
6868
await page.click('#button2');
6969
const req1 = await reqPromise1;
7070

7171
const replay3 = await getReplaySnapshot(page);
7272
expect(replay3._isEnabled).toEqual(true);
7373
expect(replay3._isPaused).toEqual(false);
7474

75-
const replayEvent1 = getReplayEvent(req1);
76-
expect(replayEvent1).toEqual(getExpectedReplayEvent({}));
77-
7875
const fullSnapshots1 = getFullRecordingSnapshots(req1);
7976
expect(fullSnapshots1.length).toEqual(1);
8077
const stringifiedSnapshot1 = normalize(fullSnapshots1[0]);

packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Sentry.init({
1717
});
1818

1919
window.Replay._replay.timeouts = {
20-
sessionIdle: 300000, // default: 5min
20+
sessionIdlePause: 300000, // default: 5min
21+
sessionIdleExpire: 900000, // default: 15min
2122
maxSessionLife: 4000, // this is usually 60min, but we want to test this with shorter times
2223
};

packages/core/src/transports/multiplexed.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ interface MatchParam {
1717
/**
1818
* A function that returns an event from the envelope if one exists. You can optionally pass an array of envelope item
1919
* types to filter by - only envelopes matching the given types will be multiplexed.
20+
* Allowed values are: 'event', 'transaction', 'profile', 'replay_event'
2021
*
21-
* @param types Defaults to ['event', 'transaction', 'profile', 'replay_event']
22+
* @param types Defaults to ['event']
2223
*/
2324
getEvent(types?: EnvelopeItemType[]): Event | undefined;
2425
}
@@ -61,8 +62,7 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
6162

6263
async function send(envelope: Envelope): Promise<void | TransportMakeRequestResponse> {
6364
function getEvent(types?: EnvelopeItemType[]): Event | undefined {
64-
const eventTypes: EnvelopeItemType[] =
65-
types && types.length ? types : ['event', 'transaction', 'profile', 'replay_event'];
65+
const eventTypes: EnvelopeItemType[] = types && types.length ? types : ['event'];
6666
return eventFromEnvelope(envelope, eventTypes);
6767
}
6868

packages/core/test/lib/transports/multiplexed.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { BaseTransportOptions, ClientReport, EventEnvelope, EventItem, Transport } from '@sentry/types';
1+
import type {
2+
BaseTransportOptions,
3+
ClientReport,
4+
EventEnvelope,
5+
EventItem,
6+
TransactionEvent,
7+
Transport,
8+
} from '@sentry/types';
29
import { createClientReportEnvelope, createEnvelope, dsnFromString } from '@sentry/utils';
310
import { TextEncoder } from 'util';
411

@@ -15,9 +22,10 @@ const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4b
1522
[{ type: 'event' }, ERROR_EVENT] as EventItem,
1623
]);
1724

25+
const TRANSACTION_EVENT: TransactionEvent = { type: 'transaction', event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' };
1826
const TRANSACTION_ENVELOPE = createEnvelope<EventEnvelope>(
1927
{ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' },
20-
[[{ type: 'transaction' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem],
28+
[[{ type: 'transaction' }, TRANSACTION_EVENT] as EventItem],
2129
);
2230

2331
const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [
@@ -143,15 +151,32 @@ describe('makeMultiplexedTransport', () => {
143151
await transport.send(CLIENT_REPORT_ENVELOPE);
144152
});
145153

146-
it('callback getEvent can ignore transactions', async () => {
154+
it('callback getEvent ignores transactions by default', async () => {
147155
expect.assertions(2);
148156

149157
const makeTransport = makeMultiplexedTransport(
150158
createTestTransport(url => {
151159
expect(url).toBe(DSN2_URL);
152160
}),
153161
({ getEvent }) => {
154-
expect(getEvent(['event'])).toBeUndefined();
162+
expect(getEvent()).toBeUndefined();
163+
return [DSN2];
164+
},
165+
);
166+
167+
const transport = makeTransport({ url: DSN1_URL, ...transportOptions });
168+
await transport.send(TRANSACTION_ENVELOPE);
169+
});
170+
171+
it('callback getEvent can define envelope types', async () => {
172+
expect.assertions(2);
173+
174+
const makeTransport = makeMultiplexedTransport(
175+
createTestTransport(url => {
176+
expect(url).toBe(DSN2_URL);
177+
}),
178+
({ getEvent }) => {
179+
expect(getEvent(['event', 'transaction'])).toBe(TRANSACTION_EVENT);
155180
return [DSN2];
156181
},
157182
);

packages/e2e-tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"glob": "8.0.3",
2828
"ts-node": "10.9.1",
2929
"typescript": "3.8.3",
30-
"yaml": "2.1.1"
30+
"yaml": "2.2.2"
3131
},
3232
"volta": {
3333
"extends": "../../package.json"

packages/replay/src/constants.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ export const REPLAY_EVENT_NAME = 'replay_event';
1111
export const RECORDING_EVENT_NAME = 'replay_recording';
1212
export const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
1313

14-
// The idle limit for a session
15-
export const SESSION_IDLE_DURATION = 300_000; // 5 minutes in ms
14+
// The idle limit for a session after which recording is paused.
15+
export const SESSION_IDLE_PAUSE_DURATION = 300_000; // 5 minutes in ms
16+
17+
// The idle limit for a session after which the session expires.
18+
export const SESSION_IDLE_EXPIRE_DURATION = 900_000; // 15 minutes in ms
1619

1720
// The maximum length of a session
18-
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes
21+
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes in ms
1922

2023
/** Default flush delays */
2124
export const DEFAULT_FLUSH_MIN_DELAY = 5_000;

packages/replay/src/replay.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { captureException, getCurrentHub } from '@sentry/core';
44
import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types';
55
import { logger } from '@sentry/utils';
66

7-
import { BUFFER_CHECKOUT_TIME, MAX_SESSION_LIFE, SESSION_IDLE_DURATION, WINDOW } from './constants';
7+
import {
8+
BUFFER_CHECKOUT_TIME,
9+
MAX_SESSION_LIFE,
10+
SESSION_IDLE_EXPIRE_DURATION,
11+
SESSION_IDLE_PAUSE_DURATION,
12+
WINDOW,
13+
} from './constants';
814
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
915
import { createEventBuffer } from './eventBuffer';
1016
import { clearSession } from './session/clearSession';
@@ -63,7 +69,8 @@ export class ReplayContainer implements ReplayContainerInterface {
6369
* @hidden
6470
*/
6571
public readonly timeouts: Timeouts = {
66-
sessionIdle: SESSION_IDLE_DURATION,
72+
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
73+
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
6774
maxSessionLife: MAX_SESSION_LIFE,
6875
} as const;
6976

@@ -484,12 +491,12 @@ export class ReplayContainer implements ReplayContainerInterface {
484491
const oldSessionId = this.getSessionId();
485492

486493
// Prevent starting a new session if the last user activity is older than
487-
// SESSION_IDLE_DURATION. Otherwise non-user activity can trigger a new
494+
// SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
488495
// session+recording. This creates noisy replays that do not have much
489496
// content in them.
490497
if (
491498
this._lastActivity &&
492-
isExpired(this._lastActivity, this.timeouts.sessionIdle) &&
499+
isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
493500
this.session &&
494501
this.session.sampled === 'session'
495502
) {
@@ -723,7 +730,7 @@ export class ReplayContainer implements ReplayContainerInterface {
723730
const isSessionActive = this.checkAndHandleExpiredSession();
724731

725732
if (!isSessionActive) {
726-
// If the user has come back to the page within SESSION_IDLE_DURATION
733+
// If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
727734
// ms, we will re-use the existing session, otherwise create a new
728735
// session
729736
__DEBUG_BUILD__ && logger.log('[Replay] Document has become active, but session has expired');

packages/replay/src/types.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export interface SendReplayData {
2525
}
2626

2727
export interface Timeouts {
28-
sessionIdle: number;
28+
sessionIdlePause: number;
29+
sessionIdleExpire: number;
2930
maxSessionLife: number;
3031
}
3132

@@ -476,10 +477,7 @@ export interface ReplayContainer {
476477
performanceEvents: AllPerformanceEntry[];
477478
session: Session | undefined;
478479
recordingMode: ReplayRecordingMode;
479-
timeouts: {
480-
sessionIdle: number;
481-
maxSessionLife: number;
482-
};
480+
timeouts: Timeouts;
483481
isEnabled(): boolean;
484482
isPaused(): boolean;
485483
getContext(): InternalEventContext;

packages/replay/src/util/addEvent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function addEvent(
3131
// page has been left open and idle for a long period of time and user
3232
// comes back to trigger a new session. The performance entries rely on
3333
// `performance.timeOrigin`, which is when the page first opened.
34-
if (timestampInMs + replay.timeouts.sessionIdle < Date.now()) {
34+
if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
3535
return null;
3636
}
3737

packages/replay/src/util/isSessionExpired.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function isSessionExpired(session: Session, timeouts: Timeouts, targetTim
99
// First, check that maximum session length has not been exceeded
1010
isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
1111
// check that the idle timeout has not been exceeded (i.e. user has
12-
// performed an action within the last `idleTimeout` ms)
13-
isExpired(session.lastActivity, timeouts.sessionIdle, targetTime)
12+
// performed an action within the last `sessionIdleExpire` ms)
13+
isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
1414
);
1515
}

packages/replay/test/integration/errorSampleRate.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
BUFFER_CHECKOUT_TIME,
66
MAX_SESSION_LIFE,
77
REPLAY_SESSION_KEY,
8-
SESSION_IDLE_DURATION,
8+
SESSION_IDLE_EXPIRE_DURATION,
99
WINDOW,
1010
} from '../../src/constants';
1111
import type { ReplayContainer } from '../../src/replay';
@@ -252,15 +252,15 @@ describe('Integration | errorSampleRate', () => {
252252
});
253253
});
254254

255-
it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_DURATION]ms', async () => {
255+
it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => {
256256
Object.defineProperty(document, 'visibilityState', {
257257
configurable: true,
258258
get: function () {
259259
return 'visible';
260260
},
261261
});
262262

263-
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
263+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
264264

265265
document.dispatchEvent(new Event('visibilitychange'));
266266

@@ -284,8 +284,8 @@ describe('Integration | errorSampleRate', () => {
284284

285285
expect(replay).not.toHaveLastSentReplay();
286286

287-
// User comes back before `SESSION_IDLE_DURATION` elapses
288-
jest.advanceTimersByTime(SESSION_IDLE_DURATION - 100);
287+
// User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses
288+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100);
289289
Object.defineProperty(document, 'visibilityState', {
290290
configurable: true,
291291
get: function () {
@@ -466,6 +466,27 @@ describe('Integration | errorSampleRate', () => {
466466
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
467467
await new Promise(process.nextTick);
468468

469+
// Remains disabled!
470+
expect(replay.isEnabled()).toBe(false);
471+
});
472+
473+
// Should behave the same as above test
474+
it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => {
475+
// Idle for 15 minutes
476+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
477+
478+
const TEST_EVENT = {
479+
data: { name: 'lost event' },
480+
timestamp: BASE_TIMESTAMP,
481+
type: 3,
482+
};
483+
mockRecord._emitter(TEST_EVENT);
484+
expect(replay).not.toHaveLastSentReplay();
485+
486+
jest.runAllTimers();
487+
await new Promise(process.nextTick);
488+
489+
// We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode
469490
expect(replay).not.toHaveLastSentReplay();
470491
expect(replay.isEnabled()).toBe(true);
471492
expect(replay.isPaused()).toBe(false);
@@ -605,7 +626,7 @@ describe('Integration | errorSampleRate', () => {
605626
expect(replay).not.toHaveLastSentReplay();
606627

607628
// Go idle
608-
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
629+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
609630
await new Promise(process.nextTick);
610631

611632
mockRecord._emitter(TEST_EVENT);

0 commit comments

Comments
 (0)