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; } /**