diff --git a/packages/browser-integration-tests/suites/feedback/captureFeedback/init.js b/packages/browser-integration-tests/suites/feedback/captureFeedback/init.js new file mode 100644 index 000000000000..6c9382f04575 --- /dev/null +++ b/packages/browser-integration-tests/suites/feedback/captureFeedback/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; +import { Feedback } from '@sentry-internal/feedback'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Feedback()], +}); diff --git a/packages/browser-integration-tests/suites/feedback/captureFeedback/template.html b/packages/browser-integration-tests/suites/feedback/captureFeedback/template.html new file mode 100644 index 000000000000..57334d4ad2f1 --- /dev/null +++ b/packages/browser-integration-tests/suites/feedback/captureFeedback/template.html @@ -0,0 +1,7 @@ + + +
+ + + + diff --git a/packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts b/packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts new file mode 100644 index 000000000000..26ba665a418f --- /dev/null +++ b/packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts @@ -0,0 +1,74 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, getEnvelopeType } from '../../../utils/helpers'; + +sentryTest('should capture feedback (@sentry-internal/feedback import)', async ({ getLocalTestPath, page }) => { + if (process.env.PW_BUNDLE) { + sentryTest.skip(); + } + + 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(); + expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); + 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 feedbackEvent = envelopeRequestParser((await feedbackRequestPromise).request()); + expect(feedbackEvent).toEqual({ + type: 'feedback', + contexts: { + feedback: { + contact_email: 'janedoe@example.org', + message: 'my example feedback', + name: 'Jane Doe', + 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/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/init.js b/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/init.js new file mode 100644 index 000000000000..cceaaea1a373 --- /dev/null +++ b/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; +import { Feedback } from '@sentry-internal/feedback'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + replaysOnErrorSampleRate: 1.0, + replaysSessionSampleRate: 0, + integrations: [ + new Sentry.Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, + }), + new Feedback(), + ], +}); diff --git a/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/template.html b/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/template.html new file mode 100644 index 000000000000..57334d4ad2f1 --- /dev/null +++ b/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/template.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/test.ts b/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/test.ts new file mode 100644 index 000000000000..1590c68652a3 --- /dev/null +++ b/packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/test.ts @@ -0,0 +1,90 @@ +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([ + { + category: 'sentry.feedback', + data: { feedbackId: expect.any(String) }, + }, + ]), + ); + + expect(feedbackEvent).toEqual({ + type: 'feedback', + 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/constants.ts b/packages/feedback/src/constants.ts index a91283f4a541..ea0db22525f4 100644 --- a/packages/feedback/src/constants.ts +++ b/packages/feedback/src/constants.ts @@ -60,3 +60,6 @@ export const MESSAGE_LABEL = 'Description'; export const NAME_PLACEHOLDER = 'Your Name'; export const NAME_LABEL = 'Name'; export const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!'; + +export const FEEDBACK_WIDGET_SOURCE = 'widget'; +export const FEEDBACK_API_SOURCE = 'api'; diff --git a/packages/feedback/src/sendFeedback.ts b/packages/feedback/src/sendFeedback.ts index c77013602d6b..4f1b79761a96 100644 --- a/packages/feedback/src/sendFeedback.ts +++ b/packages/feedback/src/sendFeedback.ts @@ -2,6 +2,7 @@ import type { BrowserClient, Replay } from '@sentry/browser'; import { getCurrentHub } from '@sentry/core'; import { getLocationHref } from '@sentry/utils'; +import { FEEDBACK_API_SOURCE } from './constants'; import type { SendFeedbackOptions } from './types'; import { sendFeedbackRequest } from './util/sendFeedbackRequest'; @@ -17,7 +18,7 @@ interface SendFeedbackParams { * Public API to send a Feedback item to Sentry */ export function sendFeedback( - { name, email, message, source = 'api', url = getLocationHref() }: SendFeedbackParams, + { name, email, message, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams, { includeReplay = true }: SendFeedbackOptions = {}, ): ReturnType