diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/init.js rename to dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts new file mode 100644 index 000000000000..6868caf99545 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts @@ -0,0 +1,107 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, getEnvelopeType } from '../../../../utils/helpers'; +import { getCustomRecordingEvents, getReplayEvent, waitForReplayRequest } from '../../../../utils/replayHelpers'; + +sentryTest( + 'should capture feedback (@sentry-internal/feedback import)', + async ({ forceFlushReplay, getLocalTestPath, page }) => { + if (process.env.PW_BUNDLE) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + const feedbackRequestPromise = page.waitForResponse(res => { + const req = res.request(); + + const postData = req.postData(); + if (!postData) { + return false; + } + + try { + return getEnvelopeType(req) === 'feedback'; + } catch (err) { + return false; + } + }); + + 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 }); + + const [, , replayReq0] = await Promise.all([page.goto(url), page.getByText('Report a Bug').click(), reqPromise0]); + + // Inputs are slow, these need to be serial + await page.locator('[name="name"]').fill('Jane Doe'); + await page.locator('[name="email"]').fill('janedoe@example.org'); + await page.locator('[name="message"]').fill('my example feedback'); + + // Force flush here, as inputs are slow and can cause click event to be in unpredictable segments + await Promise.all([forceFlushReplay(), reqPromise1]); + + const [, feedbackResp, replayReq2] = await Promise.all([ + page.getByLabel('Send Bug Report').click(), + feedbackRequestPromise, + reqPromise2, + ]); + + const feedbackEvent = envelopeRequestParser(feedbackResp.request()); + const replayEvent = getReplayEvent(replayReq0); + // Feedback breadcrumb is on second segment because we flush when "Report a Bug" is clicked + // And then the breadcrumb is sent when feedback form is submitted + const { breadcrumbs } = getCustomRecordingEvents(replayReq2); + + expect(breadcrumbs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + category: 'sentry.feedback', + data: { feedbackId: expect.any(String) }, + timestamp: expect.any(Number), + type: 'default', + }), + ]), + ); + + expect(feedbackEvent).toEqual({ + type: 'feedback', + breadcrumbs: expect.any(Array), + contexts: { + feedback: { + contact_email: 'janedoe@example.org', + message: 'my example feedback', + name: 'Jane Doe', + replay_id: replayEvent.event_id, + source: 'widget', + url: expect.stringContaining('/dist/index.html'), + }, + }, + level: 'info', + timestamp: expect.any(Number), + event_id: expect.stringMatching(/\w{32}/), + environment: 'production', + sdk: { + integrations: expect.arrayContaining(['Feedback']), + version: expect.any(String), + name: 'sentry.javascript.browser', + packages: expect.anything(), + }, + request: { + url: expect.stringContaining('/dist/index.html'), + headers: { + 'User-Agent': expect.stringContaining(''), + }, + }, + platform: 'javascript', + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/test.ts deleted file mode 100644 index 057b5d43a1c8..000000000000 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { expect } from '@playwright/test'; - -import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, getEnvelopeType } from '../../../utils/helpers'; -import { getCustomRecordingEvents, getReplayEvent, waitForReplayRequest } from '../../../utils/replayHelpers'; - -sentryTest('should capture feedback (@sentry-internal/feedback import)', async ({ getLocalTestPath, page }) => { - if (process.env.PW_BUNDLE) { - sentryTest.skip(); - } - - const reqPromise0 = waitForReplayRequest(page, 0); - const feedbackRequestPromise = page.waitForResponse(res => { - const req = res.request(); - - const postData = req.postData(); - if (!postData) { - return false; - } - - try { - return getEnvelopeType(req) === 'feedback'; - } catch (err) { - return false; - } - }); - - 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.getByText('Report a Bug').click(); - await page.locator('[name="name"]').fill('Jane Doe'); - await page.locator('[name="email"]').fill('janedoe@example.org'); - await page.locator('[name="message"]').fill('my example feedback'); - await page.getByLabel('Send Bug Report').click(); - - const [feedbackResp, replayReq] = await Promise.all([feedbackRequestPromise, reqPromise0]); - - const feedbackEvent = envelopeRequestParser(feedbackResp.request()); - const replayEvent = getReplayEvent(replayReq); - const { breadcrumbs } = getCustomRecordingEvents(replayReq); - - expect(breadcrumbs).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - category: 'sentry.feedback', - data: { feedbackId: expect.any(String) }, - }), - ]), - ); - - expect(feedbackEvent).toEqual({ - type: 'feedback', - breadcrumbs: expect.any(Array), - contexts: { - feedback: { - contact_email: 'janedoe@example.org', - message: 'my example feedback', - name: 'Jane Doe', - replay_id: replayEvent.event_id, - source: 'widget', - url: expect.stringContaining('/dist/index.html'), - }, - }, - level: 'info', - timestamp: expect.any(Number), - event_id: expect.stringMatching(/\w{32}/), - environment: 'production', - sdk: { - integrations: expect.arrayContaining(['Feedback']), - version: expect.any(String), - name: 'sentry.javascript.browser', - packages: expect.anything(), - }, - request: { - url: expect.stringContaining('/dist/index.html'), - headers: { - 'User-Agent': expect.stringContaining(''), - }, - }, - platform: 'javascript', - }); -}); diff --git a/packages/feedback/src/integration.ts b/packages/feedback/src/integration.ts index 501f844abeaa..b70bf008d24d 100644 --- a/packages/feedback/src/integration.ts +++ b/packages/feedback/src/integration.ts @@ -79,17 +79,17 @@ export class Feedback implements Integration { private _hasInsertedActorStyles: boolean; public constructor({ + autoInject = true, id = 'sentry-feedback', + isEmailRequired = false, + isNameRequired = false, showBranding = true, - autoInject = true, showEmail = true, showName = true, useSentryUser = { email: 'email', name: 'username', }, - isEmailRequired = false, - isNameRequired = false, themeDark, themeLight, @@ -123,9 +123,9 @@ export class Feedback implements Integration { this._hasInsertedActorStyles = false; this.options = { - id, - showBranding, autoInject, + showBranding, + id, isEmailRequired, isNameRequired, showEmail, diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts index b5e414803121..05b52ca64725 100644 --- a/packages/feedback/src/widget/createWidget.ts +++ b/packages/feedback/src/widget/createWidget.ts @@ -1,4 +1,4 @@ -import { getCurrentScope } from '@sentry/core'; +import { getClient, getCurrentScope } from '@sentry/core'; import { logger } from '@sentry/utils'; import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget } from '../types'; @@ -9,6 +9,8 @@ import type { DialogComponent } from './Dialog'; import { Dialog } from './Dialog'; import { SuccessMessage } from './SuccessMessage'; +import { DEBUG_BUILD } from '../debug-build'; + interface CreateWidgetParams { /** * Shadow DOM to append to @@ -124,6 +126,21 @@ export function createWidget({ } } + /** + * Internal handler when dialog is opened + */ + function handleOpenDialog(): void { + // Flush replay if integration exists + const client = getClient(); + const replay = client && client.getIntegrationByName<{ name: string; flush: () => Promise }>('Replay'); + if (!replay) { + return; + } + replay.flush().catch(err => { + DEBUG_BUILD && logger.error(err); + }); + } + /** * Displays the default actor */ @@ -156,6 +173,7 @@ export function createWidget({ if (options.onFormOpen) { options.onFormOpen(); } + handleOpenDialog(); return; } @@ -208,6 +226,7 @@ export function createWidget({ if (options.onFormOpen) { options.onFormOpen(); } + handleOpenDialog(); } catch (err) { // TODO: Error handling? logger.error(err); diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index 9b57e7dafec8..a5900fdea696 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -59,8 +59,6 @@ export function addGlobalListeners(replay: ReplayContainer): void { const replayId = replay.getSessionId(); if (options && options.includeReplay && replay.isEnabled() && replayId) { // This should never reject - // eslint-disable-next-line @typescript-eslint/no-floating-promises - replay.flush(); if (feedbackEvent.contexts && feedbackEvent.contexts.feedback) { feedbackEvent.contexts.feedback.replay_id = replayId; }