diff --git a/packages/integration-tests/suites/replay/captureReplay/template.html b/packages/integration-tests/suites/replay/captureReplay/template.html
new file mode 100644
index 000000000000..2b3e2f0b27b4
--- /dev/null
+++ b/packages/integration-tests/suites/replay/captureReplay/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/integration-tests/suites/replay/captureReplay/test.ts b/packages/integration-tests/suites/replay/captureReplay/test.ts
new file mode 100644
index 000000000000..80a3f52201dd
--- /dev/null
+++ b/packages/integration-tests/suites/replay/captureReplay/test.ts
@@ -0,0 +1,67 @@
+import { expect } from '@playwright/test';
+import { SDK_VERSION } from '@sentry/browser';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
+
+sentryTest('captureReplay', async ({ getLocalTestPath, page }) => {
+ // Currently bundle tests are not supported for replay
+ if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) {
+ sentryTest.skip();
+ }
+
+ 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);
+
+ await page.click('button');
+ await page.waitForTimeout(200);
+
+ const replayEvent = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(replayEvent).toBeDefined();
+ expect(replayEvent).toEqual({
+ type: 'replay_event',
+ timestamp: expect.any(Number),
+ error_ids: [],
+ trace_ids: [],
+ urls: [expect.stringContaining('/dist/index.html')],
+ replay_id: expect.stringMatching(/\w{32}/),
+ segment_id: 2,
+ replay_type: 'session',
+ event_id: expect.stringMatching(/\w{32}/),
+ environment: 'production',
+ sdk: {
+ integrations: [
+ 'InboundFilters',
+ 'FunctionToString',
+ 'TryCatch',
+ 'Breadcrumbs',
+ 'GlobalHandlers',
+ 'LinkedErrors',
+ 'Dedupe',
+ 'HttpContext',
+ 'Replay',
+ ],
+ version: SDK_VERSION,
+ name: 'sentry.javascript.browser',
+ },
+ sdkProcessingMetadata: {},
+ request: {
+ url: expect.stringContaining('/dist/index.html'),
+ headers: {
+ 'User-Agent': expect.stringContaining(''),
+ },
+ },
+ platform: 'javascript',
+ tags: { sessionSampleRate: 1, errorSampleRate: 0 },
+ });
+});
diff --git a/packages/integration-tests/suites/replay/init.js b/packages/integration-tests/suites/replay/init.js
new file mode 100644
index 000000000000..9050f274417c
--- /dev/null
+++ b/packages/integration-tests/suites/replay/init.js
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ initialFlushDelay: 200,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 0,
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+
+ integrations: [window.Replay],
+});
diff --git a/packages/integration-tests/suites/replay/sampling/init.js b/packages/integration-tests/suites/replay/sampling/init.js
new file mode 100644
index 000000000000..67b681515697
--- /dev/null
+++ b/packages/integration-tests/suites/replay/sampling/init.js
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ initialFlushDelay: 200,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 0,
+ replaysSessionSampleRate: 0.0,
+ replaysOnErrorSampleRate: 0.0,
+
+ integrations: [window.Replay],
+});
diff --git a/packages/integration-tests/suites/replay/sampling/template.html b/packages/integration-tests/suites/replay/sampling/template.html
new file mode 100644
index 000000000000..2b3e2f0b27b4
--- /dev/null
+++ b/packages/integration-tests/suites/replay/sampling/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/integration-tests/suites/replay/sampling/test.ts b/packages/integration-tests/suites/replay/sampling/test.ts
new file mode 100644
index 000000000000..9a351ce30f6f
--- /dev/null
+++ b/packages/integration-tests/suites/replay/sampling/test.ts
@@ -0,0 +1,34 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../utils/fixtures';
+import { getReplaySnapshot } from '../../../utils/helpers';
+
+sentryTest('sampling', async ({ getLocalTestPath, page }) => {
+ // Currently bundle tests are not supported for replay
+ if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ // This should never be called!
+ expect(true).toBe(false);
+
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(url);
+
+ await page.click('button');
+ await page.waitForTimeout(200);
+
+ const replay = await getReplaySnapshot(page);
+
+ expect(replay.session?.sampled).toBe(false);
+
+ // Cannot wait on getFirstSentryEnvelopeRequest, as that never resolves
+});
diff --git a/packages/integration-tests/utils/helpers.ts b/packages/integration-tests/utils/helpers.ts
index 12078c09039e..54e1cd44a0bb 100644
--- a/packages/integration-tests/utils/helpers.ts
+++ b/packages/integration-tests/utils/helpers.ts
@@ -1,4 +1,5 @@
import type { Page, Request } from '@playwright/test';
+import type { ReplayContainer } from '@sentry/replay/build/npm/types/types';
import type { Event, EventEnvelopeHeaders } from '@sentry/types';
const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
@@ -8,7 +9,13 @@ const envelopeRequestParser = (request: Request | null): Event => {
const envelope = request?.postData() || '';
// Third row of the envelop is the event payload.
- return envelope.split('\n').map(line => JSON.parse(line))[2];
+ return envelope.split('\n').map(line => {
+ try {
+ return JSON.parse(line);
+ } catch (error) {
+ return line;
+ }
+ })[2];
};
export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {
@@ -46,24 +53,34 @@ async function getSentryEvents(page: Page, url?: string): Promise>
return eventsHandle.jsonValue();
}
+/**
+ * This returns the replay container (assuming it exists).
+ * Note that due to how this works with playwright, this is a POJO copy of replay.
+ * This means that we cannot access any methods on it, and also not mutate it in any way.
+ */
+export async function getReplaySnapshot(page: Page): Promise {
+ const replayIntegration = await page.evaluate<{ _replay: ReplayContainer }>('window.Replay');
+ return replayIntegration._replay;
+}
+
/**
* Waits until a number of requests matching urlRgx at the given URL arrive.
* If the timout option is configured, this function will abort waiting, even if it hasn't reveived the configured
* amount of requests, and returns all the events recieved up to that point in time.
*/
-async function getMultipleRequests(
+async function getMultipleRequests(
page: Page,
count: number,
urlRgx: RegExp,
- requestParser: (req: Request) => Event,
+ requestParser: (req: Request) => T,
options?: {
url?: string;
timeout?: number;
},
-): Promise {
- const requests: Promise = new Promise((resolve, reject) => {
+): Promise {
+ const requests: Promise = new Promise((resolve, reject) => {
let reqCount = count;
- const requestData: Event[] = [];
+ const requestData: T[] = [];
let timeoutId: NodeJS.Timeout | undefined = undefined;
function requestHandler(request: Request): void {
@@ -115,7 +132,7 @@ async function getMultipleSentryEnvelopeRequests(
): Promise {
// TODO: This is not currently checking the type of envelope, just casting for now.
// We can update this to include optional type-guarding when we have types for Envelope.
- return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise;
+ return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise;
}
/**