diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js b/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js
deleted file mode 100644
index 46af904118a6..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as Sentry from '@sentry/browser';
-
-window.Sentry = Sentry;
-window.Replay = new Sentry.Replay({
- flushMinDelay: 500,
- flushMaxDelay: 500,
-});
-
-Sentry.init({
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- sampleRate: 0,
- replaysSessionSampleRate: 1.0,
- replaysOnErrorSampleRate: 0.0,
- debug: true,
-
- integrations: [window.Replay],
-});
-
-window.Replay._replay.timeouts = {
- sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
- sessionIdleExpire: 2000, // this is usually 15min, but we want to test this with shorter times
- maxSessionLife: 3600000, // default: 60min
-};
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts
deleted file mode 100644
index bd303c9e68c3..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { expect } from '@playwright/test';
-
-import { sentryTest } from '../../../utils/fixtures';
-import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates';
-import {
- getFullRecordingSnapshots,
- getReplayEvent,
- getReplaySnapshot,
- normalize,
- shouldSkipReplayTest,
- waitForReplayRequest,
-} from '../../../utils/replayHelpers';
-
-// Session should expire after 2s - keep in sync with init.js
-const SESSION_TIMEOUT = 2000;
-
-sentryTest('handles an expired session', async ({ browserName, getLocalTestPath, page }) => {
- // This test seems to only be flakey on firefox
- if (shouldSkipReplayTest() || ['firefox'].includes(browserName)) {
- sentryTest.skip();
- }
-
- const reqPromise0 = waitForReplayRequest(page, 0);
- const reqPromise1 = waitForReplayRequest(page, 1);
-
- await page.route('https://dsn.ingest.sentry.io/**/*', route => {
- return route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify({ id: 'test-id' }),
- });
- });
-
- const url = await getLocalTestPath({ testDir: __dirname });
-
- await page.goto(url);
- const req0 = await reqPromise0;
-
- const replayEvent0 = getReplayEvent(req0);
- expect(replayEvent0).toEqual(getExpectedReplayEvent({}));
-
- const fullSnapshots0 = getFullRecordingSnapshots(req0);
- expect(fullSnapshots0.length).toEqual(1);
- const stringifiedSnapshot = normalize(fullSnapshots0[0]);
- expect(stringifiedSnapshot).toMatchSnapshot('snapshot-0.json');
-
- // We wait for another segment 0
- const reqPromise2 = waitForReplayRequest(page, 0);
-
- await page.click('#button1');
- const req1 = await reqPromise1;
-
- const replayEvent1 = getReplayEvent(req1);
- expect(replayEvent1).toEqual(getExpectedReplayEvent({ segment_id: 1, urls: [] }));
-
- const replay = await getReplaySnapshot(page);
- const oldSessionId = replay.session?.id;
-
- await new Promise(resolve => setTimeout(resolve, SESSION_TIMEOUT));
-
- await page.click('#button2');
- const req2 = await reqPromise2;
-
- const replay2 = await getReplaySnapshot(page);
-
- expect(replay2.session?.id).not.toEqual(oldSessionId);
-
- const replayEvent2 = getReplayEvent(req2);
- expect(replayEvent2).toEqual(getExpectedReplayEvent({}));
-
- const fullSnapshots2 = getFullRecordingSnapshots(req2);
- expect(fullSnapshots2.length).toEqual(1);
- const stringifiedSnapshot2 = normalize(fullSnapshots2[0]);
- expect(stringifiedSnapshot2).toMatchSnapshot('snapshot-2.json');
-});
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-chromium.json b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-chromium.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-chromium.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-firefox.json b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-firefox.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-firefox.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-webkit.json b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-webkit.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-webkit.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2.json b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js b/packages/browser-integration-tests/suites/replay/sessionIdle/init.js
similarity index 91%
rename from packages/browser-integration-tests/suites/replay/sessionInactive/init.js
rename to packages/browser-integration-tests/suites/replay/sessionIdle/init.js
index 4c641d160d79..4a8d05c16b32 100644
--- a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js
+++ b/packages/browser-integration-tests/suites/replay/sessionIdle/init.js
@@ -18,6 +18,5 @@ Sentry.init({
window.Replay._replay.timeouts = {
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
- sessionIdleExpire: 900000, // defayult: 15min
maxSessionLife: 3600000, // default: 60min
};
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/template.html b/packages/browser-integration-tests/suites/replay/sessionIdle/template.html
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionExpiry/template.html
rename to packages/browser-integration-tests/suites/replay/sessionIdle/template.html
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts
similarity index 96%
rename from packages/browser-integration-tests/suites/replay/sessionInactive/test.ts
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts
index ef2b841cbea3..0e9f1e51cd6a 100644
--- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts
+++ b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts
@@ -14,7 +14,7 @@ import {
// Session should be paused after 2s - keep in sync with init.js
const SESSION_PAUSED = 2000;
-sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) => {
+sentryTest('pauses an idle session', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}
@@ -66,6 +66,7 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) =>
// Trigger an action, should resume the recording
await page.click('#button2');
+
const req1 = await reqPromise1;
const replay3 = await getReplaySnapshot(page);
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0-chromium.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0-chromium.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0-chromium.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0-chromium.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0-firefox.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0-firefox.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0-firefox.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0-firefox.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0-webkit.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0-webkit.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0-webkit.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0-webkit.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-0.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-0.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-chromium.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1-chromium.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-chromium.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1-chromium.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-firefox.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1-firefox.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-firefox.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1-firefox.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-webkit.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1-webkit.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-webkit.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1-webkit.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1.json b/packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1.json
similarity index 100%
rename from packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1.json
rename to packages/browser-integration-tests/suites/replay/sessionIdle/test.ts-snapshots/snapshot-1.json
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/template.html b/packages/browser-integration-tests/suites/replay/sessionInactive/template.html
deleted file mode 100644
index 7223a20f82ba..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionInactive/template.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-chromium.json b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-chromium.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-chromium.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-firefox.json b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-firefox.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-firefox.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-webkit.json b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-webkit.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0-webkit.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0.json b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0.json
deleted file mode 100644
index d510b410a343..000000000000
--- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-0.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "type": 2,
- "data": {
- "node": {
- "type": 0,
- "childNodes": [
- {
- "type": 1,
- "name": "html",
- "publicId": "",
- "systemId": "",
- "id": 2
- },
- {
- "type": 2,
- "tagName": "html",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "head",
- "attributes": {},
- "childNodes": [
- {
- "type": 2,
- "tagName": "meta",
- "attributes": {
- "charset": "utf-8"
- },
- "childNodes": [],
- "id": 5
- }
- ],
- "id": 4
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 6
- },
- {
- "type": 2,
- "tagName": "body",
- "attributes": {},
- "childNodes": [
- {
- "type": 3,
- "textContent": "\n ",
- "id": 8
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 1')",
- "id": "button1"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 10
- }
- ],
- "id": 9
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 11
- },
- {
- "type": 2,
- "tagName": "button",
- "attributes": {
- "onclick": "console.log('Test log 2')",
- "id": "button2"
- },
- "childNodes": [
- {
- "type": 3,
- "textContent": "***** **",
- "id": 13
- }
- ],
- "id": 12
- },
- {
- "type": 3,
- "textContent": "\n ",
- "id": 14
- },
- {
- "type": 3,
- "textContent": "\n\n",
- "id": 15
- }
- ],
- "id": 7
- }
- ],
- "id": 3
- }
- ],
- "id": 1
- },
- "initialOffset": {
- "left": 0,
- "top": 0
- }
- },
- "timestamp": [timestamp]
-}
\ No newline at end of file
diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js
index 0c16dc6ca3a1..9ab8bbe47797 100644
--- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js
+++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js
@@ -18,6 +18,5 @@ Sentry.init({
window.Replay._replay.timeouts = {
sessionIdlePause: 300000, // default: 5min
- sessionIdleExpire: 900000, // default: 15min
- maxSessionLife: 4000, // this is usually 60min, but we want to test this with shorter times
+ maxSessionLife: 2000, // this is usually 60min, but we want to test this with shorter times
};
diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts
index ca50c5a62203..4e7a8b123eef 100644
--- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts
+++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts
@@ -11,13 +11,8 @@ import {
waitForReplayRequest,
} from '../../../utils/replayHelpers';
-// Session should be max. 4s long
-const SESSION_MAX_AGE = 4000;
+const SESSION_MAX_AGE = 2000;
-/*
- The main difference between this and sessionExpiry test, is that here we wait for the overall time (4s)
- in multiple steps (2s, 2s) instead of waiting for the whole time at once (4s).
-*/
sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
diff --git a/packages/replay/jest.setup.ts b/packages/replay/jest.setup.ts
index 665d29e2386a..7e20018cef3a 100644
--- a/packages/replay/jest.setup.ts
+++ b/packages/replay/jest.setup.ts
@@ -129,21 +129,23 @@ function checkCallForSentReplay(
}
/**
-* Only want calls that send replay events, i.e. ignore error events
-*/
+ * Only want calls that send replay events, i.e. ignore error events
+ */
function getReplayCalls(calls: any[][][]): any[][][] {
- return calls.map(call => {
- const arg = call[0];
+ return calls
+ .map(call => {
+ const arg = call[0];
if (arg.length !== 2) {
return [];
}
- if (!arg[1][0].find(({type}: {type: string}) => ['replay_event', 'replay_recording'].includes(type))) {
+ if (!arg[1][0].find(({ type }: { type: string }) => ['replay_event', 'replay_recording'].includes(type))) {
return [];
}
- return [ arg ];
- }).filter(Boolean);
+ return [arg];
+ })
+ .filter(Boolean);
}
/**
@@ -159,9 +161,11 @@ const toHaveSentReplay = function (
let result: CheckCallForSentReplayResult;
- const expectedKeysLength = expected ? ('sample' in expected ? Object.keys(expected.sample) : Object.keys(expected)).length : 0;
+ const expectedKeysLength = expected
+ ? ('sample' in expected ? Object.keys(expected.sample) : Object.keys(expected)).length
+ : 0;
- const replayCalls = getReplayCalls(calls)
+ const replayCalls = getReplayCalls(calls);
for (const currentCall of replayCalls) {
result = checkCallForSentReplay.call(this, currentCall[0], expected);
@@ -213,7 +217,7 @@ const toHaveLastSentReplay = function (
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
) {
const { calls } = (getCurrentHub().getClient()?.getTransport()?.send as MockTransport).mock;
- const replayCalls = getReplayCalls(calls)
+ const replayCalls = getReplayCalls(calls);
const lastCall = replayCalls[calls.length - 1]?.[0];
diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts
index 86dc228c50bf..a09738dc594e 100644
--- a/packages/replay/src/constants.ts
+++ b/packages/replay/src/constants.ts
@@ -14,9 +14,6 @@ export const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
// The idle limit for a session after which recording is paused.
export const SESSION_IDLE_PAUSE_DURATION = 300_000; // 5 minutes in ms
-// The idle limit for a session after which the session expires.
-export const SESSION_IDLE_EXPIRE_DURATION = 900_000; // 15 minutes in ms
-
// The maximum length of a session
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes in ms
diff --git a/packages/replay/src/coreHandlers/handleHistory.ts b/packages/replay/src/coreHandlers/handleHistory.ts
index a72618a2203f..6a10b2c07076 100644
--- a/packages/replay/src/coreHandlers/handleHistory.ts
+++ b/packages/replay/src/coreHandlers/handleHistory.ts
@@ -39,12 +39,14 @@ export function handleHistorySpanListener(replay: ReplayContainer): (handlerData
// Need to collect visited URLs
replay.getContext().urls.push(result.name);
- replay.triggerUserActivity();
-
- replay.addUpdate(() => {
- createPerformanceSpans(replay, [result]);
- // Returning false to flush
- return false;
+ replay.updateUserActivity();
+
+ replay.checkSessionState(() => {
+ replay.addUpdate(() => {
+ createPerformanceSpans(replay, [result]);
+ // Returning false to flush
+ return false;
+ });
});
};
}
diff --git a/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts b/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts
index 947fb12f1ae4..090f0d259a8e 100644
--- a/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts
+++ b/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts
@@ -13,25 +13,25 @@ export function addBreadcrumbEvent(replay: ReplayContainer, breadcrumb: Breadcru
}
if (['ui.click', 'ui.input'].includes(breadcrumb.category as string)) {
- replay.triggerUserActivity();
- } else {
- replay.checkAndHandleExpiredSession();
+ replay.updateUserActivity();
}
- replay.addUpdate(() => {
- void replay.throttledAddEvent({
- type: EventType.Custom,
- // TODO: We were converting from ms to seconds for breadcrumbs, spans,
- // but maybe we should just keep them as milliseconds
- timestamp: (breadcrumb.timestamp || 0) * 1000,
- data: {
- tag: 'breadcrumb',
- // normalize to max. 10 depth and 1_000 properties per object
- payload: normalize(breadcrumb, 10, 1_000),
- },
- });
+ replay.checkSessionState(() => {
+ replay.addUpdate(() => {
+ void replay.throttledAddEvent({
+ type: EventType.Custom,
+ // TODO: We were converting from ms to seconds for breadcrumbs, spans,
+ // but maybe we should just keep them as milliseconds
+ timestamp: (breadcrumb.timestamp || 0) * 1000,
+ data: {
+ tag: 'breadcrumb',
+ // normalize to max. 10 depth and 1_000 properties per object
+ payload: normalize(breadcrumb, 10, 1_000),
+ },
+ });
- // Do not flush after console log messages
- return breadcrumb.category === 'console';
+ // Do not flush after console log messages
+ return breadcrumb.category === 'console';
+ });
});
}
diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts
index c72306239533..43d4f97cb1f5 100644
--- a/packages/replay/src/replay.ts
+++ b/packages/replay/src/replay.ts
@@ -7,7 +7,6 @@ import { logger } from '@sentry/utils';
import {
BUFFER_CHECKOUT_TIME,
MAX_SESSION_LIFE,
- SESSION_IDLE_EXPIRE_DURATION,
SESSION_IDLE_PAUSE_DURATION,
SLOW_CLICK_SCROLL_TIMEOUT,
SLOW_CLICK_THRESHOLD,
@@ -17,8 +16,9 @@ import { ClickDetector } from './coreHandlers/handleClick';
import { handleKeyboardEvent } from './coreHandlers/handleKeyboardEvent';
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
import { createEventBuffer } from './eventBuffer';
+import { checkSessionState } from './session/checkSessionState';
import { clearSession } from './session/clearSession';
-import { getSession } from './session/getSession';
+import { restoreOrCreateSession } from './session/restoreOrCreateSession';
import { saveSession } from './session/saveSession';
import type {
AddEventResult,
@@ -32,6 +32,7 @@ import type {
RecordingOptions,
ReplayContainer as ReplayContainerInterface,
ReplayPluginOptions,
+ Sampled,
SendBufferedReplayOptions,
Session,
SlowClickConfig,
@@ -45,8 +46,7 @@ import { createPerformanceEntries } from './util/createPerformanceEntries';
import { createPerformanceSpans } from './util/createPerformanceSpans';
import { debounce } from './util/debounce';
import { getHandleRecordingEmit } from './util/handleRecordingEmit';
-import { isExpired } from './util/isExpired';
-import { isSessionExpired } from './util/isSessionExpired';
+import { sampleSession } from './util/sampleSession';
import { sendReplay } from './util/sendReplay';
import type { SKIPPED } from './util/throttle';
import { throttle, THROTTLED } from './util/throttle';
@@ -87,7 +87,6 @@ export class ReplayContainer implements ReplayContainerInterface {
*/
public readonly timeouts: Timeouts = {
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
maxSessionLife: MAX_SESSION_LIFE,
} as const;
@@ -108,11 +107,6 @@ export class ReplayContainer implements ReplayContainerInterface {
private _debouncedFlush: ReturnType;
private _flushLock: Promise | null = null;
- /**
- * Timestamp of the last user activity. This lives across sessions.
- */
- private _lastActivity: number = Date.now();
-
/**
* Is the integration currently active?
*/
@@ -208,37 +202,13 @@ export class ReplayContainer implements ReplayContainerInterface {
*/
public initializeSampling(): void {
const { errorSampleRate, sessionSampleRate } = this._options;
+ const sampled = sampleSession({ errorSampleRate, sessionSampleRate });
- // If neither sample rate is > 0, then do nothing - user will need to call one of
- // `start()` or `startBuffering` themselves.
- if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
- return;
- }
-
- // 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;
- }
-
- if (!this.session) {
- // This should not happen, something wrong has occurred
- this._handleException(new Error('Unable to initialize and create session'));
+ if (!sampled) {
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';
- }
-
+ this._initializeSession(sampled);
this._initializeRecording();
}
@@ -258,20 +228,7 @@ export class ReplayContainer implements ReplayContainerInterface {
throw new Error('Replay buffering is in progress, call `flush()` to save the replay');
}
- 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,
- });
-
- session.previousSessionId = previousSessionId;
- this.session = session;
-
+ this._initializeSession('session', { forceSampled: true });
this._initializeRecording();
}
@@ -284,20 +241,7 @@ export class ReplayContainer implements ReplayContainerInterface {
throw new Error('Replay recording is already in progress');
}
- 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,
- });
-
- session.previousSessionId = previousSessionId;
- this.session = session;
-
- this.recordingMode = 'buffer';
+ this._initializeSession('buffer', { forceSampled: true });
this._initializeRecording();
}
@@ -379,8 +323,7 @@ export class ReplayContainer implements ReplayContainerInterface {
this.eventBuffer && this.eventBuffer.destroy();
this.eventBuffer = null;
- // Clear session from session storage, note this means if a new session
- // is started after, it will not have `previousSessionId`
+ // Clear session from session storage
clearSession(this);
} catch (err) {
this._handleException(err);
@@ -393,6 +336,10 @@ export class ReplayContainer implements ReplayContainerInterface {
* not as thorough of a shutdown as `stop()`.
*/
public pause(): void {
+ if (this.isPaused()) {
+ return;
+ }
+
this._isPaused = true;
this.stopRecording();
}
@@ -404,12 +351,14 @@ export class ReplayContainer implements ReplayContainerInterface {
* new DOM checkout.`
*/
public resume(): void {
- if (!this._loadAndCheckSession()) {
+ if (!this.isPaused()) {
return;
}
this._isPaused = false;
- this.startRecording();
+ this.checkSessionState(() => {
+ this.startRecording();
+ });
}
/**
@@ -444,20 +393,8 @@ export class ReplayContainer implements ReplayContainerInterface {
// starting a new recording
this.recordingMode = 'session';
- // Once this session ends, we do not want to refresh it
if (this.session) {
- this.session.shouldRefresh = false;
-
- // It's possible that the session lifespan is > max session lifespan
- // because we have been buffering beyond max session lifespan (we ignore
- // expiration given that `shouldRefresh` is true). Since we flip
- // `shouldRefresh`, the session could be considered expired due to
- // lifespan, which is not what we want. Update session start date to be
- // the current timestamp, so that session is not considered to be
- // expired. This means that max replay duration can be MAX_SESSION_LIFE +
- // (length of buffer), which we are ok with.
- this._updateUserActivity(activityTime);
- this._updateSessionActivity(activityTime);
+ this.session.lastActivity = activityTime;
this.session.started = activityTime;
this._maybeSaveSession();
}
@@ -495,42 +432,9 @@ export class ReplayContainer implements ReplayContainerInterface {
}
/**
- * Updates the user activity timestamp and resumes recording. This should be
- * called in an event handler for a user action that we consider as the user
- * being "active" (e.g. a mouse click).
- */
- public triggerUserActivity(): void {
- this._updateUserActivity();
-
- // This case means that recording was once stopped due to inactivity.
- // Ensure that recording is resumed.
- 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()) {
- return;
- }
-
- // Note: This will cause a new DOM checkout
- this.resume();
- return;
- }
-
- // Otherwise... recording was never suspended, continue as normalish
- this.checkAndHandleExpiredSession();
-
- this._updateSessionActivity();
- }
-
- /**
- * Updates the user activity timestamp *without* resuming
- * recording. Some user events (e.g. keydown) can be create
- * low-value replays that only contain the keypress as a
- * breadcrumb. Instead this would require other events to
- * create a new replay after a session has expired.
+ * Updates the user activity timestamp.
*/
public updateUserActivity(): void {
- this._updateUserActivity();
this._updateSessionActivity();
}
@@ -575,54 +479,6 @@ export class ReplayContainer implements ReplayContainerInterface {
return this.session && this.session.id;
}
- /**
- * Checks if recording should be stopped due to user inactivity. Otherwise
- * check if session is expired and create a new session if so. Triggers a new
- * full snapshot on new session.
- *
- * Returns true if session is not expired, false otherwise.
- * @hidden
- */
- public checkAndHandleExpiredSession(): boolean | void {
- const oldSessionId = this.getSessionId();
-
- // Prevent starting a new session if the last user activity is older than
- // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
- // session+recording. This creates noisy replays that do not have much
- // content in them.
- if (
- this._lastActivity &&
- isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
- this.session &&
- this.session.sampled === 'session'
- ) {
- // Pause recording only for session-based replays. Otherwise, resuming
- // will create a new replay and will conflict with users who only choose
- // to record error-based replays only. (e.g. the resumed replay will not
- // contain a reference to an error)
- this.pause();
- return;
- }
-
- // --- There is recent user activity --- //
- // This will create a new session if expired, based on expiry length
- if (!this._loadAndCheckSession()) {
- return;
- }
-
- // Session was expired if session ids do not match
- const expired = oldSessionId !== this.getSessionId();
-
- if (!expired) {
- return true;
- }
-
- // Session is expired, trigger a full snapshot (which will create a new session)
- this._triggerFullSnapshot();
-
- return false;
- }
-
/**
* Capture some initial state that can change throughout the lifespan of the
* replay. This is required because otherwise they would be captured at the
@@ -675,6 +531,23 @@ export class ReplayContainer implements ReplayContainerInterface {
return res;
}
+ /**
+ * Check the state/expiration of the session.
+ * The callback is called when the session is neither paused nor expired.
+ */
+ public checkSessionState(onContinue: () => void): void {
+ if (!this.session || !this.session.sampled) {
+ return;
+ }
+
+ checkSessionState(this.session, this.recordingMode, this.timeouts, {
+ onPause: () => this.pause(),
+ ensureResumed: () => this.resume(),
+ onEnd: () => this._refreshSession(),
+ onContinue,
+ });
+ }
+
/**
* This will get the parametrized route name of the current page.
* This is only available if performance is enabled, and if an instrumented router is used.
@@ -688,6 +561,31 @@ export class ReplayContainer implements ReplayContainerInterface {
return lastTransaction.name;
}
+ /**
+ * Initialize a new session.
+ */
+ private _initializeSession(sampled: Sampled, { forceSampled = false }: { forceSampled?: boolean } = {}): void {
+ const { stickySession } = this._options;
+
+ // If neither sample rate is > 0, then do nothing - user will need to call one of
+ // `start()` or `startBuffering` themselves.
+ if (!sampled) {
+ return;
+ }
+
+ const session = restoreOrCreateSession({ stickySession, sampled, forceSampled });
+
+ // This shouldn't be possible really, but just in case...
+ if (!session.sampled) {
+ return;
+ }
+
+ this.session = session;
+ this.recordingMode = this.session.sampled === 'buffer' ? 'buffer' : 'session';
+
+ return;
+ }
+
/**
* Initialize and start all listeners to varying events (DOM,
* Performance Observer, Recording, Sentry SDK, etc)
@@ -697,7 +595,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// this method is generally called on page load or manually - in both cases
// we should treat it as an activity
- this._updateSessionActivity();
+ this.updateUserActivity();
this.eventBuffer = createEventBuffer({
useCompression: this._options.useCompression,
@@ -708,6 +606,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
this._isEnabled = true;
+ this._isPaused = false;
this.startRecording();
}
@@ -722,37 +621,31 @@ export class ReplayContainer implements ReplayContainerInterface {
}
/**
- * Loads (or refreshes) the current session.
- * Returns false if session is not recorded.
+ * Refresh a session that has ended, either when it exceeded the max. age or when it was inactive for too long.
+ * This means there was a sampled & sent session before - this will never be called while the session is buffering.
*/
- 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',
- });
-
- // If session was newly created (i.e. was not loaded from storage), then
- // enable flag to create the root replay
- if (type === 'new') {
- this.setInitialState();
- }
-
- const currentSessionId = this.getSessionId();
- if (session.id !== currentSessionId) {
- session.previousSessionId = currentSessionId;
+ private _refreshSession(): void {
+ // To avoid firing this multiple times, we check if we are even recording
+ if (!this.isEnabled()) {
+ return;
}
- this.session = session;
+ const { errorSampleRate, sessionSampleRate } = this._options;
+ const sampled = sampleSession({ errorSampleRate, sessionSampleRate });
- if (!this.session.sampled) {
- void this.stop('session unsampled');
- return false;
+ if (!sampled || !this.session) {
+ void this.stop('session expired without new session');
+ return;
}
- return true;
+ void this.stop('session expired with refreshing').then(() => {
+ if (sampled === 'session') {
+ return this.start();
+ }
+ if (sampled === 'buffer') {
+ return this.startBuffering();
+ }
+ });
}
/**
@@ -860,71 +753,35 @@ export class ReplayContainer implements ReplayContainerInterface {
* Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
*/
private _doChangeToBackgroundTasks(breadcrumb?: BreadcrumbFrame): void {
- if (!this.session) {
- return;
- }
-
- const expired = isSessionExpired(this.session, this.timeouts);
-
- if (breadcrumb && !expired) {
- this._createCustomBreadcrumb(breadcrumb);
- }
+ this.checkSessionState(() => {
+ if (breadcrumb) {
+ this._createCustomBreadcrumb(breadcrumb);
+ }
- // Send replay when the page/tab becomes hidden. There is no reason to send
- // replay if it becomes visible, since no actions we care about were done
- // while it was hidden
- void this.conditionalFlush();
+ // Send replay when the page/tab becomes hidden. There is no reason to send
+ // replay if it becomes visible, since no actions we care about were done
+ // while it was hidden
+ void this.conditionalFlush();
+ });
}
/**
* Tasks to run when we consider a page to be visible (via focus and/or visibility)
*/
private _doChangeToForegroundTasks(breadcrumb?: BreadcrumbFrame): void {
- if (!this.session) {
- return;
- }
-
- const isSessionActive = this.checkAndHandleExpiredSession();
-
- if (!isSessionActive) {
- // If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
- // ms, we will re-use the existing session, otherwise create a new
- // session
- __DEBUG_BUILD__ && logger.log('[Replay] Document has become active, but session has expired');
- return;
- }
-
- if (breadcrumb) {
- this._createCustomBreadcrumb(breadcrumb);
- }
- }
-
- /**
- * Trigger rrweb to take a full snapshot which will cause this plugin to
- * create a new Replay event.
- */
- private _triggerFullSnapshot(checkout = true): void {
- try {
- __DEBUG_BUILD__ && logger.log('[Replay] Taking full rrweb snapshot');
- record.takeFullSnapshot(checkout);
- } catch (err) {
- this._handleException(err);
- }
- }
-
- /**
- * Update user activity (across session lifespans)
- */
- private _updateUserActivity(_lastActivity: number = Date.now()): void {
- this._lastActivity = _lastActivity;
+ this.checkSessionState(() => {
+ if (breadcrumb) {
+ this._createCustomBreadcrumb(breadcrumb);
+ }
+ });
}
/**
* Updates the session's last activity timestamp
*/
- private _updateSessionActivity(_lastActivity: number = Date.now()): void {
+ private _updateSessionActivity(lastActivity: number = Date.now()): void {
if (this.session) {
- this.session.lastActivity = _lastActivity;
+ this.session.lastActivity = lastActivity;
this._maybeSaveSession();
}
}
@@ -1090,42 +947,58 @@ export class ReplayContainer implements ReplayContainerInterface {
return;
}
- if (!this.checkAndHandleExpiredSession()) {
- __DEBUG_BUILD__ && logger.error('[Replay] Attempting to finish replay event after session expired.');
- return;
- }
+ return new Promise(resolve => {
+ if (!this.session) {
+ resolve();
+ return;
+ }
+ checkSessionState(this.session, this.recordingMode, this.timeouts, {
+ onPause: () => {
+ this.pause();
+ resolve();
+ },
+ ensureResumed: () => this.resume(),
+ onEnd: () => {
+ __DEBUG_BUILD__ && logger.error('[Replay] Attempting to finish replay event after session expired.');
+ this._refreshSession();
+ resolve();
+ },
+ onContinue: () => {
+ void this._flushLocked().then(resolve);
+ },
+ });
+ });
+ };
- if (!this.session) {
- __DEBUG_BUILD__ && logger.error('[Replay] No session found to flush.');
- return;
- }
+ /**
+ * Flush, while locking so any future flush attemps while this is ongoing are queued.
+ */
+ private async _flushLocked(): Promise {
+ try {
+ // A flush is about to happen, cancel any queued flushes
+ this._debouncedFlush.cancel();
- // A flush is about to happen, cancel any queued flushes
- this._debouncedFlush.cancel();
+ // this._flushLock acts as a lock so that future calls to `_flush()`
+ // will be blocked until this promise resolves
+ if (!this._flushLock) {
+ this._flushLock = this._runFlush();
+ await this._flushLock;
+ this._flushLock = null;
+ return;
+ }
- // this._flushLock acts as a lock so that future calls to `_flush()`
- // will be blocked until this promise resolves
- if (!this._flushLock) {
- this._flushLock = this._runFlush();
+ // Wait for previous flush to finish, then call the debounced `_flush()`.
+ // It's possible there are other flush requests queued and waiting for it
+ // to resolve. We want to reduce all outstanding requests (as well as any
+ // new flush requests that occur within a second of the locked flush
+ // completing) into a single flush.
await this._flushLock;
- this._flushLock = null;
- return;
+ } catch (error) {
+ __DEBUG_BUILD__ && logger.error(error);
}
- // Wait for previous flush to finish, then call the debounced `_flush()`.
- // It's possible there are other flush requests queued and waiting for it
- // to resolve. We want to reduce all outstanding requests (as well as any
- // new flush requests that occur within a second of the locked flush
- // completing) into a single flush.
-
- try {
- await this._flushLock;
- } catch (err) {
- __DEBUG_BUILD__ && logger.error(err);
- } finally {
- this._debouncedFlush();
- }
- };
+ this._debouncedFlush();
+ }
/** Save the session, if it is sticky */
private _maybeSaveSession(): void {
diff --git a/packages/replay/src/session/Session.ts b/packages/replay/src/session/Session.ts
index b5ecddcbdb84..95ca32ba3cda 100644
--- a/packages/replay/src/session/Session.ts
+++ b/packages/replay/src/session/Session.ts
@@ -20,6 +20,5 @@ export function makeSession(session: Partial & { sampled: Sampled }): S
lastActivity,
segmentId,
sampled,
- shouldRefresh: true,
};
}
diff --git a/packages/replay/src/session/checkSessionState.ts b/packages/replay/src/session/checkSessionState.ts
new file mode 100644
index 000000000000..bc1df40f65f8
--- /dev/null
+++ b/packages/replay/src/session/checkSessionState.ts
@@ -0,0 +1,45 @@
+import type { ReplayRecordingMode } from '@sentry/types';
+
+import type { Session, Timeouts } from '../types';
+import { isExpired } from '../util/isExpired';
+
+/** Check the state of the session. */
+export function checkSessionState(
+ session: Session,
+ recordingMode: ReplayRecordingMode,
+ timeouts: Timeouts,
+ callbacks: {
+ onPause: () => void;
+ ensureResumed: () => void;
+ onEnd: () => void;
+ onContinue: () => void;
+ },
+): void {
+ const _isIdle = (): boolean => {
+ return isExpired(session.lastActivity, timeouts.sessionIdlePause);
+ };
+
+ const _exceedsMaxLength = (): boolean => {
+ return isExpired(session.started, timeouts.maxSessionLife);
+ };
+
+ // When buffering, we never want to expire/end/pause/restart the recording
+ if (recordingMode === 'buffer') {
+ callbacks.onContinue();
+ return;
+ }
+
+ if (_exceedsMaxLength()) {
+ callbacks.onEnd();
+ return;
+ }
+
+ if (_isIdle()) {
+ callbacks.onPause();
+ return;
+ }
+
+ callbacks.ensureResumed();
+
+ callbacks.onContinue();
+}
diff --git a/packages/replay/src/session/createSession.ts b/packages/replay/src/session/createSession.ts
index f5f2d120b92b..841db948c93e 100644
--- a/packages/replay/src/session/createSession.ts
+++ b/packages/replay/src/session/createSession.ts
@@ -1,6 +1,6 @@
import { logger } from '@sentry/utils';
-import type { Sampled, Session, SessionOptions } from '../types';
+import type { Sampled, Session } from '../types';
import { isSampled } from '../util/isSampled';
import { saveSession } from './saveSession';
import { makeSession } from './Session';
@@ -17,8 +17,13 @@ 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 {
- const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
+export function createSession({
+ sampled,
+ stickySession = false,
+}: {
+ sampled: Sampled;
+ stickySession: boolean;
+}): Session {
const session = makeSession({
sampled,
});
diff --git a/packages/replay/src/session/getSession.ts b/packages/replay/src/session/getSession.ts
deleted file mode 100644
index 73554a8860de..000000000000
--- a/packages/replay/src/session/getSession.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { logger } from '@sentry/utils';
-
-import type { Session, SessionOptions, Timeouts } from '../types';
-import { isSessionExpired } from '../util/isSessionExpired';
-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;
-}
-
-/**
- * Get or create a session
- */
-export function getSession({
- timeouts,
- currentSession,
- stickySession,
- sessionSampleRate,
- allowBuffering,
-}: 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());
-
- 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 });
- return { type: 'new', session: discardedSession };
- } else {
- __DEBUG_BUILD__ && logger.log('[Replay] Session has expired');
- }
- // Otherwise continue to create a new session
- }
-
- const newSession = createSession({
- stickySession,
- sessionSampleRate,
- allowBuffering,
- });
-
- return { type: 'new', session: newSession };
-}
diff --git a/packages/replay/src/session/restoreOrCreateSession.ts b/packages/replay/src/session/restoreOrCreateSession.ts
new file mode 100644
index 000000000000..7fffcd91bb53
--- /dev/null
+++ b/packages/replay/src/session/restoreOrCreateSession.ts
@@ -0,0 +1,34 @@
+import { logger } from '@sentry/utils';
+
+import type { Sampled, Session } from '../types';
+import { createSession } from './createSession';
+import { fetchSession } from './fetchSession';
+
+/** Either restore a session from sessionStorage, or create a new one. */
+export function restoreOrCreateSession({
+ stickySession,
+ sampled,
+ forceSampled,
+}: {
+ stickySession: boolean;
+ sampled: Sampled;
+ forceSampled: boolean;
+}): Session {
+ const currentSession = stickySession && fetchSession();
+
+ if (currentSession && currentSession.sampled !== sampled && forceSampled) {
+ // If for whatever reason the session from sessionStorage has a different sampling, we force to the new sampling
+ __DEBUG_BUILD__ &&
+ logger.log(`[Replay] Tried to restore session with different sampling: ${currentSession.sampled} !== ${sampled}`);
+ } else if (currentSession) {
+ __DEBUG_BUILD__ && logger.log(`[Replay] Loaded session from sessionStorage: id=${currentSession.id}`);
+ return currentSession;
+ }
+
+ const newSession = createSession({
+ stickySession,
+ sampled,
+ });
+
+ return newSession;
+}
diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts
index 00cda5e3c6e7..0cda7108413e 100644
--- a/packages/replay/src/types/replay.ts
+++ b/packages/replay/src/types/replay.ts
@@ -30,7 +30,6 @@ export interface SendReplayData {
export interface Timeouts {
sessionIdlePause: number;
- sessionIdleExpire: number;
maxSessionLife: number;
}
@@ -206,18 +205,6 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
}>;
}
-/**
- * Session options that are configurable by the integration configuration
- */
-export interface SessionOptions extends Pick {
- /**
- * Should buffer recordings to be saved later either by error sampling, or by
- * manually calling `flush()`. This is only a factor if not sampled for a
- * session-based replay.
- */
- allowBuffering: boolean;
-}
-
export interface ReplayIntegrationPrivacyOptions {
/**
* Mask text content for elements that match the CSS selectors in the list.
@@ -351,22 +338,10 @@ export interface Session {
*/
segmentId: number;
- /**
- * The ID of the previous session.
- * If this is empty, there was no previous session.
- */
- previousSessionId?: string;
-
/**
* Is the session sampled? `false` if not sampled, otherwise, `session` or `buffer`
*/
sampled: Sampled;
-
- /**
- * If this is false, the session should not be refreshed when it was inactive.
- * This can be the case if you had a buffered session which is now recording because an error happened.
- */
- shouldRefresh: boolean;
}
export type EventBufferType = 'sync' | 'worker';
@@ -449,12 +424,11 @@ export interface ReplayContainer {
flush(): Promise;
flushImmediate(): Promise;
cancelFlush(): void;
- triggerUserActivity(): void;
updateUserActivity(): void;
addUpdate(cb: AddUpdateCallback): void;
getOptions(): ReplayPluginOptions;
getSessionId(): string | undefined;
- checkAndHandleExpiredSession(): boolean | void;
+ checkSessionState(onContinue: () => void): void;
setInitialState(): void;
getCurrentRoute(): string | undefined;
}
diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts
index 987f589412ed..42e0b10581d2 100644
--- a/packages/replay/src/util/handleRecordingEmit.ts
+++ b/packages/replay/src/util/handleRecordingEmit.ts
@@ -1,5 +1,4 @@
import { EventType } from '@sentry-internal/rrweb';
-import { logger } from '@sentry/utils';
import { saveSession } from '../session/saveSession';
import type { AddEventResult, OptionFrameEvent, RecordingEvent, ReplayContainer } from '../types';
@@ -16,79 +15,64 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
let hadFirstEvent = false;
return (event: RecordingEvent, _isCheckout?: boolean) => {
- // If this is false, it means session is expired, create and a new session and wait for checkout
- if (!replay.checkAndHandleExpiredSession()) {
- __DEBUG_BUILD__ && logger.warn('[Replay] Received replay event after session expired.');
-
- return;
- }
-
- // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
- // We also want to treat the first event as a checkout, so we handle this specifically here
- const isCheckout = _isCheckout || !hadFirstEvent;
- hadFirstEvent = true;
-
- // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
- replay.addUpdate(() => {
- // The session is always started immediately on pageload/init, but for
- // error-only replays, it should reflect the most recent checkout
- // when an error occurs. Clear any state that happens before this current
- // checkout. This needs to happen before `addEvent()` which updates state
- // dependent on this reset.
- if (replay.recordingMode === 'buffer' && isCheckout) {
- replay.setInitialState();
- }
-
- // We need to clear existing events on a checkout, otherwise they are
- // incremental event updates and should be appended
- void addEvent(replay, event, isCheckout);
-
- // Different behavior for full snapshots (type=2), ignore other event types
- // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
- if (!isCheckout) {
- return false;
- }
-
- // Additionally, create a meta event that will capture certain SDK settings.
- // In order to handle buffer mode, this needs to either be done when we
- // receive checkout events or at flush time.
- //
- // `isCheckout` is always true, but want to be explicit that it should
- // only be added for checkouts
- void addSettingsEvent(replay, isCheckout);
+ replay.checkSessionState(() => {
+ // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
+ // We also want to treat the first event as a checkout, so we handle this specifically here
+ const isCheckout = _isCheckout || !hadFirstEvent;
+ hadFirstEvent = true;
+
+ // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
+ replay.addUpdate(() => {
+ // The session is always started immediately on pageload/init, but for
+ // error-only replays, it should reflect the most recent checkout
+ // when an error occurs. Clear any state that happens before this current
+ // checkout. This needs to happen before `addEvent()` which updates state
+ // dependent on this reset.
+ if (replay.recordingMode === 'buffer' && isCheckout) {
+ replay.setInitialState();
+ }
- // If there is a previousSessionId after a full snapshot occurs, then
- // the replay session was started due to session expiration. The new session
- // is started before triggering a new checkout and contains the id
- // of the previous session. Do not immediately flush in this case
- // to avoid capturing only the checkout and instead the replay will
- // be captured if they perform any follow-up actions.
- if (replay.session && replay.session.previousSessionId) {
- return true;
- }
+ // We need to clear existing events on a checkout, otherwise they are
+ // incremental event updates and should be appended
+ void addEvent(replay, event, isCheckout);
- // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
- // this should usually be the timestamp of the checkout event, but to be safe...
- if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
- const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
- if (earliestEvent) {
- replay.session.started = earliestEvent;
+ // Different behavior for full snapshots (type=2), ignore other event types
+ // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
+ if (!isCheckout) {
+ return false;
+ }
- if (replay.getOptions().stickySession) {
- saveSession(replay.session);
+ // Additionally, create a meta event that will capture certain SDK settings.
+ // In order to handle buffer mode, this needs to either be done when we
+ // receive checkout events or at flush time.
+ //
+ // `isCheckout` is always true, but want to be explicit that it should
+ // only be added for checkouts
+ void addSettingsEvent(replay, isCheckout);
+
+ // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
+ // this should usually be the timestamp of the checkout event, but to be safe...
+ if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
+ const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
+ if (earliestEvent) {
+ replay.session.started = earliestEvent;
+
+ if (replay.getOptions().stickySession) {
+ saveSession(replay.session);
+ }
}
}
- }
- if (replay.recordingMode === 'session') {
- // If the full snapshot is due to an initial load, we will not have
- // a previous session ID. In this case, we want to buffer events
- // for a set amount of time before flushing. This can help avoid
- // capturing replays of users that immediately close the window.
- void replay.flush();
- }
+ if (replay.recordingMode === 'session') {
+ // If the full snapshot is due to an initial load, we will not have
+ // a previous session ID. In this case, we want to buffer events
+ // for a set amount of time before flushing. This can help avoid
+ // capturing replays of users that immediately close the window.
+ void replay.flush();
+ }
- return true;
+ return true;
+ });
});
};
}
diff --git a/packages/replay/src/util/isSessionExpired.ts b/packages/replay/src/util/isSessionExpired.ts
deleted file mode 100644
index a51104fd5a47..000000000000
--- a/packages/replay/src/util/isSessionExpired.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Session, Timeouts } from '../types';
-import { isExpired } from './isExpired';
-
-/**
- * Checks to see if session is expired
- */
-export function isSessionExpired(session: Session, timeouts: Timeouts, targetTime: number = +new Date()): boolean {
- return (
- // First, check that maximum session length has not been exceeded
- isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
- // check that the idle timeout has not been exceeded (i.e. user has
- // performed an action within the last `sessionIdleExpire` ms)
- isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
- );
-}
diff --git a/packages/replay/src/util/sampleSession.ts b/packages/replay/src/util/sampleSession.ts
new file mode 100644
index 000000000000..4a9223574e02
--- /dev/null
+++ b/packages/replay/src/util/sampleSession.ts
@@ -0,0 +1,23 @@
+import type { Sampled } from '../types';
+import { isSampled } from './isSampled';
+
+/**
+ * Sample a session based on the provided sample rates.
+ */
+export function sampleSession({
+ errorSampleRate,
+ sessionSampleRate,
+}: {
+ errorSampleRate: number;
+ sessionSampleRate: number;
+}): Sampled {
+ if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
+ return false;
+ }
+
+ if (isSampled(sessionSampleRate)) {
+ return 'session';
+ }
+
+ return errorSampleRate > 0 ? 'buffer' : false;
+}
diff --git a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
index 6bf33f182e66..b26f0e1b3586 100644
--- a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
+++ b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
@@ -84,7 +84,7 @@ 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['_initializeSession']('session');
mockSendReplayRequest.mockClear();
});
@@ -94,7 +94,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 fe3049f9704f..0a07ba502e79 100644
--- a/packages/replay/test/integration/errorSampleRate.test.ts
+++ b/packages/replay/test/integration/errorSampleRate.test.ts
@@ -1,11 +1,10 @@
-import { captureException, getCurrentHub } from '@sentry/core';
+import { captureException } from '@sentry/core';
import {
BUFFER_CHECKOUT_TIME,
DEFAULT_FLUSH_MIN_DELAY,
MAX_SESSION_LIFE,
REPLAY_SESSION_KEY,
- SESSION_IDLE_EXPIRE_DURATION,
WINDOW,
} from '../../src/constants';
import type { ReplayContainer } from '../../src/replay';
@@ -229,11 +228,9 @@ describe('Integration | errorSampleRate', () => {
// This tests a regression where we were calling flush indiscriminantly in `stop()`
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.
+ // rate is > 0
replay.stop();
- replay['_options']['errorSampleRate'] = 0;
- replay['_loadAndCheckSession']();
+ replay['_initializeSession']('buffer');
jest.runAllTimers();
await new Promise(process.nextTick);
@@ -241,7 +238,7 @@ describe('Integration | errorSampleRate', () => {
expect(replay).not.toHaveLastSentReplay();
});
- it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => {
+ it('does not send a replay when triggering a full dom snapshot when document becomes visible after 60s', async () => {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: function () {
@@ -249,7 +246,7 @@ describe('Integration | errorSampleRate', () => {
},
});
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
+ jest.advanceTimersByTime(60 * 1000);
document.dispatchEvent(new Event('visibilitychange'));
@@ -273,8 +270,7 @@ describe('Integration | errorSampleRate', () => {
expect(replay).not.toHaveLastSentReplay();
- // User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100);
+ jest.advanceTimersByTime(60 * 1000);
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: function () {
@@ -356,92 +352,36 @@ describe('Integration | errorSampleRate', () => {
expect(replay).not.toHaveLastSentReplay();
});
- // When the error session records as a normal session, we want to stop
- // recording after the session ends. Otherwise, we get into a state where the
- // new session is a session type replay (this could conflict with the session
- // sample rate of 0.0), or an error session that has no errors. Instead we
- // simply stop the session replay completely and wait for a new page load to
- // resample.
- it.each([
- ['MAX_SESSION_LIFE', MAX_SESSION_LIFE],
- ['SESSION_IDLE_DURATION', SESSION_IDLE_EXPIRE_DURATION],
- ])(
- 'stops replay if session had an error and exceeds %s and does not start a new session thereafter',
- async (_label, waitTime) => {
- expect(replay.session?.shouldRefresh).toBe(true);
-
- captureException(new Error('testing'));
-
- await waitForBufferFlush();
-
- expect(replay).toHaveLastSentReplay({
- recordingPayloadHeader: { segment_id: 0 },
- replayEventPayload: expect.objectContaining({
- replay_type: 'buffer',
- }),
- });
-
- await waitForFlush();
-
- // segment_id is 1 because it sends twice on error
- expect(replay).toHaveLastSentReplay({
- recordingPayloadHeader: { segment_id: 1 },
- replayEventPayload: expect.objectContaining({
- replay_type: 'buffer',
- }),
- });
- expect(replay.session?.shouldRefresh).toBe(false);
-
- // Idle for given time
- jest.advanceTimersByTime(waitTime + 1);
- await new Promise(process.nextTick);
-
- const TEST_EVENT = {
- data: { name: 'lost event' },
- timestamp: BASE_TIMESTAMP,
- type: 3,
- };
- mockRecord._emitter(TEST_EVENT);
-
- jest.runAllTimers();
- await new Promise(process.nextTick);
-
- // We stop recording after 15 minutes of inactivity in error mode
-
- // still no new replay sent
- expect(replay).toHaveLastSentReplay({
- recordingPayloadHeader: { segment_id: 1 },
- replayEventPayload: expect.objectContaining({
- replay_type: 'buffer',
- }),
- });
+ it('stops & restarts replay if session had an error and exceeds MAX_SESSION_LIFE', async () => {
+ captureException(new Error('testing'));
- expect(replay.isEnabled()).toBe(false);
+ await waitForBufferFlush();
- domHandler({
- name: 'click',
- });
+ expect(replay).toHaveLastSentReplay({
+ recordingPayloadHeader: { segment_id: 0 },
+ replayEventPayload: expect.objectContaining({
+ replay_type: 'buffer',
+ }),
+ });
- // Remains disabled!
- expect(replay.isEnabled()).toBe(false);
- },
- );
+ await waitForFlush();
- it.each([
- ['MAX_SESSION_LIFE', MAX_SESSION_LIFE],
- ['SESSION_IDLE_EXPIRE_DURATION', SESSION_IDLE_EXPIRE_DURATION],
- ])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => {
- const oldSessionId = replay.session?.id;
- expect(oldSessionId).toBeDefined();
+ // segment_id is 1 because it sends twice on error
+ expect(replay).toHaveLastSentReplay({
+ recordingPayloadHeader: { segment_id: 1 },
+ replayEventPayload: expect.objectContaining({
+ replay_type: 'buffer',
+ }),
+ });
- expect(replay).not.toHaveLastSentReplay();
+ const sessionId1 = replay.getSessionId();
// Idle for given time
- jest.advanceTimersByTime(waitTime + 1);
+ jest.advanceTimersByTime(MAX_SESSION_LIFE + 1);
await new Promise(process.nextTick);
const TEST_EVENT = {
- data: { name: 'lost event' },
+ data: { name: 'new session event' },
timestamp: BASE_TIMESTAMP,
type: 3,
};
@@ -450,52 +390,36 @@ describe('Integration | errorSampleRate', () => {
jest.runAllTimers();
await new Promise(process.nextTick);
- // still no new replay sent
- expect(replay).not.toHaveLastSentReplay();
-
- expect(replay.isEnabled()).toBe(true);
- expect(replay.isPaused()).toBe(false);
+ const sessionId2 = replay.getSessionId();
+ // Session has changed
+ expect(sessionId2).not.toBe(sessionId1);
expect(replay.recordingMode).toBe('buffer');
- domHandler({
- name: 'click',
- });
-
- await waitForFlush();
-
- expect(replay).not.toHaveLastSentReplay();
- expect(replay.isEnabled()).toBe(true);
- expect(replay.isPaused()).toBe(false);
- expect(replay.recordingMode).toBe('buffer');
-
- // should still react to errors later on
captureException(new Error('testing'));
await waitForBufferFlush();
- expect(replay.session?.id).toBe(oldSessionId);
-
+ // sent a new session
expect(replay).toHaveLastSentReplay({
recordingPayloadHeader: { segment_id: 0 },
replayEventPayload: expect.objectContaining({
replay_type: 'buffer',
+ replay_id: sessionId2,
}),
});
expect(replay.isEnabled()).toBe(true);
- expect(replay.isPaused()).toBe(false);
- expect(replay.recordingMode).toBe('session');
- expect(replay.session?.sampled).toBe('buffer');
- expect(replay.session?.shouldRefresh).toBe(false);
});
- // Should behave the same as above test
- it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => {
+ it('continues buffering replay if session had no error and exceeds MAX_SESSION_LIFE', async () => {
const oldSessionId = replay.session?.id;
expect(oldSessionId).toBeDefined();
- // Idle for 15 minutes
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
+ expect(replay).not.toHaveLastSentReplay();
+
+ // Idle for given time
+ jest.advanceTimersByTime(MAX_SESSION_LIFE + 1);
+ await new Promise(process.nextTick);
const TEST_EVENT = {
data: { name: 'lost event' },
@@ -503,12 +427,23 @@ describe('Integration | errorSampleRate', () => {
type: 3,
};
mockRecord._emitter(TEST_EVENT);
- expect(replay).not.toHaveLastSentReplay();
jest.runAllTimers();
await new Promise(process.nextTick);
- // We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode
+ // still no new replay sent
+ expect(replay).not.toHaveLastSentReplay();
+
+ expect(replay.isEnabled()).toBe(true);
+ expect(replay.isPaused()).toBe(false);
+ expect(replay.recordingMode).toBe('buffer');
+
+ domHandler({
+ name: 'click',
+ });
+
+ await waitForFlush();
+
expect(replay).not.toHaveLastSentReplay();
expect(replay.isEnabled()).toBe(true);
expect(replay.isPaused()).toBe(false);
@@ -517,20 +452,10 @@ describe('Integration | errorSampleRate', () => {
// should still react to errors later on
captureException(new Error('testing'));
- await new Promise(process.nextTick);
- jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
- await new Promise(process.nextTick);
- expect(replay.session?.id).toBe(oldSessionId);
+ await waitForBufferFlush();
- // buffered events
- expect(replay).toHaveSentReplay({
- recordingPayloadHeader: { segment_id: 0 },
- replayEventPayload: expect.objectContaining({
- replay_type: 'buffer',
- }),
- });
+ expect(replay.session?.id).toBe(oldSessionId);
- // `startRecording` full checkout
expect(replay).toHaveLastSentReplay({
recordingPayloadHeader: { segment_id: 0 },
replayEventPayload: expect.objectContaining({
@@ -542,7 +467,6 @@ describe('Integration | errorSampleRate', () => {
expect(replay.isPaused()).toBe(false);
expect(replay.recordingMode).toBe('session');
expect(replay.session?.sampled).toBe('buffer');
- expect(replay.session?.shouldRefresh).toBe(false);
});
it('has the correct timestamps with deferred root event and last replay update', async () => {
@@ -637,51 +561,15 @@ describe('Integration | errorSampleRate', () => {
});
});
- it('stops replay when user goes idle', async () => {
+ it('does not pause/expire session while buffering', async () => {
jest.setSystemTime(BASE_TIMESTAMP);
- const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
- mockRecord._emitter(TEST_EVENT);
-
- expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
- expect(replay).not.toHaveLastSentReplay();
-
- jest.runAllTimers();
+ // Wait for session to expire
+ jest.advanceTimersByTime(MAX_SESSION_LIFE + 1);
await new Promise(process.nextTick);
- captureException(new Error('testing'));
-
- await waitForBufferFlush();
-
- expect(replay).toHaveLastSentReplay();
-
- // Flush from calling `stopRecording`
- await waitForFlush();
-
- // Now wait after session expires - should stop recording
- mockRecord.takeFullSnapshot.mockClear();
- (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear();
-
- expect(replay).not.toHaveLastSentReplay();
-
- // Go idle
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
- await new Promise(process.nextTick);
-
- mockRecord._emitter(TEST_EVENT);
-
- expect(replay).not.toHaveLastSentReplay();
-
- await waitForFlush();
-
- expect(replay).not.toHaveLastSentReplay();
- expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0);
- expect(replay.isEnabled()).toBe(false);
- });
-
- it('stops replay when session exceeds max length after latest captured error', async () => {
- const sessionId = replay.session?.id;
- jest.setSystemTime(BASE_TIMESTAMP);
+ expect(replay.isPaused()).toBe(false);
+ expect(replay.isEnabled()).toBe(true);
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
mockRecord._emitter(TEST_EVENT);
@@ -692,124 +580,11 @@ describe('Integration | errorSampleRate', () => {
jest.runAllTimers();
await new Promise(process.nextTick);
- jest.advanceTimersByTime(2 * MAX_SESSION_LIFE);
-
- captureException(new Error('testing'));
-
- // Flush due to exception
- await new Promise(process.nextTick);
- await waitForFlush();
-
- expect(replay.session?.id).toBe(sessionId);
- expect(replay).toHaveLastSentReplay({
- recordingPayloadHeader: { segment_id: 0 },
- });
-
- // This comes from `startRecording()` in `sendBufferedReplayOrFlush()`
- await waitForFlush();
- expect(replay).toHaveLastSentReplay({
- recordingPayloadHeader: { segment_id: 1 },
- recordingData: JSON.stringify([
- {
- data: {
- isCheckout: true,
- },
- timestamp: BASE_TIMESTAMP + 2 * MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 40,
- type: 2,
- },
- ]),
- });
-
- // Now wait after session expires - should stop recording
- mockRecord.takeFullSnapshot.mockClear();
- (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear();
-
- jest.advanceTimersByTime(MAX_SESSION_LIFE);
- await new Promise(process.nextTick);
-
- mockRecord._emitter(TEST_EVENT);
- jest.runAllTimers();
- await new Promise(process.nextTick);
-
- expect(replay).not.toHaveLastSentReplay();
- expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0);
- expect(replay.isEnabled()).toBe(false);
-
- // Once the session is stopped after capturing a replay already
- // (buffer-mode), another error will not trigger a new replay
- captureException(new Error('testing'));
-
- await new Promise(process.nextTick);
- jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
- await new Promise(process.nextTick);
- expect(replay).not.toHaveLastSentReplay();
- });
-
- it('does not stop replay based on earliest event in buffer', async () => {
- jest.setSystemTime(BASE_TIMESTAMP);
-
- const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP - 60000, type: 3 };
- mockRecord._emitter(TEST_EVENT);
-
- expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
- expect(replay).not.toHaveLastSentReplay();
-
- jest.runAllTimers();
- await new Promise(process.nextTick);
-
- expect(replay).not.toHaveLastSentReplay();
captureException(new Error('testing'));
await waitForBufferFlush();
expect(replay).toHaveLastSentReplay();
-
- // Flush from calling `stopRecording`
- await waitForFlush();
-
- // Now wait after session expires - should stop recording
- mockRecord.takeFullSnapshot.mockClear();
- (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear();
-
- expect(replay).not.toHaveLastSentReplay();
-
- const TICKS = 80;
-
- // We advance time so that we are on the border of expiring, taking into
- // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The
- // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has
- // happened, and for the next two that will happen. The first following
- // `waitForFlush` does not expire session, but the following one will.
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS);
- await new Promise(process.nextTick);
-
- mockRecord._emitter(TEST_EVENT);
- expect(replay).not.toHaveLastSentReplay();
- await waitForFlush();
-
- expect(replay).not.toHaveLastSentReplay();
- expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0);
- expect(replay.isEnabled()).toBe(true);
-
- mockRecord._emitter(TEST_EVENT);
- expect(replay).not.toHaveLastSentReplay();
- await waitForFlush();
-
- expect(replay).not.toHaveLastSentReplay();
- expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0);
- expect(replay.isEnabled()).toBe(true);
-
- // It's hard to test, but if we advance the below time less 1 ms, it should
- // be enabled, but we can't trigger a session check via flush without
- // incurring another DEFAULT_FLUSH_MIN_DELAY timeout.
- jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY);
- mockRecord._emitter(TEST_EVENT);
- expect(replay).not.toHaveLastSentReplay();
- await waitForFlush();
-
- expect(replay).not.toHaveLastSentReplay();
- expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0);
- expect(replay.isEnabled()).toBe(false);
});
});
@@ -820,7 +595,7 @@ describe('Integration | errorSampleRate', () => {
* sampling since we can load a saved session that did not have an error (and
* thus no replay was created).
*/
-it('sends a replay after loading the session from storage', async () => {
+it('Integration | errorSampleRate | sends a replay after loading the session from storage', async () => {
// Pretend that a session is already saved before loading replay
WINDOW.sessionStorage.setItem(
REPLAY_SESSION_KEY,
@@ -831,6 +606,7 @@ it('sends a replay after loading the session from storage', async () => {
stickySession: true,
},
sentryOptions: {
+ replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1.0,
},
autoStart: false,
diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts
index b95faffa59da..4d105df9a2bc 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['_initializeSession']('session');
+ replay.setInitialState();
mockTransportSend.mockClear();
});
@@ -93,7 +94,7 @@ describe('Integration | events', () => {
it('has correct timestamps when there are events earlier than initial timestamp', async function () {
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSession']('session');
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 cf142ae8c45c..959a1931e4b9 100644
--- a/packages/replay/test/integration/flush.test.ts
+++ b/packages/replay/test/integration/flush.test.ts
@@ -95,7 +95,7 @@ describe('Integration | flush', () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSession']('session');
mockRecord.takeFullSnapshot.mockClear();
Object.defineProperty(WINDOW, 'location', {
value: prevLocation,
diff --git a/packages/replay/test/integration/rateLimiting.test.ts b/packages/replay/test/integration/rateLimiting.test.ts
index 723dc682d100..705ae0e86729 100644
--- a/packages/replay/test/integration/rateLimiting.test.ts
+++ b/packages/replay/test/integration/rateLimiting.test.ts
@@ -46,7 +46,7 @@ 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['_initializeSession']('session');
mockSendReplayRequest.mockClear();
});
@@ -57,7 +57,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..8c164cf146c2 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['_initializeSession']('session');
+ 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..4c65e36b2371 100644
--- a/packages/replay/test/integration/session.test.ts
+++ b/packages/replay/test/integration/session.test.ts
@@ -5,7 +5,6 @@ import {
DEFAULT_FLUSH_MIN_DELAY,
MAX_SESSION_LIFE,
REPLAY_SESSION_KEY,
- SESSION_IDLE_EXPIRE_DURATION,
SESSION_IDLE_PAUSE_DURATION,
WINDOW,
} from '../../src/constants';
@@ -13,7 +12,6 @@ import type { ReplayContainer } from '../../src/replay';
import { clearSession } from '../../src/session/clearSession';
import type { Session } from '../../src/types';
import { addEvent } from '../../src/util/addEvent';
-import { createPerformanceSpans } from '../../src/util/createPerformanceSpans';
import { createOptionsEvent } from '../../src/util/handleRecordingEmit';
import { BASE_TIMESTAMP } from '../index';
import type { RecordMock } from '../mocks/mockRrweb';
@@ -60,7 +58,7 @@ describe('Integration | session', () => {
// Require a "user interaction" to start a new session, visibility is not enough. This can be noisy
// (e.g. rapidly switching tabs/window focus) and leads to empty sessions.
- it('does not create a new session when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', () => {
+ it('does not create a new session when document becomes visible after [MAX_SESSION_LIFE]ms', () => {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: function () {
@@ -70,7 +68,7 @@ describe('Integration | session', () => {
const initialSession = { ...replay.session } as Session;
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
+ jest.advanceTimersByTime(MAX_SESSION_LIFE + 1);
document.dispatchEvent(new Event('visibilitychange'));
@@ -78,167 +76,7 @@ describe('Integration | session', () => {
expect(replay).toHaveSameSession(initialSession);
});
- it('does not create a new session when document becomes focused after [SESSION_IDLE_EXPIRE_DURATION]ms', () => {
- const initialSession = { ...replay.session } as Session;
-
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
-
- WINDOW.dispatchEvent(new Event('focus'));
-
- expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
- expect(replay).toHaveSameSession(initialSession);
- });
-
- it('does not create a new session if user hides the tab and comes back within [SESSION_IDLE_EXPIRE_DURATION] seconds', () => {
- const initialSession = { ...replay.session } as Session;
-
- Object.defineProperty(document, 'visibilityState', {
- configurable: true,
- get: function () {
- return 'hidden';
- },
- });
- document.dispatchEvent(new Event('visibilitychange'));
- expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
- expect(replay).toHaveSameSession(initialSession);
-
- // User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses
- jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 1);
- Object.defineProperty(document, 'visibilityState', {
- configurable: true,
- get: function () {
- return 'visible';
- },
- });
- document.dispatchEvent(new Event('visibilitychange'));
-
- expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
- // Should NOT have created a new session
- expect(replay).toHaveSameSession(initialSession);
- });
-
- it('creates a new session if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and comes back to click their mouse', async () => {
- const initialSession = { ...replay.session } as Session;
-
- expect(mockRecord).toHaveBeenCalledTimes(1);
- expect(initialSession?.id).toBeDefined();
- expect(replay.getContext()).toEqual(
- expect.objectContaining({
- initialUrl: 'http://localhost/',
- initialTimestamp: BASE_TIMESTAMP,
- }),
- );
-
- const url = 'http://dummy/';
- Object.defineProperty(WINDOW, 'location', {
- value: new URL(url),
- });
-
- const ELAPSED = SESSION_IDLE_EXPIRE_DURATION + 1;
- jest.advanceTimersByTime(ELAPSED);
-
- // Session has become in an idle state
- //
- // This event will put the Replay SDK into a paused state
- const TEST_EVENT = {
- data: { name: 'lost event' },
- timestamp: BASE_TIMESTAMP,
- type: 3,
- };
- mockRecord._emitter(TEST_EVENT);
-
- // performance events can still be collected while recording is stopped
- // TODO: we may want to prevent `addEvent` from adding to buffer when user is inactive
- replay.addUpdate(() => {
- createPerformanceSpans(replay, [
- {
- type: 'navigation.navigate' as const,
- name: 'foo',
- start: BASE_TIMESTAMP + ELAPSED,
- end: BASE_TIMESTAMP + ELAPSED + 100,
- data: {
- decodedBodySize: 1,
- encodedBodySize: 2,
- duration: 0,
- domInteractive: 0,
- domContentLoadedEventEnd: 0,
- domContentLoadedEventStart: 0,
- loadEventStart: 0,
- loadEventEnd: 0,
- domComplete: 0,
- redirectCount: 0,
- size: 0,
- },
- },
- ]);
- return true;
- });
-
- await new Promise(process.nextTick);
-
- expect(replay).not.toHaveLastSentReplay();
- expect(replay.isPaused()).toBe(true);
- expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
- expect(replay).toHaveSameSession(initialSession);
- expect(mockRecord).toHaveBeenCalledTimes(1);
-
- // Now do a click which will create a new session and start recording again
- domHandler({
- name: 'click',
- });
-
- // This is not called because we have to start recording
- expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
- expect(mockRecord).toHaveBeenCalledTimes(2);
-
- // Should be a new session
- expect(replay).not.toHaveSameSession(initialSession);
-
- // Replay does not send immediately because checkout was due to expired session
- expect(replay).not.toHaveLastSentReplay();
-
- const optionsEvent = createOptionsEvent(replay);
-
- await advanceTimers(DEFAULT_FLUSH_MIN_DELAY);
-
- const newTimestamp = BASE_TIMESTAMP + ELAPSED + 20;
-
- expect(replay).toHaveLastSentReplay({
- recordingPayloadHeader: { segment_id: 0 },
- recordingData: JSON.stringify([
- { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 },
- optionsEvent,
- {
- type: 5,
- timestamp: newTimestamp,
- data: {
- tag: 'breadcrumb',
- payload: {
- timestamp: newTimestamp / 1000,
- type: 'default',
- category: 'ui.click',
- message: '',
- data: {},
- },
- },
- },
- ]),
- });
-
- // Earliest event is reset
- expect(replay.eventBuffer?.getEarliestTimestamp()).toBeNull();
-
- // `_context` should be reset when a new session is created
- expect(replay.getContext()).toEqual({
- initialUrl: 'http://dummy/',
- initialTimestamp: newTimestamp,
- urls: [],
- errorIds: new Set(),
- traceIds: new Set(),
- });
- });
-
- it('pauses and resumes a session if user has been idle for more than SESSION_IDLE_PASUE_DURATION and comes back to click their mouse', async () => {
+ it('pauses and resumes a session if user has been idle for more than SESSION_IDLE_PAUSE_DURATION and comes back to click their mouse', async () => {
const initialSession = { ...replay.session } as Session;
expect(initialSession?.id).toBeDefined();
@@ -267,33 +105,6 @@ describe('Integration | session', () => {
};
mockRecord._emitter(TEST_EVENT);
- // performance events can still be collected while recording is stopped
- // TODO: we may want to prevent `addEvent` from adding to buffer when user is inactive
- replay.addUpdate(() => {
- createPerformanceSpans(replay, [
- {
- type: 'navigation.navigate' as const,
- name: 'foo',
- start: BASE_TIMESTAMP + ELAPSED,
- end: BASE_TIMESTAMP + ELAPSED + 100,
- data: {
- decodedBodySize: 1,
- encodedBodySize: 2,
- duration: 0,
- domInteractive: 0,
- domContentLoadedEventEnd: 0,
- domContentLoadedEventStart: 0,
- loadEventStart: 0,
- loadEventEnd: 0,
- domComplete: 0,
- redirectCount: 0,
- size: 0,
- },
- },
- ]);
- return true;
- });
-
await new Promise(process.nextTick);
expect(replay).not.toHaveLastSentReplay();
@@ -355,7 +166,6 @@ describe('Integration | session', () => {
const ELAPSED = MAX_SESSION_LIFE + 1;
jest.advanceTimersByTime(ELAPSED);
// Update activity so as to not consider session to be idling
- replay['_updateUserActivity']();
replay['_updateSessionActivity']();
// This should trigger a new session
@@ -366,42 +176,53 @@ describe('Integration | session', () => {
};
mockRecord._emitter(TEST_EVENT);
+ await new Promise(process.nextTick);
+ await waitForFlush();
+
+ const newTimestamp = BASE_TIMESTAMP + ELAPSED;
+ const optionsEvent = createOptionsEvent(replay);
+ optionsEvent.timestamp = newTimestamp;
+
expect(replay).not.toHaveSameSession(initialSession);
- expect(mockRecord.takeFullSnapshot).toHaveBeenCalled();
- expect(replay).not.toHaveLastSentReplay();
- // @ts-ignore private
- expect(replay._stopRecording).toBeDefined();
+ expect(replay).toHaveLastSentReplay({
+ recordingPayloadHeader: { segment_id: 0 },
+ recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, optionsEvent]),
+ });
+ expect(replay.isEnabled()).toBe(true);
+ // `_context` should be reset when a new session is created
+ expect(replay.getContext()).toEqual(
+ expect.objectContaining({
+ initialUrl: 'http://dummy/',
+ initialTimestamp: newTimestamp,
+ }),
+ );
// Now do a click
domHandler({
name: 'click',
});
- const newTimestamp = BASE_TIMESTAMP + ELAPSED;
+ const postFlushTimestamp = newTimestamp + DEFAULT_FLUSH_MIN_DELAY + 40;
const NEW_TEST_EVENT = {
data: { name: 'test' },
- timestamp: newTimestamp + DEFAULT_FLUSH_MIN_DELAY + 20,
+ timestamp: postFlushTimestamp,
type: 3,
};
mockRecord._emitter(NEW_TEST_EVENT);
- const optionsEvent = createOptionsEvent(replay);
- jest.runAllTimers();
- await advanceTimers(DEFAULT_FLUSH_MIN_DELAY);
+ await waitForFlush();
expect(replay).toHaveLastSentReplay({
- recordingPayloadHeader: { segment_id: 0 },
+ recordingPayloadHeader: { segment_id: 1 },
recordingData: JSON.stringify([
- { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + ELAPSED, type: 2 },
- optionsEvent,
{
type: 5,
- timestamp: newTimestamp,
+ timestamp: postFlushTimestamp,
data: {
tag: 'breadcrumb',
payload: {
- timestamp: newTimestamp / 1000,
+ timestamp: postFlushTimestamp / 1000,
type: 'default',
category: 'ui.click',
message: '',
@@ -412,19 +233,11 @@ describe('Integration | session', () => {
NEW_TEST_EVENT,
]),
});
-
- // `_context` should be reset when a new session is created
- expect(replay.getContext()).toEqual(
- expect.objectContaining({
- initialUrl: 'http://dummy/',
- initialTimestamp: newTimestamp,
- }),
- );
});
it('increases segment id after each event', async () => {
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSession']('session');
Object.defineProperty(document, 'visibilityState', {
configurable: true,
@@ -457,3 +270,7 @@ describe('Integration | session', () => {
});
});
});
+
+async function waitForFlush() {
+ await advanceTimers(DEFAULT_FLUSH_MIN_DELAY);
+}
diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts
index cc0e28195244..31b4a4b9bce0 100644
--- a/packages/replay/test/integration/stop.test.ts
+++ b/packages/replay/test/integration/stop.test.ts
@@ -44,6 +44,7 @@ describe('Integration | stop', () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
replay.eventBuffer?.destroy();
jest.clearAllMocks();
+ replay['_initializeSession']('session');
});
afterEach(async () => {
@@ -52,7 +53,6 @@ describe('Integration | stop', () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
mockRecord.takeFullSnapshot.mockClear();
mockAddInstrumentationHandler.mockClear();
Object.defineProperty(WINDOW, 'location', {
diff --git a/packages/replay/test/unit/session/checkSessionState.test.ts b/packages/replay/test/unit/session/checkSessionState.test.ts
new file mode 100644
index 000000000000..f4ac41c67f99
--- /dev/null
+++ b/packages/replay/test/unit/session/checkSessionState.test.ts
@@ -0,0 +1,148 @@
+import { checkSessionState } from '../../../src/session/checkSessionState';
+import { makeSession } from '../../../src/session/Session';
+import type { Session, Timeouts } from '../../../src/types';
+
+describe('Unit | session | checkSessionState', () => {
+ const timeouts: Timeouts = {
+ sessionIdlePause: 10_000,
+ maxSessionLife: 10_000,
+ };
+
+ it('works for a regular session', () => {
+ const onPause = jest.fn();
+ const ensureResumed = jest.fn();
+ const onEnd = jest.fn();
+ const onContinue = jest.fn();
+
+ const session: Session = makeSession({ sampled: 'session' });
+
+ checkSessionState(session, 'session', timeouts, {
+ onPause,
+ ensureResumed,
+ onEnd,
+ onContinue,
+ });
+
+ expect(onPause).not.toHaveBeenCalled();
+ expect(ensureResumed).toHaveBeenCalledTimes(1);
+ expect(onEnd).not.toHaveBeenCalled();
+ expect(onContinue).toHaveBeenCalledTimes(1);
+ });
+
+ it('pauses an idle session', () => {
+ const onPause = jest.fn();
+ const ensureResumed = jest.fn();
+ const onEnd = jest.fn();
+ const onContinue = jest.fn();
+
+ const session: Session = makeSession({ sampled: 'session', lastActivity: Date.now() - 20_000 });
+
+ const timeouts: Timeouts = {
+ sessionIdlePause: 10_000,
+ maxSessionLife: 100_000,
+ };
+
+ checkSessionState(session, 'session', timeouts, {
+ onPause,
+ ensureResumed,
+ onEnd,
+ onContinue,
+ });
+
+ expect(onPause).toHaveBeenCalledTimes(1);
+ expect(ensureResumed).not.toHaveBeenCalled();
+ expect(onEnd).not.toHaveBeenCalled();
+ expect(onContinue).not.toHaveBeenCalled();
+ });
+
+ it('does not pause an idle buffer session', () => {
+ const onPause = jest.fn();
+ const ensureResumed = jest.fn();
+ const onEnd = jest.fn();
+ const onContinue = jest.fn();
+
+ const session: Session = makeSession({ sampled: 'buffer', lastActivity: Date.now() - 20_000 });
+
+ const timeouts: Timeouts = {
+ sessionIdlePause: 10_000,
+ maxSessionLife: 100_000,
+ };
+
+ checkSessionState(session, 'buffer', timeouts, {
+ onPause,
+ ensureResumed,
+ onEnd,
+ onContinue,
+ });
+
+ expect(onPause).not.toHaveBeenCalled();
+ expect(ensureResumed).not.toHaveBeenCalled();
+ expect(onEnd).not.toHaveBeenCalled();
+ expect(onContinue).toHaveBeenCalled();
+ });
+
+ it('ends a too long session', () => {
+ const onPause = jest.fn();
+ const ensureResumed = jest.fn();
+ const onEnd = jest.fn();
+ const onContinue = jest.fn();
+
+ const session: Session = makeSession({ sampled: 'session', started: Date.now() - 20_000 });
+
+ checkSessionState(session, 'session', timeouts, {
+ onPause,
+ ensureResumed,
+ onEnd,
+ onContinue,
+ });
+
+ expect(onPause).not.toHaveBeenCalled();
+ expect(ensureResumed).not.toHaveBeenCalled();
+ expect(onEnd).toHaveBeenCalledTimes(1);
+ expect(onContinue).not.toHaveBeenCalled();
+ });
+
+ it('does not end a too long buffer session', () => {
+ const onPause = jest.fn();
+ const ensureResumed = jest.fn();
+ const onEnd = jest.fn();
+ const onContinue = jest.fn();
+
+ const session: Session = makeSession({ sampled: 'buffer', started: Date.now() - 20_000 });
+
+ checkSessionState(session, 'buffer', timeouts, {
+ onPause,
+ ensureResumed,
+ onEnd,
+ onContinue,
+ });
+
+ expect(onPause).not.toHaveBeenCalled();
+ expect(ensureResumed).not.toHaveBeenCalled();
+ expect(onEnd).not.toHaveBeenCalled();
+ expect(onContinue).toHaveBeenCalled();
+ });
+
+ it('uses recordingMode over session.sampled', () => {
+ const onPause = jest.fn();
+ const ensureResumed = jest.fn();
+ const onEnd = jest.fn();
+ const onContinue = jest.fn();
+
+ const session: Session = makeSession({ sampled: 'buffer', started: Date.now() - 20_000 });
+
+ // A session with sampled=buffer can be in `session` mode after an error has been captured
+ // In this case, we need to use the recordingMode `session` for decisions, not the session.sampled
+ checkSessionState(session, 'session', timeouts, {
+ onPause,
+ ensureResumed,
+ onEnd,
+ onContinue,
+ });
+
+ expect(onPause).not.toHaveBeenCalled();
+ expect(ensureResumed).not.toHaveBeenCalled();
+ expect(onEnd).toHaveBeenCalledTimes(1);
+ expect(onContinue).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/replay/test/unit/session/createSession.test.ts b/packages/replay/test/unit/session/createSession.test.ts
index 891cc012edb6..03c99d3a82d0 100644
--- a/packages/replay/test/unit/session/createSession.test.ts
+++ b/packages/replay/test/unit/session/createSession.test.ts
@@ -33,8 +33,7 @@ describe('Unit | session | createSession', () => {
it('creates a new session with no sticky sessions', function () {
const newSession = createSession({
stickySession: false,
- sessionSampleRate: 1.0,
- allowBuffering: false,
+ sampled: 'session',
});
expect(captureEventMock).not.toHaveBeenCalled();
@@ -48,8 +47,7 @@ describe('Unit | session | createSession', () => {
it('creates a new session with sticky sessions', function () {
const newSession = createSession({
stickySession: true,
- sessionSampleRate: 1.0,
- allowBuffering: false,
+ sampled: 'session',
});
expect(captureEventMock).not.toHaveBeenCalled();
diff --git a/packages/replay/test/unit/session/fetchSession.test.ts b/packages/replay/test/unit/session/fetchSession.test.ts
index cf1856e53356..526c9c7969d1 100644
--- a/packages/replay/test/unit/session/fetchSession.test.ts
+++ b/packages/replay/test/unit/session/fetchSession.test.ts
@@ -28,7 +28,6 @@ describe('Unit | session | fetchSession', () => {
segmentId: 0,
sampled: 'session',
started: 1648827162630,
- shouldRefresh: true,
});
});
@@ -44,7 +43,6 @@ describe('Unit | session | fetchSession', () => {
segmentId: 0,
sampled: false,
started: 1648827162630,
- shouldRefresh: true,
});
});
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/util/isSessionExpired.test.ts b/packages/replay/test/unit/util/isSessionExpired.test.ts
deleted file mode 100644
index 38b24056d36f..000000000000
--- a/packages/replay/test/unit/util/isSessionExpired.test.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { MAX_SESSION_LIFE, SESSION_IDLE_PAUSE_DURATION } from '../../../src/constants';
-import { makeSession } from '../../../src/session/Session';
-import { isSessionExpired } from '../../../src/util/isSessionExpired';
-
-function createSession(extra?: Record) {
- return makeSession({
- // Setting started/lastActivity to 0 makes it use the default, which is `Date.now()`
- started: 1,
- lastActivity: 1,
- segmentId: 0,
- sampled: 'session',
- ...extra,
- });
-}
-
-describe('Unit | util | isSessionExpired', () => {
- it('session last activity is older than expiry time', function () {
- expect(
- isSessionExpired(
- createSession(),
- {
- maxSessionLife: MAX_SESSION_LIFE,
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 100,
- },
- 200,
- ),
- ).toBe(true); // Session expired at ts = 100
- });
-
- it('session last activity is not older than expiry time', function () {
- expect(
- isSessionExpired(
- createSession({ lastActivity: 100 }),
- {
- maxSessionLife: MAX_SESSION_LIFE,
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 150,
- },
- 200,
- ),
- ).toBe(false); // Session expires at ts >= 250
- });
-
- it('session age is not older than max session life', function () {
- expect(
- isSessionExpired(
- createSession(),
- {
- maxSessionLife: MAX_SESSION_LIFE,
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1_800_000,
- },
- 50_000,
- ),
- ).toBe(false);
- });
-
- it('session age is older than max session life', function () {
- expect(
- isSessionExpired(
- createSession(),
- {
- maxSessionLife: MAX_SESSION_LIFE,
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1_800_000,
- },
- 1_800_001,
- ),
- ).toBe(true); // Session expires at ts >= 1_800_000
- });
-});
diff --git a/packages/replay/test/unit/util/sampleSession.test.ts b/packages/replay/test/unit/util/sampleSession.test.ts
new file mode 100644
index 000000000000..321632fac0d0
--- /dev/null
+++ b/packages/replay/test/unit/util/sampleSession.test.ts
@@ -0,0 +1,28 @@
+import type { Sampled } from '../../../src/types';
+import { sampleSession } from '../../../src/util/sampleSession';
+
+// Note: We "fix" Math.random() to always return 0.2
+const cases: [number, number, Sampled][] = [
+ [0, 0, false],
+ [-1, -1, false],
+ [1, 0, 'session'],
+ [0, 1, 'buffer'],
+ [0.1, 0.1, 'buffer'],
+ [0.1, 0, false],
+ [0.3, 0.1, 'session'],
+ [0.3, 0, 'session'],
+];
+
+describe('Unit | util | sampleSession', () => {
+ const mockRandom = jest.spyOn(Math, 'random');
+
+ test.each(cases)(
+ 'given sessionSampleRate=%p and errorSampleRate=%p, result should be %p',
+ (sessionSampleRate: number, errorSampleRate: number, expectedResult: Sampled) => {
+ mockRandom.mockImplementationOnce(() => 0.2);
+
+ const result = sampleSession({ sessionSampleRate, errorSampleRate });
+ expect(result).toEqual(expectedResult);
+ },
+ );
+});
diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts
index e2a49052a799..dbef83f4a8d0 100644
--- a/packages/replay/test/utils/setupReplayContainer.ts
+++ b/packages/replay/test/utils/setupReplayContainer.ts
@@ -41,9 +41,9 @@ export function setupReplayContainer({
});
clearSession(replay);
- replay.setInitialState();
- replay['_loadAndCheckSession']();
+ replay['_initializeSession']('session');
replay['_isEnabled'] = true;
+ replay.setInitialState();
replay.eventBuffer = createEventBuffer({
useCompression: options?.useCompression || false,
});