From 83afd3e67a48f456408e52310848a4bade0ba83b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 6 Aug 2024 10:03:56 +0200 Subject: [PATCH 1/7] test: add integration test that uses csp --- .../suites/feedback/csp-nonce/init.js | 19 +++ .../suites/feedback/csp-nonce/template.html | 10 ++ .../suites/feedback/csp-nonce/test.ts | 119 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/feedback/csp-nonce/init.js create mode 100644 dev-packages/browser-integration-tests/suites/feedback/csp-nonce/template.html create mode 100644 dev-packages/browser-integration-tests/suites/feedback/csp-nonce/test.ts diff --git a/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/init.js b/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/init.js new file mode 100644 index 000000000000..576e213e8de9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; +// Import this separately so that generatePlugin can handle it for CDN scenarios +import { feedbackIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + replaysOnErrorSampleRate: 1.0, + replaysSessionSampleRate: 1.0, + integrations: [Sentry.replayIntegration(), feedbackIntegration()], +}); + +document.addEventListener('securitypolicyviolation', () => { + const container = document.querySelector('#csp-violation'); + if (container) { + container.innerText = 'CSP Violation'; + } +}); diff --git a/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/template.html b/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/template.html new file mode 100644 index 000000000000..842369a489f8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/template.html @@ -0,0 +1,10 @@ + + + + + + + +
+ + diff --git a/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/test.ts b/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/test.ts new file mode 100644 index 000000000000..3f1003d24fa4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/csp-nonce/test.ts @@ -0,0 +1,119 @@ +import { expect } from '@playwright/test'; + +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, getEnvelopeType, shouldSkipFeedbackTest } from '../../../utils/helpers'; +import { + collectReplayRequests, + getReplayBreadcrumbs, + shouldSkipReplayTest, + waitForReplayRequest, +} from '../../../utils/replayHelpers'; + +sentryTest('should not log CSP errors', async ({ forceFlushReplay, getLocalTestUrl, page }) => { + if (shouldSkipFeedbackTest() || shouldSkipReplayTest()) { + 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 getLocalTestUrl({ testDir: __dirname }); + + await Promise.all([page.goto(url), page.getByText('Report a Bug').click(), reqPromise0]); + + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayBreadcrumbs(recordingEvents).some(breadcrumb => breadcrumb.category === 'sentry.feedback'); + }); + + // 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()]); + + const [, feedbackResp] = await Promise.all([ + page.locator('[data-sentry-feedback] .btn--primary').click(), + feedbackRequestPromise, + ]); + + const { replayEvents, replayRecordingSnapshots } = await replayRequestPromise; + const breadcrumbs = getReplayBreadcrumbs(replayRecordingSnapshots); + + const replayEvent = replayEvents[0]; + const feedbackEvent = envelopeRequestParser(feedbackResp.request()); + + 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: `${TEST_HOST}/index.html`, + }, + trace: { + trace_id: expect.stringMatching(/\w{32}/), + span_id: expect.stringMatching(/\w{16}/), + }, + }, + level: 'info', + tags: {}, + 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: `${TEST_HOST}/index.html`, + headers: { + 'User-Agent': expect.stringContaining(''), + }, + }, + platform: 'javascript', + }); + + const cspContainer = await page.locator('#csp-violation'); + expect(cspContainer).not.toContainText('CSP Violation'); +}); From 9f54dbb0742b32d1c2169f79efccc0fdfcfa9a75 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 6 Aug 2024 13:56:16 +0200 Subject: [PATCH 2/7] feat: add nonce to replay integration --- packages/browser/src/utils/lazyLoadIntegration.ts | 5 +++++ packages/feedback/src/core/createMainStyles.ts | 11 ++++++++++- packages/feedback/src/core/integration.ts | 2 ++ packages/types/src/feedback/config.ts | 5 +++++ packages/types/src/options.ts | 5 +++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index 4479c2e69590..3d69203dfef0 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -33,6 +33,8 @@ const WindowWithMaybeIntegration = WINDOW as { */ export async function lazyLoadIntegration(name: keyof typeof LazyLoadableIntegrations): Promise { const bundle = LazyLoadableIntegrations[name]; + const client = getClient(); + const nonce = client && client.getOptions().nonce; // `window.Sentry` is only set when using a CDN bundle, but this method can also be used via the NPM package const sentryOnWindow = (WindowWithMaybeIntegration.Sentry = WindowWithMaybeIntegration.Sentry || {}); @@ -52,6 +54,9 @@ export async function lazyLoadIntegration(name: keyof typeof LazyLoadableIntegra script.src = url; script.crossOrigin = 'anonymous'; script.referrerPolicy = 'origin'; + if (nonce) { + script.nonce = nonce; + } const waitForLoad = new Promise((resolve, reject) => { script.addEventListener('load', () => resolve()); diff --git a/packages/feedback/src/core/createMainStyles.ts b/packages/feedback/src/core/createMainStyles.ts index 68ac751b8584..bed88d67a324 100644 --- a/packages/feedback/src/core/createMainStyles.ts +++ b/packages/feedback/src/core/createMainStyles.ts @@ -51,7 +51,12 @@ function getThemedCssVariables(theme: InternalTheme): string { /** * Creates