diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d8562473837..b179159f4dfd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1048,7 +1048,8 @@ jobs: # TODO(v8): Re-enable hapi tests # 'node-hapi-app', 'node-exports-test-app', - 'vue-3' + 'vue-3', + 'webpack-5' ] build-command: - false diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/.npmrc b/dev-packages/e2e-tests/test-applications/webpack-5/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-5/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/build.mjs b/dev-packages/e2e-tests/test-applications/webpack-5/build.mjs new file mode 100644 index 000000000000..11874cb62374 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-5/build.mjs @@ -0,0 +1,44 @@ +import * as path from 'path'; +import * as url from 'url'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import webpack from 'webpack'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +webpack( + { + entry: path.join(__dirname, 'entry.js'), + output: { + path: path.join(__dirname, 'build'), + filename: 'app.js', + }, + optimization: { + minimize: true, + minimizer: [new TerserPlugin()], + }, + plugins: [new HtmlWebpackPlugin(), new webpack.EnvironmentPlugin(['E2E_TEST_DSN'])], + mode: 'production', + }, + (err, stats) => { + if (err) { + console.error(err.stack || err); + if (err.details) { + console.error(err.details); + } + return; + } + + const info = stats.toJson(); + + if (stats.hasErrors()) { + console.error(info.errors); + process.exit(1); + } + + if (stats.hasWarnings()) { + console.warn(info.warnings); + process.exit(1); + } + }, +); diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/entry.js b/dev-packages/e2e-tests/test-applications/webpack-5/entry.js new file mode 100644 index 000000000000..4fd9cd67e7e3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-5/entry.js @@ -0,0 +1,11 @@ +import { browserTracingIntegration, captureException, init } from '@sentry/browser'; + +init({ + dsn: process.env.E2E_TEST_DSN, + integrations: [browserTracingIntegration()], +}); + +setTimeout(() => { + const eventId = captureException(new Error('I am an error!')); + window.capturedExceptionId = eventId; +}, 2000); diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/package.json b/dev-packages/e2e-tests/test-applications/webpack-5/package.json new file mode 100644 index 000000000000..871e43589971 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-5/package.json @@ -0,0 +1,18 @@ +{ + "name": "webpack-5-test", + "version": "1.0.0", + "scripts": { + "start": "serve -s build", + "build": "node build.mjs", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.42.1", + "@sentry/browser": "latest || *", + "webpack": "^5.91.0", + "terser-webpack-plugin": "^5.3.10", + "html-webpack-plugin": "^5.6.0", + "serve": "^14.2.1" + } +} diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/playwright.config.ts b/dev-packages/e2e-tests/test-applications/webpack-5/playwright.config.ts new file mode 100644 index 000000000000..5f93f826ebf0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-5/playwright.config.ts @@ -0,0 +1,70 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'pnpm start', + port: 3030, + env: { + PORT: '3030', + }, + }, +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/tests/behaviour-test.spec.ts b/dev-packages/e2e-tests/test-applications/webpack-5/tests/behaviour-test.spec.ts new file mode 100644 index 000000000000..4f762a4028d4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/webpack-5/tests/behaviour-test.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from '@playwright/test'; +import axios, { AxiosError } from 'axios'; + +const EVENT_POLLING_TIMEOUT = 90_000; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; + +test('Sends an exception to Sentry', async ({ page }) => { + await page.goto('/'); + + const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); + const exceptionEventId = await exceptionIdHandle.jsonValue(); + + console.log(`Polling for error eventId: ${exceptionEventId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +});