diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 765bfb1437d6..3a07b71794d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -571,3 +571,38 @@ jobs: run: | cd packages/node-integration-tests yarn test + + job_remix_integration_tests: + name: Remix SDK Integration Tests (${{ matrix.node }}) + needs: [job_get_metadata, job_build] + runs-on: ubuntu-latest + timeout-minutes: 10 + continue-on-error: true + strategy: + matrix: + node: [14, 16, 18] + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - name: Check dependency cache + uses: actions/cache@v2 + with: + path: ${{ env.CACHED_DEPENDENCY_PATHS }} + key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Check build cache + uses: actions/cache@v2 + with: + path: ${{ env.CACHED_BUILD_PATHS }} + key: ${{ env.BUILD_CACHE_KEY }} + - name: Run integration tests + env: + NODE_VERSION: ${{ matrix.node }} + run: | + cd packages/remix + yarn test:integration:ci diff --git a/packages/remix/.eslintrc.js b/packages/remix/.eslintrc.js index de52e01b9c4b..1e7a1ce3ea44 100644 --- a/packages/remix/.eslintrc.js +++ b/packages/remix/.eslintrc.js @@ -6,6 +6,7 @@ module.exports = { parserOptions: { jsx: true, }, + ignorePatterns: ['playwright.config.ts', 'test/integration/**'], extends: ['../../.eslintrc.js'], rules: { '@sentry-internal/sdk/no-async-await': 'off', diff --git a/packages/remix/jest.config.js b/packages/remix/jest.config.js index 24f49ab59a4c..51baeb4f27c0 100644 --- a/packages/remix/jest.config.js +++ b/packages/remix/jest.config.js @@ -1 +1,6 @@ -module.exports = require('../../jest/jest.config.js'); +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + testPathIgnorePatterns: ['/build/', '/node_modules/', '/test/integration/'], +}; diff --git a/packages/remix/package.json b/packages/remix/package.json index 844f26a96ea4..8b2c1b990f79 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -60,6 +60,12 @@ "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", "test": "run-s test:unit", + "test:integration": "run-s test:integration:prepare test:integration:client test:integration:server", + "test:integration:ci": "run-s test:integration:prepare test:integration:client:ci test:integration:server", + "test:integration:prepare": "(cd test/integration && yarn)", + "test:integration:client": "yarn playwright install-deps && yarn playwright test test/integration/test/client/", + "test:integration:client:ci": "yarn test:integration:client --browser='all' --reporter='line'", + "test:integration:server": "jest --config=test/integration/jest.config.js test/integration/test/server/", "test:unit": "jest", "test:watch": "jest --watch" }, diff --git a/packages/remix/playwright.config.ts b/packages/remix/playwright.config.ts new file mode 100644 index 000000000000..91146860f3f7 --- /dev/null +++ b/packages/remix/playwright.config.ts @@ -0,0 +1,16 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + retries: 2, + timeout: 12000, + use: { + baseURL: 'http://localhost:3000', + }, + workers: 3, + webServer: { + command: '(cd test/integration/ && yarn build && yarn start)', + port: 3000, + }, +}; + +export default config; diff --git a/packages/remix/test/integration/.gitignore b/packages/remix/test/integration/.gitignore new file mode 100644 index 000000000000..94f2cbbd8a42 --- /dev/null +++ b/packages/remix/test/integration/.gitignore @@ -0,0 +1,10 @@ +node_modules + +/.cache +/build +/public/build +.env +/test-results/ +/playwright-report/ +/playwright/.cache/ +yarn.lock diff --git a/packages/remix/test/integration/app/entry.client.tsx b/packages/remix/test/integration/app/entry.client.tsx new file mode 100644 index 000000000000..f9cfc14f2507 --- /dev/null +++ b/packages/remix/test/integration/app/entry.client.tsx @@ -0,0 +1,16 @@ +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import { hydrate } from 'react-dom'; +import * as Sentry from '@sentry/remix'; +import { useEffect } from 'react'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + integrations: [ + new Sentry.BrowserTracing({ + routingInstrumentation: Sentry.remixRouterInstrumentation(useEffect, useLocation, useMatches), + }), + ], +}); + +hydrate(, document); diff --git a/packages/remix/test/integration/app/entry.server.tsx b/packages/remix/test/integration/app/entry.server.tsx new file mode 100644 index 000000000000..e51fd3f73f87 --- /dev/null +++ b/packages/remix/test/integration/app/entry.server.tsx @@ -0,0 +1,25 @@ +import type { EntryContext } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import { renderToString } from 'react-dom/server'; +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, +}); + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + let markup = renderToString(); + + responseHeaders.set('Content-Type', 'text/html'); + + return new Response('' + markup, { + status: responseStatusCode, + headers: responseHeaders, + }); +} diff --git a/packages/remix/test/integration/app/root.tsx b/packages/remix/test/integration/app/root.tsx new file mode 100644 index 000000000000..53e82e0e391e --- /dev/null +++ b/packages/remix/test/integration/app/root.tsx @@ -0,0 +1,28 @@ +import type { MetaFunction } from '@remix-run/node'; +import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react'; +import { withSentry } from '@sentry/remix'; + +export const meta: MetaFunction = () => ({ + charset: 'utf-8', + title: 'New Remix App', + viewport: 'width=device-width,initial-scale=1', +}); + +function App() { + return ( + + + + + + + + + + + + + ); +} + +export default withSentry(App); diff --git a/packages/remix/test/integration/app/routes/index.tsx b/packages/remix/test/integration/app/routes/index.tsx new file mode 100644 index 000000000000..c21dea0629f3 --- /dev/null +++ b/packages/remix/test/integration/app/routes/index.tsx @@ -0,0 +1,7 @@ +export default function Index() { + return ( +
+

Remix Integration Tests Home

+
+ ); +} diff --git a/packages/remix/test/integration/app/routes/loader-json-response/$id.tsx b/packages/remix/test/integration/app/routes/loader-json-response/$id.tsx new file mode 100644 index 000000000000..833b59f5bda2 --- /dev/null +++ b/packages/remix/test/integration/app/routes/loader-json-response/$id.tsx @@ -0,0 +1,20 @@ +import { json, LoaderFunction } from '@remix-run/node'; +import { useLoaderData } from '@remix-run/react'; + +type LoaderData = { id: string }; + +export const loader: LoaderFunction = async ({ params: { id } }) => { + return json({ + id, + }); +}; + +export default function LoaderJSONResponse() { + const data = useLoaderData(); + + return ( +
+

{data.id}

+
+ ); +} diff --git a/packages/remix/test/integration/jest.config.js b/packages/remix/test/integration/jest.config.js new file mode 100644 index 000000000000..d3173100f47f --- /dev/null +++ b/packages/remix/test/integration/jest.config.js @@ -0,0 +1,9 @@ +const baseConfig = require('../../jest.config.js'); + +module.exports = { + ...baseConfig, + testMatch: [`${__dirname}/test/server/**/*.test.ts`], + testPathIgnorePatterns: [`${__dirname}/test/client`], + detectOpenHandles: true, + forceExit: true, +}; diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json new file mode 100644 index 000000000000..09f370fe8d69 --- /dev/null +++ b/packages/remix/test/integration/package.json @@ -0,0 +1,38 @@ +{ + "private": true, + "sideEffects": false, + "scripts": { + "build": "remix build", + "dev": "remix dev", + "start": "remix-serve build" + }, + "dependencies": { + "@remix-run/express": "^1.6.5", + "@remix-run/node": "^1.6.5", + "@remix-run/react": "^1.6.5", + "@remix-run/serve": "^1.6.5", + "@sentry/remix": "file:../..", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@remix-run/dev": "^1.6.5", + "@types/react": "^17.0.47", + "@types/react-dom": "^17.0.17", + "typescript": "^4.2.4" + }, + "resolutions": { + "@sentry/browser": "file:../../../browser", + "@sentry/core": "file:../../../core", + "@sentry/hub": "file:../../../hub", + "@sentry/integrations": "file:../../../integrations", + "@sentry/node": "file:../../../node", + "@sentry/react": "file:../../../react", + "@sentry/tracing": "file:../../../tracing", + "@sentry/types": "file:../../../types", + "@sentry/utils": "file:../../../utils" + }, + "engines": { + "node": ">=14" + } +} diff --git a/packages/remix/test/integration/remix.config.js b/packages/remix/test/integration/remix.config.js new file mode 100644 index 000000000000..02f847cbf1ca --- /dev/null +++ b/packages/remix/test/integration/remix.config.js @@ -0,0 +1,7 @@ +/** @type {import('@remix-run/dev').AppConfig} */ +module.exports = { + appDirectory: 'app', + assetsBuildDirectory: 'public/build', + serverBuildPath: 'build/index.js', + publicPath: '/build/', +}; diff --git a/packages/remix/test/integration/test/client/pageload.test.ts b/packages/remix/test/integration/test/client/pageload.test.ts new file mode 100644 index 000000000000..1543bd2a342c --- /dev/null +++ b/packages/remix/test/integration/test/client/pageload.test.ts @@ -0,0 +1,12 @@ +import { getFirstSentryEnvelopeRequest } from './utils/helpers'; +import { test, expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +test('should add `pageload` transaction on load.', async ({ page }) => { + const envelope = await getFirstSentryEnvelopeRequest(page, '/'); + + expect(envelope.contexts?.trace.op).toBe('pageload'); + expect(envelope.tags?.['routing.instrumentation']).toBe('remix-router'); + expect(envelope.type).toBe('transaction'); + expect(envelope.transaction).toBe('routes/index'); +}); diff --git a/packages/remix/test/integration/test/client/utils/helpers.ts b/packages/remix/test/integration/test/client/utils/helpers.ts new file mode 100644 index 000000000000..38619935d908 --- /dev/null +++ b/packages/remix/test/integration/test/client/utils/helpers.ts @@ -0,0 +1 @@ +export * from '../../../../../../integration-tests/utils/helpers'; diff --git a/packages/remix/test/integration/test/server/loader.test.ts b/packages/remix/test/integration/test/server/loader.test.ts new file mode 100644 index 000000000000..07cf52213150 --- /dev/null +++ b/packages/remix/test/integration/test/server/loader.test.ts @@ -0,0 +1,23 @@ +import { assertSentryTransaction, getEnvelopeRequest, runServer } from './utils/helpers'; + +describe('Remix API Loaders', () => { + it('correctly instruments a Remix API loader', async () => { + const baseURL = await runServer(); + const url = `${baseURL}/loader-json-response/123123`; + const envelope = await getEnvelopeRequest(url); + const transaction = envelope[2]; + + assertSentryTransaction(transaction, { + spans: [ + { + description: url, + op: 'remix.server.loader', + }, + { + description: url, + op: 'remix.server.documentRequest', + }, + ], + }); + }); +}); diff --git a/packages/remix/test/integration/test/server/utils/helpers.ts b/packages/remix/test/integration/test/server/utils/helpers.ts new file mode 100644 index 000000000000..dcce78644415 --- /dev/null +++ b/packages/remix/test/integration/test/server/utils/helpers.ts @@ -0,0 +1,24 @@ +import express from 'express'; +import { createRequestHandler } from '@remix-run/express'; +import { getPortPromise } from 'portfinder'; + +export * from '../../../../../../node-integration-tests/utils'; + +/** + * Runs a test server + * @returns URL + */ +export async function runServer(): Promise { + const app = express(); + const port = await getPortPromise(); + + app.all('*', createRequestHandler({ build: require('../../../build') })); + + const server = app.listen(port, () => { + setTimeout(() => { + server.close(); + }, 4000); + }); + + return `http://localhost:${port}`; +} diff --git a/packages/remix/test/integration/tsconfig.json b/packages/remix/test/integration/tsconfig.json new file mode 100644 index 000000000000..2129c1a599f6 --- /dev/null +++ b/packages/remix/test/integration/tsconfig.json @@ -0,0 +1,20 @@ +{ + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true + } +} diff --git a/packages/remix/test/integration/tsconfig.test.json b/packages/remix/test/integration/tsconfig.test.json new file mode 100644 index 000000000000..d3175b6a1b01 --- /dev/null +++ b/packages/remix/test/integration/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*"], + + "compilerOptions": { + "types": ["node", "jest"] + } +} diff --git a/packages/remix/tsconfig.test.json b/packages/remix/tsconfig.test.json index d3175b6a1b01..7aa20c05d60c 100644 --- a/packages/remix/tsconfig.test.json +++ b/packages/remix/tsconfig.test.json @@ -4,6 +4,7 @@ "include": ["test/**/*"], "compilerOptions": { - "types": ["node", "jest"] + "types": ["node", "jest"], + "esModuleInterop": true } }