From 4ce48e97c3e144eeb6356ef5908150eacb53f48f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 25 Sep 2024 13:17:14 +0200 Subject: [PATCH 01/22] ci: Fix adding bundle sizes to releases (#13792) Implements https://github.com/getsentry/size-limit-release/pull/6 This is now working again: https://github.com/getsentry/sentry-javascript/actions/runs/11031636865 --- .github/workflows/release-size-info.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-size-info.yml b/.github/workflows/release-size-info.yml index f56883faf986..04e51e5ae14e 100644 --- a/.github/workflows/release-size-info.yml +++ b/.github/workflows/release-size-info.yml @@ -23,7 +23,7 @@ jobs: - name: Update Github Release if: steps.get_version.outputs.version != '' - uses: getsentry/size-limit-release@v1 + uses: getsentry/size-limit-release@v2 with: github_token: ${{ secrets.GITHUB_TOKEN }} version: ${{ steps.get_version.outputs.version }} From a02cdb41ebd2d804efed2c23b050121bd4fdc95c Mon Sep 17 00:00:00 2001 From: Jasmin <77064737+jas-kas@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:37:01 -0400 Subject: [PATCH 02/22] Update Bug Report template to clarify that the event URL is public.yml Based on customer feedback. --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 43e4e82c61b0..c0b943277570 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -78,7 +78,7 @@ body: label: Link to Sentry event description: If applicable, please provide a link to the affected event from your Sentry account. The event will only be - viewable by Sentry staff. + viewable by Sentry staff; however, the event URL will still appear on your public GitHub issue. placeholder: https://sentry.io/organizations//issues//events//?project= - type: textarea id: sdk-setup From e5478ad86d46e5f19cc4eb2cc9d53095d643cd2f Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:11:45 +0200 Subject: [PATCH 03/22] fix(nuxt): Use absolute path for client config (#13798) The `buildDir` can be changed in Nuxt and Adding the client config with a relative path worked so far as the `buildDir` in Nuxt v3 is [`//.nuxt`](https://nuxt.com/docs/api/nuxt-config#builddir) but the `buildDir` can be modified by the user and is also different in Nuxt v4 ([`//node_modules/.cache/nuxt/.nuxt`](https://github.com/nuxt/nuxt/blob/f56c05d39de5223c5fa8603ed216e91747937d54/packages/kit/src/loader/config.ts#L54)). By using the absolute path, the client config can be injected correctly with all `buildDir` variations --- packages/nuxt/src/vite/utils.ts | 4 +--- packages/nuxt/test/vite/utils.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 7d794e807fd7..e41d3fb06cab 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -22,7 +22,5 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde } } - const filePath = filePaths.find(filename => fs.existsSync(filename)); - - return filePath ? path.basename(filePath) : undefined; + return filePaths.find(filename => fs.existsSync(filename)); } diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 0ca81b3e2986..5115742be0f0 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -10,26 +10,26 @@ describe('findDefaultSdkInitFile', () => { }); it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( - 'should return the server file with .%s extension if it exists', + 'should return the server file path with .%s extension if it exists', ext => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return !(filePath instanceof URL) && filePath.includes(`sentry.server.config.${ext}`); }); const result = findDefaultSdkInitFile('server'); - expect(result).toBe(`sentry.server.config.${ext}`); + expect(result).toMatch(`packages/nuxt/sentry.server.config.${ext}`); }, ); it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( - 'should return the client file with .%s extension if it exists', + 'should return the client file path with .%s extension if it exists', ext => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return !(filePath instanceof URL) && filePath.includes(`sentry.client.config.${ext}`); }); const result = findDefaultSdkInitFile('client'); - expect(result).toBe(`sentry.client.config.${ext}`); + expect(result).toMatch(`packages/nuxt/sentry.client.config.${ext}`); }, ); @@ -47,7 +47,7 @@ describe('findDefaultSdkInitFile', () => { expect(result).toBeUndefined(); }); - it('should return the server config file if server.config and instrument exist', () => { + it('should return the server config file path if server.config and instrument exist', () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return ( !(filePath instanceof URL) && @@ -56,6 +56,6 @@ describe('findDefaultSdkInitFile', () => { }); const result = findDefaultSdkInitFile('server'); - expect(result).toBe('sentry.server.config.js'); + expect(result).toMatch('packages/nuxt/sentry.server.config.js'); }); }); From 1bd15f3c95106c635af3f0d6941dabac21a507ba Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:44:42 +0200 Subject: [PATCH 04/22] fix(nuxt): Don't restrict source map assets upload (#13800) Source maps generated by Vite are only for the Nuxt-part of the application whereas source maps generated by Rollup are only for the Nitro-part of the application. The Nuxt-part of the application has some overlap with the Nitro-part and so it **would** be nice to specify the client/server folder in the `assets` option, but the output is different for every Nitro preset (the files are always located under a different path). Should fix https://github.com/getsentry/sentry-javascript/issues/13703 --- packages/nuxt/src/vite/sourceMaps.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index 18eed2cbcfd8..801d16ab71cf 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -39,7 +39,7 @@ export function setupSourceMaps(moduleOptions: SentryNuxtModuleOptions, nuxt: Nu } // Add Sentry plugin - nitroConfig.rollupConfig.plugins.push(sentryRollupPlugin(getPluginOptions(moduleOptions, true))); + nitroConfig.rollupConfig.plugins.push(sentryRollupPlugin(getPluginOptions(moduleOptions))); // Enable source maps nitroConfig.rollupConfig.output = nitroConfig?.rollupConfig?.output || {}; @@ -58,10 +58,7 @@ function normalizePath(path: string): string { return path.replace(/^(\.\.\/)+/, './'); } -function getPluginOptions( - moduleOptions: SentryNuxtModuleOptions, - isNitro = false, -): SentryVitePluginOptions | SentryRollupPluginOptions { +function getPluginOptions(moduleOptions: SentryNuxtModuleOptions): SentryVitePluginOptions | SentryRollupPluginOptions { const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; return { @@ -70,8 +67,10 @@ function getPluginOptions( authToken: sourceMapsUploadOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN, telemetry: sourceMapsUploadOptions.telemetry ?? true, sourcemaps: { - assets: - sourceMapsUploadOptions.sourcemaps?.assets ?? isNitro ? ['./.output/server/**/*'] : ['./.output/public/**/*'], + // The server/client files are in different places depending on the nitro preset (e.g. '.output/server' or '.netlify/functions-internal/server') + // We cannot determine automatically how the build folder looks like (depends on the preset), so we have to accept that sourcemaps are uploaded multiple times (with the vitePlugin for Nuxt and the rollupPlugin for Nitro). + // If we could know where the server/client assets are located, we could do something like this (based on the Nitro preset): isNitro ? ['./.output/server/**/*'] : ['./.output/public/**/*'], + assets: sourceMapsUploadOptions.sourcemaps?.assets ?? undefined, ignore: sourceMapsUploadOptions.sourcemaps?.ignore ?? undefined, filesToDeleteAfterUpload: sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload ?? undefined, rewriteSources: (source: string) => normalizePath(source), From 3714d606dbd0ad54c995fd5fa1b5a75f67f28a3c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 26 Sep 2024 13:32:52 +0200 Subject: [PATCH 05/22] ci: Automatically notify issues after release (#13808) This ensures that whenever we publish a release, we automatically add a comment to all issues linked to PRs like this: https://github.com/getsentry/sentry-javascript/issues/13312#issuecomment-2376455183 ![image](https://github.com/user-attachments/assets/c84a810f-5653-4fcc-85d0-bc6ad0aa4801) This way, we do not need to manually go through issues, but users can automatically be notified that a change should be out now. --- .github/workflows/release-comment-issues.yml | 33 +++++ .../.eslintrc.cjs | 14 ++ .../action.yml | 12 ++ .../index.mjs | 138 ++++++++++++++++++ .../package.json | 23 +++ package.json | 1 + yarn.lock | 110 ++++++++++++++ 7 files changed, 331 insertions(+) create mode 100644 .github/workflows/release-comment-issues.yml create mode 100644 dev-packages/release-comment-issues-gh-action/.eslintrc.cjs create mode 100644 dev-packages/release-comment-issues-gh-action/action.yml create mode 100644 dev-packages/release-comment-issues-gh-action/index.mjs create mode 100644 dev-packages/release-comment-issues-gh-action/package.json diff --git a/.github/workflows/release-comment-issues.yml b/.github/workflows/release-comment-issues.yml new file mode 100644 index 000000000000..80cb04eb6b54 --- /dev/null +++ b/.github/workflows/release-comment-issues.yml @@ -0,0 +1,33 @@ +name: "Automation: Notify issues for release" +on: + release: + types: + - published + workflow_dispatch: + inputs: + version: + description: Which version to notify issues for + required: false + +# This workflow is triggered when a release is published +jobs: + release-comment-issues: + runs-on: ubuntu-20.04 + name: 'Notify issues' + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Get version + id: get_version + run: echo "version=${{ github.event.inputs.version || github.event.release.tag_name }}" >> $GITHUB_OUTPUT + + - name: Comment on linked issues that are mentioned in release + if: steps.get_version.outputs.version != '' + uses: ./dev-packages/release-comment-issues-gh-action + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ steps.get_version.outputs.version }} diff --git a/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs b/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs new file mode 100644 index 000000000000..8c67e0037908 --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + extends: ['../../.eslintrc.js'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + }, + + overrides: [ + { + files: ['*.mjs'], + extends: ['@sentry-internal/sdk/src/base'], + }, + ], +}; diff --git a/dev-packages/release-comment-issues-gh-action/action.yml b/dev-packages/release-comment-issues-gh-action/action.yml new file mode 100644 index 000000000000..fcfb0bf00f2b --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/action.yml @@ -0,0 +1,12 @@ +name: 'release-comment-issues-gh-action' +description: 'An internal Github Action to comment on related issues when a release is published.' +inputs: + github_token: + required: true + description: 'a github access token' + version: + required: true + description: 'Which version was released' +runs: + using: 'node20' + main: 'index.mjs' diff --git a/dev-packages/release-comment-issues-gh-action/index.mjs b/dev-packages/release-comment-issues-gh-action/index.mjs new file mode 100644 index 000000000000..c49ad6575ef7 --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/index.mjs @@ -0,0 +1,138 @@ +import * as core from '@actions/core'; +import { context, getOctokit } from '@actions/github'; + +const RELEASE_COMMENT_HEADING = '## A PR closing this issue has just been released 🚀'; + +async function run() { + const { getInput } = core; + + const githubToken = getInput('github_token'); + const version = getInput('version'); + + if (!githubToken || !version) { + core.debug('Skipping because github_token or version are empty.'); + return; + } + + const { owner, repo } = context.repo; + + const octokit = getOctokit(githubToken); + + const release = await octokit.request('GET /repos/{owner}/{repo}/releases/tags/{tag}', { + owner, + repo, + tag: version, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + + const prNumbers = extractPrsFromReleaseBody(release.data.body, { repo, owner }); + + if (!prNumbers.length) { + core.debug('No PRs found in release body.'); + return; + } + + core.debug(`Found PRs in release body: ${prNumbers.join(', ')}`); + + const linkedIssues = await Promise.all( + prNumbers.map(prNumber => getLinkedIssuesForPr(octokit, { repo, owner, prNumber })), + ); + + for (const pr of linkedIssues) { + if (!pr.issues.length) { + core.debug(`No linked issues found for PR #${pr.prNumber}`); + continue; + } + + core.debug(`Linked issues for PR #${pr.prNumber}: ${pr.issues.map(issue => issue.number).join(',')}`); + + for (const issue of pr.issues) { + if (await hasExistingComment(octokit, { repo, owner, issueNumber: issue.number })) { + core.debug(`Comment already exists for issue #${issue.number}`); + continue; + } + + const body = `${RELEASE_COMMENT_HEADING}\n\nThis issue was referenced by PR #${pr.prNumber}, which was included in the [${version} release](https://github.com/${owner}/${repo}/releases/tag/${version}).`; + + core.debug(`Creating comment for issue #${issue.number}`); + + await octokit.rest.issues.createComment({ + repo, + owner, + issue_number: issue.number, + body, + }); + } + } +} + +/** + * + * @param {string} body + * @param {{ repo: string, owner: string}} options + * @returns {number[]} + */ +function extractPrsFromReleaseBody(body, { repo, owner }) { + const regex = new RegExp(`\\[#(\\d+)\\]\\(https:\\/\\/github\\.com\\/${owner}\\/${repo}\\/pull\\/(?:\\d+)\\)`, 'gm'); + const prNumbers = Array.from(new Set([...body.matchAll(regex)].map(match => parseInt(match[1])))); + + return prNumbers.filter(number => !!number && !Number.isNaN(number)); +} + +/** + * + * @param {ReturnType} octokit + * @param {{ repo: string, owner: string, prNumber: number}} options + * @returns {Promise<{ prNumber: number, issues: {id: string, number: number}[] }>} + */ +async function getLinkedIssuesForPr(octokit, { repo, owner, prNumber }) { + const res = await octokit.graphql( + ` +query issuesForPr($owner: String!, $repo: String!, $prNumber: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $prNumber) { + id + closingIssuesReferences (first: 50) { + edges { + node { + id + number + } + } + } + } + } +}`, + { + prNumber, + owner, + repo, + }, + ); + + const issues = res.repository?.pullRequest?.closingIssuesReferences.edges.map(edge => edge.node); + return { + prNumber, + issues, + }; +} + +/** + * + * @param {ReturnType} octokit + * @param {{ repo: string, owner: string, issueNumber: number}} options + * @returns {Promise} + */ +async function hasExistingComment(octokit, { repo, owner, issueNumber }) { + const { data: commentList } = await octokit.rest.issues.listComments({ + repo, + owner, + issue_number: issueNumber, + }); + + return commentList.some(comment => comment.body.startsWith(RELEASE_COMMENT_HEADING)); +} + +run(); diff --git a/dev-packages/release-comment-issues-gh-action/package.json b/dev-packages/release-comment-issues-gh-action/package.json new file mode 100644 index 000000000000..49c8f2ad5caa --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/package.json @@ -0,0 +1,23 @@ +{ + "name": "@sentry-internal/release-comment-issues-gh-action", + "description": "An internal Github Action to comment on related issues when a release is published.", + "version": "8.31.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "private": true, + "main": "index.mjs", + "type": "module", + "scripts": { + "lint": "eslint . --format stylish", + "fix": "eslint . --format stylish --fix" + }, + "dependencies": { + "@actions/core": "1.10.1", + "@actions/github": "6.0.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/package.json b/package.json index 365e1eb13922..b17092f04a80 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "dev-packages/size-limit-gh-action", "dev-packages/clear-cache-gh-action", "dev-packages/external-contributor-gh-action", + "dev-packages/release-comment-issues-gh-action", "dev-packages/rollup-utils" ], "devDependencies": { diff --git a/yarn.lock b/yarn.lock index a32337e3835f..91c55bb33d69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,16 @@ dependencies: "@actions/io" "^1.0.1" +"@actions/github@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-6.0.0.tgz#65883433f9d81521b782a64cc1fd45eef2191ea7" + integrity sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g== + dependencies: + "@actions/http-client" "^2.2.0" + "@octokit/core" "^5.0.1" + "@octokit/plugin-paginate-rest" "^9.0.0" + "@octokit/plugin-rest-endpoint-methods" "^10.0.0" + "@actions/github@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.1.1.tgz#40b9b9e1323a5efcf4ff7dadd33d8ea51651bbcb" @@ -53,6 +63,14 @@ tunnel "^0.0.6" undici "^5.25.4" +"@actions/http-client@^2.2.0": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674" + integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA== + dependencies: + tunnel "^0.0.6" + undici "^5.25.4" + "@actions/io@1.1.3", "@actions/io@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71" @@ -6747,6 +6765,11 @@ dependencies: "@octokit/types" "^8.0.0" +"@octokit/auth-token@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" + integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== + "@octokit/core@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" @@ -6786,6 +6809,19 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" +"@octokit/core@^5.0.1": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.0.tgz#ddbeaefc6b44a39834e1bb2e58a49a117672a7ea" + integrity sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg== + dependencies: + "@octokit/auth-token" "^4.0.0" + "@octokit/graphql" "^7.1.0" + "@octokit/request" "^8.3.1" + "@octokit/request-error" "^5.1.0" + "@octokit/types" "^13.0.0" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + "@octokit/endpoint@^6.0.1": version "6.0.12" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" @@ -6804,6 +6840,14 @@ is-plain-object "^5.0.0" universal-user-agent "^6.0.0" +"@octokit/endpoint@^9.0.1": + version "9.0.5" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.5.tgz#e6c0ee684e307614c02fc6ac12274c50da465c44" + integrity sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw== + dependencies: + "@octokit/types" "^13.1.0" + universal-user-agent "^6.0.0" + "@octokit/graphql@^4.5.8": version "4.8.0" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" @@ -6822,6 +6866,15 @@ "@octokit/types" "^8.0.0" universal-user-agent "^6.0.0" +"@octokit/graphql@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-7.1.0.tgz#9bc1c5de92f026648131f04101cab949eeffe4e0" + integrity sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ== + dependencies: + "@octokit/request" "^8.3.0" + "@octokit/types" "^13.0.0" + universal-user-agent "^6.0.0" + "@octokit/openapi-types@^12.11.0": version "12.11.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" @@ -6842,6 +6895,16 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.0.0.tgz#f43d765b3c7533fd6fb88f3f25df079c24fccf69" integrity sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw== +"@octokit/openapi-types@^20.0.0": + version "20.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz#9ec2daa0090eeb865ee147636e0c00f73790c6e5" + integrity sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA== + +"@octokit/openapi-types@^22.2.0": + version "22.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" + integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== + "@octokit/plugin-enterprise-rest@6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" @@ -6869,11 +6932,25 @@ "@octokit/tsconfig" "^1.0.2" "@octokit/types" "^9.2.3" +"@octokit/plugin-paginate-rest@^9.0.0": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz#2e2a2f0f52c9a4b1da1a3aa17dabe3c459b9e401" + integrity sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw== + dependencies: + "@octokit/types" "^12.6.0" + "@octokit/plugin-request-log@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== +"@octokit/plugin-rest-endpoint-methods@^10.0.0": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz#41ba478a558b9f554793075b2e20cd2ef973be17" + integrity sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg== + dependencies: + "@octokit/types" "^12.6.0" + "@octokit/plugin-rest-endpoint-methods@^5.13.0": version "5.16.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz#7ee8bf586df97dd6868cf68f641354e908c25342" @@ -6915,6 +6992,15 @@ deprecation "^2.0.0" once "^1.4.0" +"@octokit/request-error@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" + integrity sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q== + dependencies: + "@octokit/types" "^13.1.0" + deprecation "^2.0.0" + once "^1.4.0" + "@octokit/request@^5.6.0", "@octokit/request@^5.6.3": version "5.6.3" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" @@ -6939,6 +7025,16 @@ node-fetch "^2.6.7" universal-user-agent "^6.0.0" +"@octokit/request@^8.3.0", "@octokit/request@^8.3.1": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.0.tgz#7f4b7b1daa3d1f48c0977ad8fffa2c18adef8974" + integrity sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw== + dependencies: + "@octokit/endpoint" "^9.0.1" + "@octokit/request-error" "^5.1.0" + "@octokit/types" "^13.1.0" + universal-user-agent "^6.0.0" + "@octokit/rest@19.0.11": version "19.0.11" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.11.tgz#2ae01634fed4bd1fca5b642767205ed3fd36177c" @@ -6971,6 +7067,20 @@ dependencies: "@octokit/openapi-types" "^18.0.0" +"@octokit/types@^12.6.0": + version "12.6.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.6.0.tgz#8100fb9eeedfe083aae66473bd97b15b62aedcb2" + integrity sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw== + dependencies: + "@octokit/openapi-types" "^20.0.0" + +"@octokit/types@^13.0.0", "@octokit/types@^13.1.0": + version "13.5.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.5.1.tgz#5685a91f295195ddfff39723b093b0df9609ce6e" + integrity sha512-F41lGiWBKPIWPBgjSvaDXTTQptBujnozENAK3S//nj7xsFdYdirImKlBB/hTjr+Vii68SM+8jG3UJWRa6DMuDA== + dependencies: + "@octokit/openapi-types" "^22.2.0" + "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": version "6.41.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04" From 5e2bc47a02ac9186520930c6aec15ca872d7f195 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:35:06 +0200 Subject: [PATCH 06/22] test(nuxt): Add E2E test for distributed tracing (#13752) Test for pageload. Tests for distributed tracing for navigations are still coming (waiting for next Nuxt version which includes the new version of ofetch with [this PR](https://github.com/unjs/ofetch/pull/377)) --- .../nuxt-3/pages/test-param/[param].vue | 22 +++++--- .../nuxt-3/tests/errors.server.test.ts | 4 +- .../nuxt-3/tests/tracing.test.ts | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue index 379e8e417b35..e83392b37b5c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue @@ -1,17 +1,23 @@ - - + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts index e9445d4c2382..d1556d511bf0 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts @@ -8,7 +8,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/fetch-server-error`); - await page.getByText('Fetch Server Data').click(); + await page.getByText('Fetch Server Data', { exact: true }).click(); const error = await errorPromise; @@ -26,7 +26,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/test-param/1234`); - await page.getByText('Fetch Server Data').click(); + await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click(); const error = await errorPromise; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts new file mode 100644 index 000000000000..46e2b135a9b7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('distributed tracing', () => { + const PARAM = 's0me-param'; + + test('capture a distributed pageload trace', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction === '/test-param/:param()'; + }); + + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction.includes('GET /test-param/'); + }); + + const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ + page.goto(`/test-param/${PARAM}`), + clientTxnEventPromise, + serverTxnEventPromise, + expect(page.getByText(`Param: ${PARAM}`)).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/test-param/:param()', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /test-param/s0me-param', // todo: parametrize (nitro) + transaction_info: { source: 'url' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.otel.http', + }, + }, + }); + + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); +}); From 2343380d18b95242a0cbf3961b6b292fc490fc3e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 26 Sep 2024 16:00:01 +0300 Subject: [PATCH 07/22] feat(node): Add `lru-memoizer` instrumentation (#13796) Resolves: #13309 Adds integration for `lru-memoizer` using [@opentelemetry/instrumentation-lru-memoizer](https://www.npmjs.com/package/@opentelemetry/instrumentation-lru-memoizer). This instrumentation does not create any spans. It only assigns the span context into memoized callbacks used in `lru-memoizer`'s `load`. Ported a test case from the original implementation and tested manually to validate. --- .../node-integration-tests/package.json | 1 + .../suites/tracing/lru-memoizer/scenario.js | 47 +++++++++++++++++++ .../suites/tracing/lru-memoizer/test.ts | 29 ++++++++++++ packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/package.json | 1 + packages/node/src/index.ts | 1 + .../node/src/integrations/tracing/index.ts | 3 ++ .../src/integrations/tracing/lrumemoizer.ts | 25 ++++++++++ yarn.lock | 29 +++++++++--- 12 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.js create mode 100644 dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts create mode 100644 packages/node/src/integrations/tracing/lrumemoizer.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 91792650a13f..ff6b5665d237 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -51,6 +51,7 @@ "http-terminator": "^3.2.0", "ioredis": "^5.4.1", "kafkajs": "2.2.4", + "lru-memoizer": "2.3.0", "mongodb": "^3.7.3", "mongodb-memory-server-global": "^7.6.3", "mongoose": "^5.13.22", diff --git a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.js b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.js new file mode 100644 index 000000000000..79f5564d1971 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.js @@ -0,0 +1,47 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const run = async () => { + // Test ported from the OTEL implementation: + // https://github.com/open-telemetry/opentelemetry-js-contrib/blob/0d6ebded313bb75b5a0e7a6422206c922daf3943/plugins/node/instrumentation-lru-memoizer/test/index.test.ts#L28 + const memoizer = require('lru-memoizer'); + + let memoizerLoadCallback; + const memoizedFoo = memoizer({ + load: (_param, callback) => { + memoizerLoadCallback = callback; + }, + hash: () => 'bar', + }); + + Sentry.startSpan({ op: 'run' }, async span => { + const outerSpanContext = span.spanContext(); + + memoizedFoo({ foo: 'bar' }, () => { + const innerContext = Sentry.getActiveSpan().spanContext(); + + // The span context should be the same as the outer span + // Throwing an error here will cause the test to fail + if (outerSpanContext !== innerContext) { + throw new Error('Outer and inner span context should match'); + } + }); + + span.end(); + }); + + // Invoking the load callback outside the span + memoizerLoadCallback(); +}; + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts new file mode 100644 index 000000000000..050505e4055e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts @@ -0,0 +1,29 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('lru-memoizer', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('keeps outer context inside the memoized inner functions', done => { + createRunner(__dirname, 'scenario.js') + // We expect only one transaction and nothing else. + // A failed test will result in an error event being sent to Sentry. + // Which will fail this suite. + .expect({ + transaction: { + transaction: '', + contexts: { + trace: expect.objectContaining({ + op: 'run', + data: expect.objectContaining({ + 'sentry.op': 'run', + 'sentry.origin': 'manual', + }), + }), + }, + }, + }) + .start(done); + }); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index bfd6886f3861..91ea79f833bc 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -72,6 +72,7 @@ export { lastEventId, linkedErrorsIntegration, localVariablesIntegration, + lruMemoizerIntegration, makeNodeTransport, metrics, modulesIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 9bbba6eeda66..7b0e05c9a48f 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -91,6 +91,7 @@ export { genericPoolIntegration, graphqlIntegration, kafkaIntegration, + lruMemoizerIntegration, mongoIntegration, mongooseIntegration, mysqlIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index ef3bcb020823..7026e6800b14 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -112,6 +112,7 @@ export { genericPoolIntegration, graphqlIntegration, kafkaIntegration, + lruMemoizerIntegration, mongoIntegration, mongooseIntegration, mysqlIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index a9d3e5025d92..c2d743eb1bf1 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -91,6 +91,7 @@ export { genericPoolIntegration, graphqlIntegration, kafkaIntegration, + lruMemoizerIntegration, mongoIntegration, mongooseIntegration, mysqlIntegration, diff --git a/packages/node/package.json b/packages/node/package.json index 1ace8a11f408..7f3b18b4590a 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -82,6 +82,7 @@ "@opentelemetry/instrumentation-ioredis": "0.43.0", "@opentelemetry/instrumentation-kafkajs": "0.3.0", "@opentelemetry/instrumentation-koa": "0.43.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.40.0", "@opentelemetry/instrumentation-mongodb": "0.47.0", "@opentelemetry/instrumentation-mongoose": "0.42.0", "@opentelemetry/instrumentation-mysql": "0.41.0", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index e97780f79ead..bc63094e2e87 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -15,6 +15,7 @@ export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } fro export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; export { graphqlIntegration } from './integrations/tracing/graphql'; export { kafkaIntegration } from './integrations/tracing/kafka'; +export { lruMemoizerIntegration } from './integrations/tracing/lrumemoizer'; export { mongoIntegration } from './integrations/tracing/mongo'; export { mongooseIntegration } from './integrations/tracing/mongoose'; export { mysqlIntegration } from './integrations/tracing/mysql'; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index cc8ef752c815..3c038b14354c 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -11,6 +11,7 @@ import { graphqlIntegration, instrumentGraphql } from './graphql'; import { hapiIntegration, instrumentHapi } from './hapi'; import { instrumentKafka, kafkaIntegration } from './kafka'; import { instrumentKoa, koaIntegration } from './koa'; +import { instrumentLruMemoizer, lruMemoizerIntegration } from './lrumemoizer'; import { instrumentMongo, mongoIntegration } from './mongo'; import { instrumentMongoose, mongooseIntegration } from './mongoose'; import { instrumentMysql, mysqlIntegration } from './mysql'; @@ -45,6 +46,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { kafkaIntegration(), dataloaderIntegration(), amqplibIntegration(), + lruMemoizerIntegration(), ]; } @@ -61,6 +63,7 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentHapi, instrumentKafka, instrumentKoa, + instrumentLruMemoizer, instrumentNest, instrumentMongo, instrumentMongoose, diff --git a/packages/node/src/integrations/tracing/lrumemoizer.ts b/packages/node/src/integrations/tracing/lrumemoizer.ts new file mode 100644 index 000000000000..d94234c3e57d --- /dev/null +++ b/packages/node/src/integrations/tracing/lrumemoizer.ts @@ -0,0 +1,25 @@ +import { LruMemoizerInstrumentation } from '@opentelemetry/instrumentation-lru-memoizer'; + +import { defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; + +const INTEGRATION_NAME = 'LruMemoizer'; + +export const instrumentLruMemoizer = generateInstrumentOnce(INTEGRATION_NAME, () => new LruMemoizerInstrumentation()); + +const _lruMemoizerIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentLruMemoizer(); + }, + }; +}) satisfies IntegrationFn; + +/** + * LruMemoizer integration + * + * Propagate traces through LruMemoizer. + */ +export const lruMemoizerIntegration = defineIntegration(_lruMemoizerIntegration); diff --git a/yarn.lock b/yarn.lock index 91c55bb33d69..ad46ff0a2882 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7321,6 +7321,13 @@ "@opentelemetry/instrumentation" "^0.53.0" "@opentelemetry/semantic-conventions" "^1.27.0" +"@opentelemetry/instrumentation-lru-memoizer@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.40.0.tgz#dc60d7fdfd2a0c681cb23e7ed4f314d1506ccdc0" + integrity sha512-21xRwZsEdMPnROu/QsaOIODmzw59IYpGFmuC4aFWvMj6stA8+Ei1tX67nkarJttlNjoM94um0N4X26AD7ff54A== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation-mongodb@0.47.0": version "0.47.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.47.0.tgz#f8107d878281433905e717f223fb4c0f10356a7b" @@ -23413,6 +23420,13 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@6.0.0, lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lru-cache@^10.0.1, lru-cache@^10.2.0: version "10.2.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" @@ -23425,13 +23439,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - lru-cache@^7.10.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: version "7.14.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" @@ -23457,6 +23464,14 @@ lru-cache@^9.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== +lru-memoizer@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.3.0.tgz#ef0fbc021bceb666794b145eefac6be49dc47f31" + integrity sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug== + dependencies: + lodash.clonedeep "^4.5.0" + lru-cache "6.0.0" + lunr@^2.3.8: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" From 0d39763ffc7ad074d57a1ee40a859c5b63c25d5c Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 26 Sep 2024 15:00:17 +0200 Subject: [PATCH 08/22] fix(types): Add jsdocs to cron types (#13776) Closes #13736 --- packages/types/src/checkin.ts | 74 +++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts index 02811295870e..9d200811183a 100644 --- a/packages/types/src/checkin.ts +++ b/packages/types/src/checkin.ts @@ -2,7 +2,7 @@ import type { TraceContext } from './context'; interface CrontabSchedule { type: 'crontab'; - // The crontab schedule string, e.g. 0 * * * *. + /** The crontab schedule string, e.g. 0 * * * *. */ value: string; } @@ -14,32 +14,37 @@ interface IntervalSchedule { type MonitorSchedule = CrontabSchedule | IntervalSchedule; -// https://develop.sentry.dev/sdk/check-ins/ export interface SerializedCheckIn { - // Check-In ID (unique and client generated). + /** Check-In ID (unique and client generated). */ check_in_id: string; - // The distinct slug of the monitor. + /** The distinct slug of the monitor. */ monitor_slug: string; - // The status of the check-in. + /** The status of the check-in. */ status: 'in_progress' | 'ok' | 'error'; - // The duration of the check-in in seconds. Will only take effect if the status is ok or error. + /** The duration of the check-in in seconds. Will only take effect if the status is ok or error. */ duration?: number; release?: string; environment?: string; monitor_config?: { schedule: MonitorSchedule; - // The allowed allowed margin of minutes after the expected check-in time that - // the monitor will not be considered missed for. + /** + * The allowed allowed margin of minutes after the expected check-in time that + * the monitor will not be considered missed for. + */ checkin_margin?: number; - // The allowed allowed duration in minutes that the monitor may be `in_progress` - // for before being considered failed due to timeout. + /** + * The allowed allowed duration in minutes that the monitor may be `in_progress` + * for before being considered failed due to timeout. + */ max_runtime?: number; - // A tz database string representing the timezone which the monitor's execution schedule is in. - // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + /** + * A tz database string representing the timezone which the monitor's execution schedule is in. + * See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + */ timezone?: string; - // How many consecutive failed check-ins it takes to create an issue. + /** How many consecutive failed check-ins it takes to create an issue. */ failure_issue_threshold?: number; - // How many consecutive OK check-ins it takes to resolve an issue. + /** How many consecutive OK check-ins it takes to resolve an issue. */ recovery_threshold?: number; }; contexts?: { @@ -48,27 +53,27 @@ export interface SerializedCheckIn { } export interface HeartbeatCheckIn { - // The distinct slug of the monitor. + /** The distinct slug of the monitor. */ monitorSlug: SerializedCheckIn['monitor_slug']; - // The status of the check-in. + /** The status of the check-in. */ status: 'ok' | 'error'; } export interface InProgressCheckIn { - // The distinct slug of the monitor. + /** The distinct slug of the monitor. */ monitorSlug: SerializedCheckIn['monitor_slug']; - // The status of the check-in. + /** The status of the check-in. */ status: 'in_progress'; } export interface FinishedCheckIn { - // The distinct slug of the monitor. + /** The distinct slug of the monitor. */ monitorSlug: SerializedCheckIn['monitor_slug']; - // The status of the check-in. + /** The status of the check-in. */ status: 'ok' | 'error'; - // Check-In ID (unique and client generated). + /** Check-In ID (unique and client generated). */ checkInId: SerializedCheckIn['check_in_id']; - // The duration of the check-in in seconds. Will only take effect if the status is ok or error. + /** The duration of the check-in in seconds. Will only take effect if the status is ok or error. */ duration?: SerializedCheckIn['duration']; } @@ -77,18 +82,27 @@ export type CheckIn = HeartbeatCheckIn | InProgressCheckIn | FinishedCheckIn; type SerializedMonitorConfig = NonNullable; export interface MonitorConfig { + /** + * The schedule on which the monitor should run. Either a crontab schedule string or an interval. + */ schedule: MonitorSchedule; - // The allowed allowed margin of minutes after the expected check-in time that - // the monitor will not be considered missed for. + /** + * The allowed allowed margin of minutes after the expected check-in time that + * the monitor will not be considered missed for. + */ checkinMargin?: SerializedMonitorConfig['checkin_margin']; - // The allowed allowed duration in minutes that the monitor may be `in_progress` - // for before being considered failed due to timeout. + /** + * The allowed allowed duration in minutes that the monitor may be `in_progress` + * for before being considered failed due to timeout. + */ maxRuntime?: SerializedMonitorConfig['max_runtime']; - // A tz database string representing the timezone which the monitor's execution schedule is in. - // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + /** + * A tz database string representing the timezone which the monitor's execution schedule is in. + * See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + */ timezone?: SerializedMonitorConfig['timezone']; - // How many consecutive failed check-ins it takes to create an issue. + /** How many consecutive failed check-ins it takes to create an issue. */ failureIssueThreshold?: SerializedMonitorConfig['failure_issue_threshold']; - // How many consecutive OK check-ins it takes to resolve an issue. + /** How many consecutive OK check-ins it takes to resolve an issue. */ recoveryThreshold?: SerializedMonitorConfig['recovery_threshold']; } From 39c0b1da3e306621bf41ddbffeb823ec49729b97 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 26 Sep 2024 16:23:38 +0200 Subject: [PATCH 09/22] fix(replay): Stop global event handling for paused replays (#13815) --- .../src/coreHandlers/handleGlobalEvent.ts | 4 ++-- .../coreHandlers/handleGlobalEvent.test.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts index a13f4d24827e..d0ea607e1c06 100644 --- a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts @@ -14,8 +14,8 @@ import { shouldSampleForBufferEvent } from './util/shouldSampleForBufferEvent'; export function handleGlobalEventListener(replay: ReplayContainer): (event: Event, hint: EventHint) => Event | null { return Object.assign( (event: Event, hint: EventHint) => { - // Do nothing if replay has been disabled - if (!replay.isEnabled()) { + // Do nothing if replay has been disabled or paused + if (!replay.isEnabled() || replay.isPaused()) { return event; } diff --git a/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts index 2c41dbfbfd62..9e888568d04d 100644 --- a/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts +++ b/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts @@ -397,4 +397,23 @@ describe('Integration | coreHandlers | handleGlobalEvent', () => { expect(handleGlobalEventListener(replay)(errorEvent, {})).toEqual(errorEvent); }); + + it('does not add replayId if replay is paused', async () => { + const transaction = Transaction(); + const error = Error(); + + replay['_isPaused'] = true; + + expect(handleGlobalEventListener(replay)(transaction, {})).toEqual( + expect.not.objectContaining({ + // no tags at all here by default + tags: expect.anything(), + }), + ); + expect(handleGlobalEventListener(replay)(error, {})).toEqual( + expect.objectContaining({ + tags: expect.not.objectContaining({ replayId: expect.anything() }), + }), + ); + }); }); From c009b0d94ad0e4dfec4f0b9e52ea3461b0215a4e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 26 Sep 2024 17:07:05 +0200 Subject: [PATCH 10/22] ci: Use release comments action from public repo (#13816) I extracted the action out into https://github.com/getsentry/release-comment-issues-gh-action. --- .github/workflows/release-comment-issues.yml | 8 +- .../.eslintrc.cjs | 14 -- .../action.yml | 12 -- .../index.mjs | 138 ------------------ .../package.json | 23 --- package.json | 1 - yarn.lock | 110 -------------- 7 files changed, 1 insertion(+), 305 deletions(-) delete mode 100644 dev-packages/release-comment-issues-gh-action/.eslintrc.cjs delete mode 100644 dev-packages/release-comment-issues-gh-action/action.yml delete mode 100644 dev-packages/release-comment-issues-gh-action/index.mjs delete mode 100644 dev-packages/release-comment-issues-gh-action/package.json diff --git a/.github/workflows/release-comment-issues.yml b/.github/workflows/release-comment-issues.yml index 80cb04eb6b54..e9d1e81b75ea 100644 --- a/.github/workflows/release-comment-issues.yml +++ b/.github/workflows/release-comment-issues.yml @@ -15,19 +15,13 @@ jobs: runs-on: ubuntu-20.04 name: 'Notify issues' steps: - - name: Check out code - uses: actions/checkout@v4 - - - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Get version id: get_version run: echo "version=${{ github.event.inputs.version || github.event.release.tag_name }}" >> $GITHUB_OUTPUT - name: Comment on linked issues that are mentioned in release if: steps.get_version.outputs.version != '' - uses: ./dev-packages/release-comment-issues-gh-action + uses: getsentry/release-comment-issues-gh-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} version: ${{ steps.get_version.outputs.version }} diff --git a/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs b/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs deleted file mode 100644 index 8c67e0037908..000000000000 --- a/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - extends: ['../../.eslintrc.js'], - parserOptions: { - sourceType: 'module', - ecmaVersion: 'latest', - }, - - overrides: [ - { - files: ['*.mjs'], - extends: ['@sentry-internal/sdk/src/base'], - }, - ], -}; diff --git a/dev-packages/release-comment-issues-gh-action/action.yml b/dev-packages/release-comment-issues-gh-action/action.yml deleted file mode 100644 index fcfb0bf00f2b..000000000000 --- a/dev-packages/release-comment-issues-gh-action/action.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: 'release-comment-issues-gh-action' -description: 'An internal Github Action to comment on related issues when a release is published.' -inputs: - github_token: - required: true - description: 'a github access token' - version: - required: true - description: 'Which version was released' -runs: - using: 'node20' - main: 'index.mjs' diff --git a/dev-packages/release-comment-issues-gh-action/index.mjs b/dev-packages/release-comment-issues-gh-action/index.mjs deleted file mode 100644 index c49ad6575ef7..000000000000 --- a/dev-packages/release-comment-issues-gh-action/index.mjs +++ /dev/null @@ -1,138 +0,0 @@ -import * as core from '@actions/core'; -import { context, getOctokit } from '@actions/github'; - -const RELEASE_COMMENT_HEADING = '## A PR closing this issue has just been released 🚀'; - -async function run() { - const { getInput } = core; - - const githubToken = getInput('github_token'); - const version = getInput('version'); - - if (!githubToken || !version) { - core.debug('Skipping because github_token or version are empty.'); - return; - } - - const { owner, repo } = context.repo; - - const octokit = getOctokit(githubToken); - - const release = await octokit.request('GET /repos/{owner}/{repo}/releases/tags/{tag}', { - owner, - repo, - tag: version, - headers: { - 'X-GitHub-Api-Version': '2022-11-28', - }, - }); - - const prNumbers = extractPrsFromReleaseBody(release.data.body, { repo, owner }); - - if (!prNumbers.length) { - core.debug('No PRs found in release body.'); - return; - } - - core.debug(`Found PRs in release body: ${prNumbers.join(', ')}`); - - const linkedIssues = await Promise.all( - prNumbers.map(prNumber => getLinkedIssuesForPr(octokit, { repo, owner, prNumber })), - ); - - for (const pr of linkedIssues) { - if (!pr.issues.length) { - core.debug(`No linked issues found for PR #${pr.prNumber}`); - continue; - } - - core.debug(`Linked issues for PR #${pr.prNumber}: ${pr.issues.map(issue => issue.number).join(',')}`); - - for (const issue of pr.issues) { - if (await hasExistingComment(octokit, { repo, owner, issueNumber: issue.number })) { - core.debug(`Comment already exists for issue #${issue.number}`); - continue; - } - - const body = `${RELEASE_COMMENT_HEADING}\n\nThis issue was referenced by PR #${pr.prNumber}, which was included in the [${version} release](https://github.com/${owner}/${repo}/releases/tag/${version}).`; - - core.debug(`Creating comment for issue #${issue.number}`); - - await octokit.rest.issues.createComment({ - repo, - owner, - issue_number: issue.number, - body, - }); - } - } -} - -/** - * - * @param {string} body - * @param {{ repo: string, owner: string}} options - * @returns {number[]} - */ -function extractPrsFromReleaseBody(body, { repo, owner }) { - const regex = new RegExp(`\\[#(\\d+)\\]\\(https:\\/\\/github\\.com\\/${owner}\\/${repo}\\/pull\\/(?:\\d+)\\)`, 'gm'); - const prNumbers = Array.from(new Set([...body.matchAll(regex)].map(match => parseInt(match[1])))); - - return prNumbers.filter(number => !!number && !Number.isNaN(number)); -} - -/** - * - * @param {ReturnType} octokit - * @param {{ repo: string, owner: string, prNumber: number}} options - * @returns {Promise<{ prNumber: number, issues: {id: string, number: number}[] }>} - */ -async function getLinkedIssuesForPr(octokit, { repo, owner, prNumber }) { - const res = await octokit.graphql( - ` -query issuesForPr($owner: String!, $repo: String!, $prNumber: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $prNumber) { - id - closingIssuesReferences (first: 50) { - edges { - node { - id - number - } - } - } - } - } -}`, - { - prNumber, - owner, - repo, - }, - ); - - const issues = res.repository?.pullRequest?.closingIssuesReferences.edges.map(edge => edge.node); - return { - prNumber, - issues, - }; -} - -/** - * - * @param {ReturnType} octokit - * @param {{ repo: string, owner: string, issueNumber: number}} options - * @returns {Promise} - */ -async function hasExistingComment(octokit, { repo, owner, issueNumber }) { - const { data: commentList } = await octokit.rest.issues.listComments({ - repo, - owner, - issue_number: issueNumber, - }); - - return commentList.some(comment => comment.body.startsWith(RELEASE_COMMENT_HEADING)); -} - -run(); diff --git a/dev-packages/release-comment-issues-gh-action/package.json b/dev-packages/release-comment-issues-gh-action/package.json deleted file mode 100644 index 49c8f2ad5caa..000000000000 --- a/dev-packages/release-comment-issues-gh-action/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@sentry-internal/release-comment-issues-gh-action", - "description": "An internal Github Action to comment on related issues when a release is published.", - "version": "8.31.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "private": true, - "main": "index.mjs", - "type": "module", - "scripts": { - "lint": "eslint . --format stylish", - "fix": "eslint . --format stylish --fix" - }, - "dependencies": { - "@actions/core": "1.10.1", - "@actions/github": "6.0.0" - }, - "volta": { - "extends": "../../package.json" - } -} diff --git a/package.json b/package.json index b17092f04a80..365e1eb13922 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,6 @@ "dev-packages/size-limit-gh-action", "dev-packages/clear-cache-gh-action", "dev-packages/external-contributor-gh-action", - "dev-packages/release-comment-issues-gh-action", "dev-packages/rollup-utils" ], "devDependencies": { diff --git a/yarn.lock b/yarn.lock index ad46ff0a2882..e19f0aac72e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,16 +27,6 @@ dependencies: "@actions/io" "^1.0.1" -"@actions/github@6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@actions/github/-/github-6.0.0.tgz#65883433f9d81521b782a64cc1fd45eef2191ea7" - integrity sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g== - dependencies: - "@actions/http-client" "^2.2.0" - "@octokit/core" "^5.0.1" - "@octokit/plugin-paginate-rest" "^9.0.0" - "@octokit/plugin-rest-endpoint-methods" "^10.0.0" - "@actions/github@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.1.1.tgz#40b9b9e1323a5efcf4ff7dadd33d8ea51651bbcb" @@ -63,14 +53,6 @@ tunnel "^0.0.6" undici "^5.25.4" -"@actions/http-client@^2.2.0": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674" - integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA== - dependencies: - tunnel "^0.0.6" - undici "^5.25.4" - "@actions/io@1.1.3", "@actions/io@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71" @@ -6765,11 +6747,6 @@ dependencies: "@octokit/types" "^8.0.0" -"@octokit/auth-token@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" - integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== - "@octokit/core@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" @@ -6809,19 +6786,6 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" -"@octokit/core@^5.0.1": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.0.tgz#ddbeaefc6b44a39834e1bb2e58a49a117672a7ea" - integrity sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg== - dependencies: - "@octokit/auth-token" "^4.0.0" - "@octokit/graphql" "^7.1.0" - "@octokit/request" "^8.3.1" - "@octokit/request-error" "^5.1.0" - "@octokit/types" "^13.0.0" - before-after-hook "^2.2.0" - universal-user-agent "^6.0.0" - "@octokit/endpoint@^6.0.1": version "6.0.12" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" @@ -6840,14 +6804,6 @@ is-plain-object "^5.0.0" universal-user-agent "^6.0.0" -"@octokit/endpoint@^9.0.1": - version "9.0.5" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.5.tgz#e6c0ee684e307614c02fc6ac12274c50da465c44" - integrity sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw== - dependencies: - "@octokit/types" "^13.1.0" - universal-user-agent "^6.0.0" - "@octokit/graphql@^4.5.8": version "4.8.0" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" @@ -6866,15 +6822,6 @@ "@octokit/types" "^8.0.0" universal-user-agent "^6.0.0" -"@octokit/graphql@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-7.1.0.tgz#9bc1c5de92f026648131f04101cab949eeffe4e0" - integrity sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ== - dependencies: - "@octokit/request" "^8.3.0" - "@octokit/types" "^13.0.0" - universal-user-agent "^6.0.0" - "@octokit/openapi-types@^12.11.0": version "12.11.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" @@ -6895,16 +6842,6 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.0.0.tgz#f43d765b3c7533fd6fb88f3f25df079c24fccf69" integrity sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw== -"@octokit/openapi-types@^20.0.0": - version "20.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz#9ec2daa0090eeb865ee147636e0c00f73790c6e5" - integrity sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA== - -"@octokit/openapi-types@^22.2.0": - version "22.2.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" - integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== - "@octokit/plugin-enterprise-rest@6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" @@ -6932,25 +6869,11 @@ "@octokit/tsconfig" "^1.0.2" "@octokit/types" "^9.2.3" -"@octokit/plugin-paginate-rest@^9.0.0": - version "9.2.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz#2e2a2f0f52c9a4b1da1a3aa17dabe3c459b9e401" - integrity sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw== - dependencies: - "@octokit/types" "^12.6.0" - "@octokit/plugin-request-log@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== -"@octokit/plugin-rest-endpoint-methods@^10.0.0": - version "10.4.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz#41ba478a558b9f554793075b2e20cd2ef973be17" - integrity sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg== - dependencies: - "@octokit/types" "^12.6.0" - "@octokit/plugin-rest-endpoint-methods@^5.13.0": version "5.16.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz#7ee8bf586df97dd6868cf68f641354e908c25342" @@ -6992,15 +6915,6 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request-error@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" - integrity sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q== - dependencies: - "@octokit/types" "^13.1.0" - deprecation "^2.0.0" - once "^1.4.0" - "@octokit/request@^5.6.0", "@octokit/request@^5.6.3": version "5.6.3" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" @@ -7025,16 +6939,6 @@ node-fetch "^2.6.7" universal-user-agent "^6.0.0" -"@octokit/request@^8.3.0", "@octokit/request@^8.3.1": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.0.tgz#7f4b7b1daa3d1f48c0977ad8fffa2c18adef8974" - integrity sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw== - dependencies: - "@octokit/endpoint" "^9.0.1" - "@octokit/request-error" "^5.1.0" - "@octokit/types" "^13.1.0" - universal-user-agent "^6.0.0" - "@octokit/rest@19.0.11": version "19.0.11" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.11.tgz#2ae01634fed4bd1fca5b642767205ed3fd36177c" @@ -7067,20 +6971,6 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@octokit/types@^12.6.0": - version "12.6.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.6.0.tgz#8100fb9eeedfe083aae66473bd97b15b62aedcb2" - integrity sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw== - dependencies: - "@octokit/openapi-types" "^20.0.0" - -"@octokit/types@^13.0.0", "@octokit/types@^13.1.0": - version "13.5.1" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.5.1.tgz#5685a91f295195ddfff39723b093b0df9609ce6e" - integrity sha512-F41lGiWBKPIWPBgjSvaDXTTQptBujnozENAK3S//nj7xsFdYdirImKlBB/hTjr+Vii68SM+8jG3UJWRa6DMuDA== - dependencies: - "@octokit/openapi-types" "^22.2.0" - "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": version "6.41.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04" From 2f53df76a88f984e671c7b24d9aea9268b2e2f88 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 26 Sep 2024 18:01:09 +0200 Subject: [PATCH 11/22] fix(core): Don't return trace data in `getTraceData` and `getTraceMetaTags` if SDK is disabled (#13760) Ensures that we don't return trace data from the new `getTraceData` and `getTraceMetaTags` APIs if the SDK is disabled. Add more tests to ensure this is now properly covered, along with a test for behaviour with `tracesSampleRate: 0`. --- .../tracing/meta-tags/server-sdk-disabled.js | 34 +++++++++++++ .../meta-tags/server-tracesSampleRate-zero.js | 33 ++++++++++++ .../suites/tracing/meta-tags/test.ts | 51 ++++++++++++++++++- packages/core/src/utils/traceData.ts | 5 ++ .../core/test/lib/utils/traceData.test.ts | 17 +++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js create mode 100644 dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js new file mode 100644 index 000000000000..3ad43cae7647 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js @@ -0,0 +1,34 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + transport: loggingTransport, + enabled: false, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.get('/test', (_req, res) => { + res.send({ + response: ` + + + ${Sentry.getTraceMetaTags()} + + + Hi :) + + + `, + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js new file mode 100644 index 000000000000..31db69722c3a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js @@ -0,0 +1,33 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 0, + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.get('/test', (_req, res) => { + res.send({ + response: ` + + + ${Sentry.getTraceMetaTags()} + + + Hi :) + + + `, + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts b/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts index c42269dd8504..ab63b1c9cb35 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts @@ -5,7 +5,7 @@ describe('getTraceMetaTags', () => { cleanupChildProcesses(); }); - test('injects sentry tracing tags', async () => { + test('injects tags with trace from incoming headers', async () => { const traceId = 'cd7ee7a6fe3ebe7ab9c3271559bc203c'; const parentSpanId = '100ff0980e7a4ead'; @@ -22,4 +22,53 @@ describe('getTraceMetaTags', () => { expect(html).toMatch(//); expect(html).toContain(''); }); + + test('injects tags with new trace if no incoming headers', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + + const response = await runner.makeRequest('get', '/test'); + + // @ts-ignore - response is defined, types just don't reflect it + const html = response?.response as unknown as string; + + const traceId = html.match(//)?.[1]; + expect(traceId).not.toBeUndefined(); + + expect(html).toContain(' tags with negative sampling decision if tracesSampleRate is 0', async () => { + const runner = createRunner(__dirname, 'server-tracesSampleRate-zero.js').start(); + + const response = await runner.makeRequest('get', '/test'); + + // @ts-ignore - response is defined, types just don't reflect it + const html = response?.response as unknown as string; + + const traceId = html.match(//)?.[1]; + expect(traceId).not.toBeUndefined(); + + expect(html).toContain(' tags if SDK is disabled", async () => { + const traceId = 'cd7ee7a6fe3ebe7ab9c3271559bc203c'; + const parentSpanId = '100ff0980e7a4ead'; + + const runner = createRunner(__dirname, 'server-sdk-disabled.js').start(); + + const response = await runner.makeRequest('get', '/test', { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: 'sentry-environment=production', + }); + + // @ts-ignore - response is defined, types just don't reflect it + const html = response?.response as unknown as string; + + expect(html).not.toContain('"sentry-trace"'); + expect(html).not.toContain('"baggage"'); + }); }); diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index 831e8187996e..c56c8f71ba1d 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -8,6 +8,7 @@ import { import { getAsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../carrier'; import { getClient, getCurrentScope } from '../currentScopes'; +import { isEnabled } from '../exports'; import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from '../tracing'; import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils'; @@ -23,6 +24,10 @@ import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils'; * or meta tag name. */ export function getTraceData(): SerializedTraceData { + if (!isEnabled()) { + return {}; + } + const carrier = getMainCarrier(); const acs = getAsyncContextStrategy(carrier); if (acs.getTraceData) { diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index a6fb3c57814e..aa6d2497dd54 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -1,5 +1,6 @@ import { SentrySpan, getTraceData } from '../../../src/'; import * as SentryCoreCurrentScopes from '../../../src/currentScopes'; +import * as SentryCoreExports from '../../../src/exports'; import * as SentryCoreTracing from '../../../src/tracing'; import * as SentryCoreSpanUtils from '../../../src/utils/spanUtils'; @@ -22,6 +23,14 @@ const mockedScope = { } as any; describe('getTraceData', () => { + beforeEach(() => { + jest.spyOn(SentryCoreExports, 'isEnabled').mockReturnValue(true); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('returns the tracing data from the span, if a span is available', () => { { jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ @@ -139,6 +148,14 @@ describe('getTraceData', () => { expect(traceData).toEqual({}); }); + + it('returns an empty object if the SDK is disabled', () => { + jest.spyOn(SentryCoreExports, 'isEnabled').mockReturnValueOnce(false); + + const traceData = getTraceData(); + + expect(traceData).toEqual({}); + }); }); describe('isValidBaggageString', () => { From 418584fdcf2c594b02068e86b0ca9d9142cf00dc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Sat, 28 Sep 2024 04:39:46 +0200 Subject: [PATCH 12/22] test(e2e/solidstart): Skip hydration error to unblock CI (#13824) CI is currently blocked because a test in our solidStart e2e test app is failing. It seems like for some reason, when first accessing the `/error-boundary` route, a hydration error is thrown that's caught by the set error boundary. I didn't get to the bottom of why this hydration error is being thrown but at least we can work around it by simply reloading the page before triggering the sample error. This PR does exactly that. We should follow up with a proper fix for this (cc @andreiborza when you get a chance) --- .../test-applications/solidstart/tests/errorboundary.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts index b709760aab94..b6164d541b93 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts @@ -10,6 +10,7 @@ test('captures an exception', async ({ page }) => { ); }); + await page.goto('/error-boundary'); await page.goto('/error-boundary'); await page.locator('#caughtErrorBtn').click(); const errorEvent = await errorEventPromise; @@ -40,6 +41,7 @@ test('captures a second exception after resetting the boundary', async ({ page } ); }); + await page.goto('/error-boundary'); await page.goto('/error-boundary'); await page.locator('#caughtErrorBtn').click(); const firstErrorEvent = await firstErrorEventPromise; From 7937308a948c6df97587d843344af5a328e1ba5e Mon Sep 17 00:00:00 2001 From: "Joshua J." <31038284+joshuajaco@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:29:47 +0200 Subject: [PATCH 13/22] Loosen @sentry/nextjs webpack peer dependency (#13826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The peer dependency introduced in https://github.com/getsentry/sentry-javascript/pull/13571 seems to be too strict. After installing the latest next version (14.2.13) and @sentry/next version (8.32.0), I get the following: ```sh └─┬ @sentry/nextjs 8.32.0 └── ✕ unmet peer webpack@5.94.0: found 5.95.0 ``` --------- Co-authored-by: Luca Forstner --- packages/nextjs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 6e86613b5bd2..cd5ef68f077d 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -92,7 +92,7 @@ }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0", - "webpack": "5.94.0" + "webpack": ">=5.0.0" }, "peerDependenciesMeta": { "webpack": { From 2ee168750ccfa28b418e72161bb9d95f37064d5b Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 30 Sep 2024 11:59:34 +0200 Subject: [PATCH 14/22] test: Run Next.js canary tests on PRs (#13829) --- .github/workflows/build.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67d956b620be..2711cd26ffcc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1067,6 +1067,10 @@ jobs: 'react-send-to-sentry', 'node-express-send-to-sentry', 'debug-id-sourcemaps', + 'nextjs-app-dir', + 'nextjs-13', + 'nextjs-14', + 'nextjs-15', ] build-command: - false @@ -1081,6 +1085,30 @@ jobs: - test-application: 'create-remix-app-legacy' assert-command: 'test:assert-sourcemaps' label: 'create-remix-app-legacy (sourcemaps)' + - test-application: 'nextjs-app-dir' + build-command: 'test:build-canary' + label: 'nextjs-app-dir (canary)' + - test-application: 'nextjs-app-dir' + build-command: 'test:build-latest' + label: 'nextjs-app-dir (latest)' + - test-application: 'nextjs-13' + build-command: 'test:build-canary' + label: 'nextjs-13 (canary)' + - test-application: 'nextjs-13' + build-command: 'test:build-latest' + label: 'nextjs-13 (latest)' + - test-application: 'nextjs-14' + build-command: 'test:build-canary' + label: 'nextjs-14 (canary)' + - test-application: 'nextjs-14' + build-command: 'test:build-latest' + label: 'nextjs-14 (latest)' + - test-application: 'nextjs-15' + build-command: 'test:build-canary' + label: 'nextjs-15 (canary)' + - test-application: 'nextjs-15' + build-command: 'test:build-latest' + label: 'nextjs-15 (latest)' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) From ca19f344998b1cbee8655534cc3cd3fe55ff6db2 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 30 Sep 2024 14:17:31 +0200 Subject: [PATCH 15/22] feat(nextjs): Support new async APIs (`headers()`, `params`, `searchParams`) (#13828) Changes in Next.js https://github.com/vercel/next.js/pull/68812 This PR is mostly just adjusting our E2E tests so they don't fail while building. Additionally, we had to update the `withServerActionInstrumentation` API in a semver-minor way so you can pass a promise to the `headers` option. The `ReadonlyHeaders` type isn't exposed in all Next.js versions so for now I typed it as `any`. Resolves https://github.com/getsentry/sentry-javascript/issues/13805 Resolves https://github.com/getsentry/sentry-javascript/issues/13779 Resolves https://github.com/getsentry/sentry-javascript/issues/13780 --- .../app/generation-functions/page.tsx | 26 +++++++++---------- .../nextjs-14/app/propagation/utils.ts | 4 +-- .../nextjs-15/app/ppr-error/[param]/page.tsx | 7 +++-- .../parameter/[...parameters]/page.tsx | 8 ++++-- .../parameter/[parameter]/page.tsx | 8 ++++-- .../parameter/[...parameters]/page.tsx | 8 ++++-- .../parameter/[parameter]/page.tsx | 8 ++++-- .../common/withServerActionInstrumentation.ts | 22 ++++++++++++---- 8 files changed, 60 insertions(+), 31 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx index 92bee1dbac4b..0d8f1841ea9d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx @@ -7,33 +7,31 @@ export default function Page() { return

Hello World!

; } -export async function generateMetadata({ - searchParams, -}: { - searchParams: { [key: string]: string | string[] | undefined }; -}) { +export async function generateMetadata({ searchParams }: { searchParams: any }) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedSearchParams = await searchParams; + Sentry.setTag('my-isolated-tag', true); Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope - if (searchParams['shouldThrowInGenerateMetadata']) { + if (normalizedSearchParams['shouldThrowInGenerateMetadata']) { throw new Error('generateMetadata Error'); } return { - title: searchParams['metadataTitle'] ?? 'not set', + title: normalizedSearchParams['metadataTitle'] ?? 'not set', }; } -export function generateViewport({ - searchParams, -}: { - searchParams: { [key: string]: string | undefined }; -}) { - if (searchParams['shouldThrowInGenerateViewport']) { +export async function generateViewport({ searchParams }: { searchParams: any }) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedSearchParams = await searchParams; + + if (normalizedSearchParams['shouldThrowInGenerateViewport']) { throw new Error('generateViewport Error'); } return { - themeColor: searchParams['viewportThemeColor'] ?? 'black', + themeColor: normalizedSearchParams['viewportThemeColor'] ?? 'black', }; } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts index a065c53ee4c9..249efabe58f3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts @@ -26,8 +26,8 @@ export function makeHttpRequest(url: string) { }); } -export function checkHandler() { - const headerList = headers(); +export async function checkHandler() { + const headerList = await headers(); const headerObj: Record = {}; headerList.forEach((value, key) => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx index ec2b2b1232c7..c67513e0e4fd 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx @@ -3,10 +3,13 @@ import * as Sentry from '@sentry/nextjs'; export default async function Page({ searchParams, }: { - searchParams: { id?: string }; + searchParams: any; }) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedSearchParams = await searchParams; + try { - console.log(searchParams.id); // Accessing a field on searchParams will throw the PPR error + console.log(normalizedSearchParams.id); // Accessing a field on searchParams will throw the PPR error } catch (e) { Sentry.captureException(e); // This error should not be reported await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for any async event processors to run diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx index 31fa4ee21be5..f09f06875c3a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx @@ -1,10 +1,14 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; -export default function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/client-component/[...parameters])

-

Params: {JSON.stringify(params['parameters'])}

+

Params: {JSON.stringify(normalizedParams['parameters'])}

); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx index 2b9c28b922ac..514a0833c998 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx @@ -1,10 +1,14 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; -export default function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/client-component/[parameter])

-

Parameter: {JSON.stringify(params['parameter'])}

+

Parameter: {JSON.stringify(normalizedParams['parameter'])}

); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx index 5d9d6c8262c5..63d0e7b53f0b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx @@ -1,12 +1,16 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; export const dynamic = 'force-dynamic'; -export default async function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/server-component/[...parameters])

-

Params: {JSON.stringify(params['parameters'])}

+

Params: {JSON.stringify(normalizedParams['parameters'])}

); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx index f88fe1cd4a06..98ecb7352ad2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx @@ -1,12 +1,16 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; export const dynamic = 'force-dynamic'; -export default async function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/server-component/[parameter])

-

Parameter: {JSON.stringify(params['parameter'])}

+

Parameter: {JSON.stringify(normalizedParams['parameter'])}

); diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 14c701638ee5..c1633d8fab1b 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -15,7 +15,18 @@ import { vercelWaitUntil } from './utils/vercelWaitUntil'; interface Options { formData?: FormData; - headers?: Headers; + + /** + * Headers as returned from `headers()`. + * + * Currently accepts both a plain `Headers` object and `Promise` to be compatible with async APIs introduced in Next.js 15: https://github.com/vercel/next.js/pull/68812 + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + headers?: Headers | Promise; + + /** + * Whether the server action response should be included in any events captured within the server action. + */ recordResponse?: boolean; } @@ -55,16 +66,17 @@ async function withServerActionInstrumentationImplementation> { return escapeNextjsTracing(() => { - return withIsolationScope(isolationScope => { + return withIsolationScope(async isolationScope => { const sendDefaultPii = getClient()?.getOptions().sendDefaultPii; let sentryTraceHeader; let baggageHeader; const fullHeadersObject: Record = {}; try { - sentryTraceHeader = options.headers?.get('sentry-trace') ?? undefined; - baggageHeader = options.headers?.get('baggage'); - options.headers?.forEach((value, key) => { + const awaitedHeaders: Headers = await options.headers; + sentryTraceHeader = awaitedHeaders?.get('sentry-trace') ?? undefined; + baggageHeader = awaitedHeaders?.get('baggage'); + awaitedHeaders?.forEach((value, key) => { fullHeadersObject[key] = value; }); } catch (e) { From b4b08cd252fc244f02791f8e5f4b8527d76eae26 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 1 Oct 2024 09:32:51 +0200 Subject: [PATCH 16/22] fix(core): Adapt trpc middleware input attachment (#13831) --- .github/workflows/build.yml | 1 + .../test-applications/nextjs-t3/.gitignore | 45 ++++++++++ .../test-applications/nextjs-t3/.npmrc | 2 + .../test-applications/nextjs-t3/next-env.d.ts | 5 ++ .../nextjs-t3/next.config.js | 11 +++ .../test-applications/nextjs-t3/package.json | 54 ++++++++++++ .../nextjs-t3/playwright.config.mjs | 19 +++++ .../nextjs-t3/postcss.config.cjs | 7 ++ .../nextjs-t3/public/favicon.ico | Bin 0 -> 15406 bytes .../nextjs-t3/sentry.client.config.ts | 8 ++ .../nextjs-t3/sentry.edge.config.ts | 13 +++ .../nextjs-t3/sentry.server.config.ts | 8 ++ .../nextjs-t3/src/app/_components/post.tsx | 75 +++++++++++++++++ .../src/app/api/trpc/[trpc]/route.ts | 32 ++++++++ .../nextjs-t3/src/app/global-error.tsx | 27 ++++++ .../nextjs-t3/src/app/layout.tsx | 22 +++++ .../nextjs-t3/src/app/page.tsx | 49 +++++++++++ .../test-applications/nextjs-t3/src/env.js | 40 +++++++++ .../nextjs-t3/src/instrumentation.ts | 13 +++ .../nextjs-t3/src/server/api/root.ts | 23 ++++++ .../nextjs-t3/src/server/api/routers/post.ts | 39 +++++++++ .../nextjs-t3/src/server/api/trpc.ts | 77 ++++++++++++++++++ .../nextjs-t3/src/styles/globals.css | 3 + .../nextjs-t3/src/trpc/query-client.ts | 20 +++++ .../nextjs-t3/src/trpc/react.tsx | 75 +++++++++++++++++ .../nextjs-t3/src/trpc/server.ts | 27 ++++++ .../nextjs-t3/start-event-proxy.mjs | 6 ++ .../nextjs-t3/tailwind.config.ts | 14 ++++ .../nextjs-t3/tests/trpc-error.test.ts | 34 ++++++++ .../nextjs-t3/tests/trpc-mutation.test.ts | 19 +++++ .../test-applications/nextjs-t3/tsconfig.json | 42 ++++++++++ .../test-applications/node-express/src/app.ts | 4 +- packages/core/src/trpc.ts | 72 ++++++++-------- 33 files changed, 847 insertions(+), 39 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/next-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/next.config.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/postcss.config.cjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.edge.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.server.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/_components/post.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/api/trpc/[trpc]/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/global-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/env.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/instrumentation.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/root.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/routers/post.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/trpc.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/styles/globals.css create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/query-client.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/react.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/server.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-error.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-mutation.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-t3/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2711cd26ffcc..8a45f7de410c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -902,6 +902,7 @@ jobs: 'nextjs-13', 'nextjs-14', 'nextjs-15', + 'nextjs-t3', 'react-17', 'react-19', 'react-create-hash-router', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-t3/.gitignore new file mode 100644 index 000000000000..e799cc33c4e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +!*.d.ts + +# Sentry +.sentryclirc + +.vscode + +test-results diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-t3/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/.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/nextjs-t3/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/next-env.d.ts new file mode 100644 index 000000000000..40c3d68096c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-t3/next.config.js new file mode 100644 index 000000000000..b22141b67893 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/next.config.js @@ -0,0 +1,11 @@ +await import('./src/env.js'); + +/** @type {import("next").NextConfig} */ +const config = {}; + +import { withSentryConfig } from '@sentry/nextjs'; + +export default withSentryConfig(config, { + disableLogger: true, + silent: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json new file mode 100644 index 000000000000..d5c3a9d20f0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json @@ -0,0 +1,54 @@ +{ + "name": "t3", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "next build", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@rc && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod && pnpm test:dev" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@t3-oss/env-nextjs": "^0.10.1", + "@tanstack/react-query": "^5.50.0", + "@trpc/client": "^11.0.0-rc.446", + "@trpc/react-query": "^11.0.0-rc.446", + "@trpc/server": "^11.0.0-rc.446", + "geist": "^1.3.0", + "next": "^14.2.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "server-only": "^0.0.1", + "superjson": "^2.2.1", + "zod": "^3.23.3" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/eslint": "^8.56.10", + "@types/node": "^20.14.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.4", + "postcss": "^8.4.39", + "prettier": "^3.3.2", + "prettier-plugin-tailwindcss": "^0.6.5", + "tailwindcss": "^3.4.3", + "typescript": "^5.5.3" + }, + "ct3aMetadata": { + "initVersion": "7.37.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-t3/playwright.config.mjs new file mode 100644 index 000000000000..8448829443d6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/playwright.config.mjs @@ -0,0 +1,19 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const config = getPlaywrightConfig( + { + startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + port: 3030, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/postcss.config.cjs b/dev-packages/e2e-tests/test-applications/nextjs-t3/postcss.config.cjs new file mode 100644 index 000000000000..4cdb2f430f8e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/postcss.config.cjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +module.exports = config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/public/favicon.ico b/dev-packages/e2e-tests/test-applications/nextjs-t3/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..60c702aac13409c82c4040f8fee9603eb84aa10c GIT binary patch literal 15406 zcmeHOX^b4j6>hH&u3!wtgoMM(Da<7x5Rn8Tjvq)?zybua1c*c20{$U^Aj%>HD9R-z zBuWB;ARB>eYK+y}Dk#s*$8Q(p+iLA_;N7bn84x`l%#I{rywlCmbkAPayBK)27Rhm!$WXNYV+Q zK^4@P%189Q__>DsD^FL?7r_P=JJvIlKl+CJ62dyd9WqHP5Sp7xG3? zPX~|xApI@EB+@r<8Zj2@M^QA-H<*IF*CS2am*|i;*E8hDd|es0ZRN*eT}q4f={qph zUyoWUdZ+R8ip52QA+%;l|Sa2^7!PrYAH1GAw4nmfKx zy1+M;Td^MB<(cfVs$m?`u57IHH)D=|NWm@3zi7tCFgENLSn7k=Pdv~@u`r2!VJ-Hn z5cXiS66w9>LT56iNCfU;)=ma= z`c9MYdEO$lXDjiAZma0~qmy`0Ud=wxm3KG>+B=)kiuq~siOx6F`)WK*<@aK}q_nFk z=W_Xo|D8k=&&!fe^n@YJ_ToIDgFft$u(^~7d^hv_v^bCawEFQf^jDeWqrcR6S<-hm zzt3;Z#bYA(s$u7s3+5#DVP-&b)99=$<^0;j3 zcc)NTm?l#!%OgJ;80Z7t%UlN9>UibS>6}kssAr=rpgqWS-2-@jo;Z(u;uA5p57xgo zI0neFT|+sU%cxe{+k^AUuVIL^eX;Kh)-j;ZL#;@rXxqP5TdFpnH#&uyA7>w*DZVL^MIqKm_{4qbT z46X9@-3V1(K8eflW%)qJ|3tvB*;bOy=By;paJ=otl~IG8!ZC!Zx){7a=ln4@zlz(V z7_;4!AJJMDRWX`AY2VT|#s$X@PdzK-T;Cwj zne8F?PcC3MKk<6qh;i;b3A`O4yiT@P9^OMkLknOd-T*uDYt$b@NVA1^;H+n%Evu7k z>pb$3Xk0dOYE15jHpV~_t(ZqPKm3n>Lf!4L`e|*bmEqhbhbHxN@*R{YCoA0!{t9D< z0rS(%aWflbbWbUxZ)$$W8ML~>yt1+aZJ3*dF|E8+{1N%xP5HMN-`hk?7#G{oA8!yQ zItP22wv^7IzFj^8bPprM{pCAx9^42%$E4xQDr*&g=#+mBoDDzCl#kAa&+K;Sa(**; z)E3lx6Km5h?MAzv?PMIaf}i>te(+aCy+em3%;8I#;TH2vtP6w}Vahi-HQy%#tysSg z73uS&TftwgXv=7vaQstao88lj{;Ha`Y{og-R9*p(zC3v2G_ByLx>&>Sg}!UPZM37{ z>Basy&#Z6gV1VnO7GpiK)nUBcX#LkJemal)mUNRv0cJMfcj3f=Dz^j|@H+FCaH(7$6r#>64S<{vsG@JOVy1oSK4xI(+V+VV|j!MdVwyb&3DSn!Z zW3G1OR$D#3*?L50mMZRep!apV>Ydsl|2+$1T6rh6M@FW$H2ME+kz+>T7Wl^_o9vCD6fxs zw5dex9l)Jn7RI#lcJW8_p3YR>!}x930X2N`de0kKO7H%-T=dC&PcQue_TD(`)d{Ti zpVECPFYhF77eC3Qa|(3&+O+N)x;5nYT$7zD;-a%M-T>Z>_WovzZD*cO#ky(fPVm!M z4>50XE?F;*jp_Cb#^0xcejdVtG(4@Ab%LME8grb(VULnMQ(qTr?XlS4sA=Z%WpG}t z#@)bAGHE<}Ce}x+=VD)Aj=U1KY1`*%OSkaTSaNZniT#y)LG`(S^d#o(f0N$S=E0Xm z69nua-)1-RfN`**lRLwjZKf-mfScCTMmtQlmSkn&*%Qh=t8_ZBe~{3IHMCdn2^iBb zU@Z0dnsO%gtl?N6Y{&Y!ir&Ac*2mk5ac|mxL_VZh2;?)RIUwSqJpgNKaYjEF@;@WI zV;5<~G|ty}hr~8c`6=kme>Q^rmKXb<0b#1W2{PGdGuxm%Zdt`cMch0MyXbn%Lwe)X zm_Ofrn*7V_#@MFAI1Y+wEV-6^4ls%5b>Nb>VQu|ugs~#hQ+hYypVk$7do)3>4&JN5 z*Qv&IioJq8V&L9GYy;NYm7v3!Od*?f)&p$Ie z=7xjyDDv&@jzB)Sqc--SY9FM2yJ2IM#O`-=V2OZPO;(?CxHJq`3Uu%~L^f2g^2 A#Q*>R literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts new file mode 100644 index 000000000000..0e3121a8f01b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + debug: false, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.edge.config.ts new file mode 100644 index 000000000000..4f1cb3e93e9c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.edge.config.ts @@ -0,0 +1,13 @@ +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.server.config.ts new file mode 100644 index 000000000000..ad780407a5b7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.server.config.ts @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/_components/post.tsx b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/_components/post.tsx new file mode 100644 index 000000000000..0b1c6dcf367b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/_components/post.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { useState } from 'react'; + +import { api } from '~/trpc/react'; + +export function LatestPost() { + const [latestPost] = api.post.getLatest.useSuspenseQuery(); + + const utils = api.useUtils(); + const [name, setName] = useState(''); + const createPost = api.post.create.useMutation({ + onSuccess: async () => { + await utils.post.invalidate(); + setName(''); + }, + }); + + const throwingMutation = api.post.throwError.useMutation({ + onSuccess: async () => { + await utils.post.invalidate(); + setName(''); + }, + }); + + return ( +
+ {latestPost ? ( +

Your most recent post: {latestPost.name}

+ ) : ( +

You have no posts yet.

+ )} +
{ + e.preventDefault(); + createPost.mutate({ name }); + }} + className="flex flex-col gap-2" + > + setName(e.target.value)} + id="createInput" + className="w-full rounded-full px-4 py-2 text-black" + /> + +
+
{ + e.preventDefault(); + throwingMutation.mutate({ name: 'I love dogs' }); + }} + className="flex flex-col gap-2" + > + +
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/api/trpc/[trpc]/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 000000000000..5756411c583e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,32 @@ +import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; +import { type NextRequest } from 'next/server'; + +import { env } from '~/env'; +import { appRouter } from '~/server/api/root'; +import { createTRPCContext } from '~/server/api/trpc'; + +/** + * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when + * handling a HTTP request (e.g. when you make requests from Client Components). + */ +const createContext = async (req: NextRequest) => { + return createTRPCContext({ + headers: req.headers, + }); +}; + +const handler = (req: NextRequest) => + fetchRequestHandler({ + endpoint: '/api/trpc', + req, + router: appRouter, + createContext: () => createContext(req), + onError: + env.NODE_ENV === 'development' + ? ({ path, error }) => { + console.error(`❌ tRPC failed on ${path ?? ''}: ${error.message}`); + } + : undefined, + }); + +export { handler as GET, handler as POST }; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/global-error.tsx new file mode 100644 index 000000000000..912ad3606a61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/global-error.tsx @@ -0,0 +1,27 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import NextError from 'next/error'; +import { useEffect } from 'react'; + +export default function GlobalError({ + error, +}: { + error: Error & { digest?: string }; +}) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx new file mode 100644 index 000000000000..e703260be1a3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx @@ -0,0 +1,22 @@ +import '~/styles/globals.css'; + +import { GeistSans } from 'geist/font/sans'; +import { type Metadata } from 'next'; + +import { TRPCReactProvider } from '~/trpc/react'; + +export const metadata: Metadata = { + title: 'Create T3 App', + description: 'Generated by create-t3-app', + icons: [{ rel: 'icon', url: '/favicon.ico' }], +}; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/page.tsx new file mode 100644 index 000000000000..f8e261c98c34 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/page.tsx @@ -0,0 +1,49 @@ +import Link from 'next/link'; + +import { LatestPost } from '~/app/_components/post'; +import { HydrateClient, api } from '~/trpc/server'; + +export default async function Home() { + const hello = await api.post.hello({ text: 'from tRPC' }); + + void api.post.getLatest.prefetch(); + + return ( + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to deploy it. +
+ +
+
+

{hello ? hello.greeting : 'Loading tRPC query...'}

+
+ + +
+
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/env.js b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/env.js new file mode 100644 index 000000000000..8c66c421c7ec --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/env.js @@ -0,0 +1,40 @@ +import { createEnv } from '@t3-oss/env-nextjs'; +import { z } from 'zod'; + +export const env = createEnv({ + /** + * Specify your server-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. + */ + server: { + NODE_ENV: z.enum(['development', 'test', 'production']), + }, + + /** + * Specify your client-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. To expose them to the client, prefix them with + * `NEXT_PUBLIC_`. + */ + client: { + // NEXT_PUBLIC_CLIENTVAR: z.string(), + }, + + /** + * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. + * middlewares) or client-side so we need to destruct manually. + */ + runtimeEnv: { + NODE_ENV: process.env.NODE_ENV, + // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, + }, + /** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially + * useful for Docker builds. + */ + skipValidation: !!process.env.SKIP_ENV_VALIDATION, + /** + * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and + * `SOME_VAR=''` will throw an error. + */ + emptyStringAsUndefined: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/instrumentation.ts new file mode 100644 index 000000000000..8aff09f087d0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('../sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('../sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/root.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/root.ts new file mode 100644 index 000000000000..4a6e7dc0f6bc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/root.ts @@ -0,0 +1,23 @@ +import { postRouter } from '~/server/api/routers/post'; +import { createCallerFactory, createTRPCRouter } from '~/server/api/trpc'; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ +export const appRouter = createTRPCRouter({ + post: postRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; + +/** + * Create a server-side caller for the tRPC API. + * @example + * const trpc = createCaller(createContext); + * const res = await trpc.post.all(); + * ^? Post[] + */ +export const createCaller = createCallerFactory(appRouter); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/routers/post.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/routers/post.ts new file mode 100644 index 000000000000..042ebe69e9bb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/routers/post.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +import { createTRPCRouter, publicProcedure } from '~/server/api/trpc'; + +// Mocked DB +interface Post { + id: number; + name: string; +} +const posts: Post[] = [ + { + id: 1, + name: 'Hello World', + }, +]; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure.input(z.object({ text: z.string() })).query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure.input(z.object({ name: z.string().min(1) })).mutation(async ({ input }) => { + const post: Post = { + id: posts.length + 1, + name: input.name, + }; + posts.push(post); + return post; + }), + + getLatest: publicProcedure.query(() => { + return posts.at(-1) ?? null; + }), + throwError: publicProcedure.input(z.object({ name: z.string().min(1) })).mutation(async () => { + throw new Error('Error thrown in trpc router'); + }), +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/trpc.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/trpc.ts new file mode 100644 index 000000000000..0bc74b51243e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/trpc.ts @@ -0,0 +1,77 @@ +import * as Sentry from '@sentry/nextjs'; +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC } from '@trpc/server'; +import superjson from 'superjson'; +import { ZodError } from 'zod'; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + * + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. + * + * @see https://trpc.io/docs/server/context + */ +export const createTRPCContext = async (opts: { headers: Headers }) => { + return { + ...opts, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +const sentryMiddleware = Sentry.trpcMiddleware({ + attachRpcInput: true, +}); + +export const publicProcedure = t.procedure.use(async opts => sentryMiddleware(opts)); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/styles/globals.css b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/styles/globals.css new file mode 100644 index 000000000000..b5c61c956711 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/styles/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/query-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/query-client.ts new file mode 100644 index 000000000000..22319e7c0a5a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/query-client.ts @@ -0,0 +1,20 @@ +import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'; +import SuperJSON from 'superjson'; + +export const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 30 * 1000, + }, + dehydrate: { + serializeData: SuperJSON.serialize, + shouldDehydrateQuery: query => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', + }, + hydrate: { + deserializeData: SuperJSON.deserialize, + }, + }, + }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/react.tsx b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/react.tsx new file mode 100644 index 000000000000..12459d66eee6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/react.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { loggerLink, unstable_httpBatchStreamLink } from '@trpc/client'; +import { createTRPCReact } from '@trpc/react-query'; +import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; +import { useState } from 'react'; +import SuperJSON from 'superjson'; + +import { type AppRouter } from '~/server/api/root'; +import { createQueryClient } from './query-client'; + +let clientQueryClientSingleton: QueryClient | undefined = undefined; +const getQueryClient = () => { + if (typeof window === 'undefined') { + // Server: always make a new query client + return createQueryClient(); + } + // Browser: use singleton pattern to keep the same query client + return (clientQueryClientSingleton ??= createQueryClient()); +}; + +export const api = createTRPCReact(); + +/** + * Inference helper for inputs. + * + * @example type HelloInput = RouterInputs['example']['hello'] + */ +export type RouterInputs = inferRouterInputs; + +/** + * Inference helper for outputs. + * + * @example type HelloOutput = RouterOutputs['example']['hello'] + */ +export type RouterOutputs = inferRouterOutputs; + +export function TRPCReactProvider(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + api.createClient({ + links: [ + loggerLink({ + enabled: op => + process.env.NODE_ENV === 'development' || (op.direction === 'down' && op.result instanceof Error), + }), + unstable_httpBatchStreamLink({ + transformer: SuperJSON, + url: getBaseUrl() + '/api/trpc', + headers: () => { + const headers = new Headers(); + headers.set('x-trpc-source', 'nextjs-react'); + return headers; + }, + }), + ], + }), + ); + + return ( + + + {props.children} + + + ); +} + +function getBaseUrl() { + if (typeof window !== 'undefined') return window.location.origin; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return `http://localhost:${process.env.PORT ?? 3000}`; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/server.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/server.ts new file mode 100644 index 000000000000..b6cb13a70781 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/trpc/server.ts @@ -0,0 +1,27 @@ +import 'server-only'; + +import { createHydrationHelpers } from '@trpc/react-query/rsc'; +import { headers } from 'next/headers'; +import { cache } from 'react'; + +import { type AppRouter, createCaller } from '~/server/api/root'; +import { createTRPCContext } from '~/server/api/trpc'; +import { createQueryClient } from './query-client'; + +/** + * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when + * handling a tRPC call from a React Server Component. + */ +const createContext = cache(() => { + const heads = new Headers(headers()); + heads.set('x-trpc-source', 'rsc'); + + return createTRPCContext({ + headers: heads, + }); +}); + +const getQueryClient = cache(createQueryClient); +const caller = createCaller(createContext); + +export const { trpc: api, HydrateClient } = createHydrationHelpers(caller, getQueryClient); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-t3/start-event-proxy.mjs new file mode 100644 index 000000000000..afc5d2e465e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-t3', +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts new file mode 100644 index 000000000000..bdd1ea1f6102 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts @@ -0,0 +1,14 @@ +import { type Config } from 'tailwindcss'; +import { fontFamily } from 'tailwindcss/defaultTheme'; + +export default ({ + content: ['./src/**/*.tsx'], + theme: { + extend: { + fontFamily: { + sans: ['var(--font-geist-sans)', ...fontFamily.sans], + }, + }, + }, + plugins: [], +} satisfies Config); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-error.test.ts new file mode 100644 index 000000000000..0245b641db5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-error.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('should capture error with trpc context', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-t3', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown in trpc router'; + }); + + await page.goto('/'); + await page.click('#error-button'); + + const trpcError = await errorEventPromise; + + expect(trpcError).toBeDefined(); + expect(trpcError.contexts.trpc).toBeDefined(); + expect(trpcError.contexts.trpc.procedure_type).toEqual('mutation'); + expect(trpcError.contexts.trpc.input).toEqual({ name: 'I love dogs' }); +}); + +test('should create transaction with trpc input for error', async ({ page }) => { + const trpcTransactionPromise = waitForTransaction('nextjs-t3', async transactionEvent => { + return transactionEvent?.transaction === 'POST /api/trpc/[trpc]'; + }); + + await page.goto('/'); + await page.click('#error-button'); + + const trpcTransaction = await trpcTransactionPromise; + + expect(trpcTransaction).toBeDefined(); + expect(trpcTransaction.contexts.trpc).toBeDefined(); + expect(trpcTransaction.contexts.trpc.procedure_type).toEqual('mutation'); + expect(trpcTransaction.contexts.trpc.input).toEqual({ name: 'I love dogs' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-mutation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-mutation.test.ts new file mode 100644 index 000000000000..47d6a52f8a19 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/tests/trpc-mutation.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create transaction with trpc input for mutation', async ({ page }) => { + const trpcTransactionPromise = waitForTransaction('nextjs-t3', async transactionEvent => { + return transactionEvent?.transaction === 'POST /api/trpc/[trpc]'; + }); + + await page.goto('/'); + await page.locator('#createInput').fill('I love dogs'); + await page.click('#createButton'); + + const trpcTransaction = await trpcTransactionPromise; + + expect(trpcTransaction).toBeDefined(); + expect(trpcTransaction.contexts.trpc).toBeDefined(); + expect(trpcTransaction.contexts.trpc.procedure_type).toEqual('mutation'); + expect(trpcTransaction.contexts.trpc.input).toEqual({ name: 'I love dogs' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-t3/tsconfig.json new file mode 100644 index 000000000000..905062ded60c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + /* Base Options: */ + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + + /* Strictness */ + "strict": true, + "noUncheckedIndexedAccess": true, + "checkJs": true, + + /* Bundled projects */ + "lib": ["dom", "dom.iterable", "ES2022"], + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "preserve", + "plugins": [{ "name": "next" }], + "incremental": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } + }, + "include": [ + ".eslintrc.cjs", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.cjs", + "**/*.js", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts index de240b761df0..4fa07d82ff6d 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts @@ -105,7 +105,9 @@ Sentry.addEventProcessor(event => { export const t = initTRPC.context().create(); -const procedure = t.procedure.use(Sentry.trpcMiddleware({ attachRpcInput: true })); +const sentryMiddleware = Sentry.trpcMiddleware({ attachRpcInput: true }); + +const procedure = t.procedure.use(async opts => sentryMiddleware(opts)); export const appRouter = t.router({ getSomething: procedure.input(z.string()).query(opts => { diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index 1320f0ff15bc..a3101d793a31 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -1,4 +1,4 @@ -import { isThenable, normalize } from '@sentry/utils'; +import { normalize } from '@sentry/utils'; import { getClient } from './currentScopes'; import { captureException, setContext } from './exports'; @@ -15,16 +15,31 @@ export interface SentryTrpcMiddlewareArguments { type?: unknown; next: () => T; rawInput?: unknown; + getRawInput?: () => Promise; } const trpcCaptureContext = { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }; +function captureIfError(nextResult: unknown): void { + // TODO: Set span status based on what TRPCError was encountered + if ( + typeof nextResult === 'object' && + nextResult !== null && + 'ok' in nextResult && + !nextResult.ok && + 'error' in nextResult + ) { + captureException(nextResult.error, trpcCaptureContext); + } +} + /** * Sentry tRPC middleware that captures errors and creates spans for tRPC procedures. */ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { - return function (opts: SentryTrpcMiddlewareArguments): T { - const { path, type, next, rawInput } = opts; + return async function (opts: SentryTrpcMiddlewareArguments): Promise { + const { path, type, next, rawInput, getRawInput } = opts; + const client = getClient(); const clientOptions = client && client.getOptions(); @@ -33,23 +48,21 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { }; if (options.attachRpcInput !== undefined ? options.attachRpcInput : clientOptions && clientOptions.sendDefaultPii) { - trpcContext.input = normalize(rawInput); - } + if (rawInput !== undefined) { + trpcContext.input = normalize(rawInput); + } - setContext('trpc', trpcContext); + if (getRawInput !== undefined && typeof getRawInput === 'function') { + try { + const rawRes = await getRawInput(); - function captureIfError(nextResult: unknown): void { - // TODO: Set span status based on what TRPCError was encountered - if ( - typeof nextResult === 'object' && - nextResult !== null && - 'ok' in nextResult && - !nextResult.ok && - 'error' in nextResult - ) { - captureException(nextResult.error, trpcCaptureContext); + trpcContext.input = normalize(rawRes); + } catch (err) { + // noop + } } } + setContext('trpc', trpcContext); return startSpanManual( { @@ -60,34 +73,17 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.rpc.trpc', }, }, - span => { - let maybePromiseResult; + async span => { try { - maybePromiseResult = next(); + const nextResult = await next(); + captureIfError(nextResult); + span.end(); + return nextResult; } catch (e) { captureException(e, trpcCaptureContext); span.end(); throw e; } - - if (isThenable(maybePromiseResult)) { - return maybePromiseResult.then( - nextResult => { - captureIfError(nextResult); - span.end(); - return nextResult; - }, - e => { - captureException(e, trpcCaptureContext); - span.end(); - throw e; - }, - ) as T; - } else { - captureIfError(maybePromiseResult); - span.end(); - return maybePromiseResult; - } }, ); }; From 35bdc87dee3498794e34c1ad35dd9927950c8766 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 1 Oct 2024 11:53:53 +0200 Subject: [PATCH 17/22] fix(browser): Ensure `wrap()` only returns functions (#13838) --- packages/browser/src/helpers.ts | 8 +++++++- packages/browser/test/integrations/helpers.test.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index a232d24044dc..702f44e36b7b 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -64,7 +64,13 @@ export function wrap( // the original wrapper. const wrapper = fn.__sentry_wrapped__; if (wrapper) { - return wrapper; + if (typeof wrapper === 'function') { + return wrapper; + } else { + // If we find that the `__sentry_wrapped__` function is not a function at the time of accessing it, it means + // that something messed with it. In that case we want to return the originally passed function. + return fn; + } } // We don't wanna wrap it twice diff --git a/packages/browser/test/integrations/helpers.test.ts b/packages/browser/test/integrations/helpers.test.ts index 37806e06f8a9..ebfabd475e09 100644 --- a/packages/browser/test/integrations/helpers.test.ts +++ b/packages/browser/test/integrations/helpers.test.ts @@ -174,4 +174,17 @@ describe('internal wrap()', () => { expect(wrapped.__sentry_original__).toBe(fn); expect(fn.__sentry_wrapped__).toBe(wrapped); }); + + it('should only return __sentry_wrapped__ when it is a function', () => { + const fn = (() => 1337) as WrappedFunction; + + wrap(fn); + expect(fn).toHaveProperty('__sentry_wrapped__'); + fn.__sentry_wrapped__ = 'something that is not a function' as any; + + const wrapped = wrap(fn); + + expect(wrapped).toBe(fn); + expect(wrapped).not.toBe('something that is not a function'); + }); }); From 5e6aef2bb98feedd3fc6f80dbf4266ccf975e77e Mon Sep 17 00:00:00 2001 From: Ogi <86684834+obostjancic@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:06:37 +0200 Subject: [PATCH 18/22] fix(sveltekit): add url param to source map upload options (#13812) --- packages/sveltekit/src/vite/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/sveltekit/src/vite/types.ts b/packages/sveltekit/src/vite/types.ts index abd526c1e13a..7102971375d4 100644 --- a/packages/sveltekit/src/vite/types.ts +++ b/packages/sveltekit/src/vite/types.ts @@ -105,6 +105,12 @@ type SourceMapsUploadOptions = { */ inject?: boolean; }; + + /** + * The URL of the Sentry instance to upload the source maps to. + */ + url?: string; + /** * Options to further customize the Sentry Vite Plugin (@sentry/vite-plugin) behavior directly. * Options specified in this object take precedence over the options specified in From a6348cca1649b6bffa9cbefa37c8412921ec9bcc Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 2 Oct 2024 14:20:13 +0300 Subject: [PATCH 19/22] ref(utils): Keep non-enumerable properties in `dropUndefinedKeys`. (#13840) --- packages/utils/src/object.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index 95346cf1f812..0ff9b32da402 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -234,7 +234,7 @@ function _dropUndefinedKeys(inputValue: T, memoizationMap: Map Date: Wed, 2 Oct 2024 14:20:30 +0300 Subject: [PATCH 20/22] ref(test): Increase amqplib integration test timeout (#13850) --- .../node-integration-tests/suites/tracing/amqplib/test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts index 3fd00abcd46c..38b3c26dd95e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts @@ -1,7 +1,8 @@ import type { TransactionEvent } from '@sentry/types'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -jest.setTimeout(30_000); +// When running docker compose, we need a larger timeout, as this takes some time. +jest.setTimeout(90_000); const EXPECTED_MESSAGE_SPAN_PRODUCER = expect.objectContaining({ op: 'message', From 8f3e5b25ede4e11779fa045df407685fda56a6fd Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:26:35 +0200 Subject: [PATCH 21/22] feat(nuxt): Add `unstable_sentryBundlerPluginOptions` to module options (#13811) Allows to pass other options from the bundler plugins (vite and rollup). closes https://github.com/getsentry/sentry-javascript/issues/13380 --- packages/nuxt/src/common/types.ts | 10 ++ packages/nuxt/src/vite/sourceMaps.ts | 24 ++-- packages/nuxt/test/vite/sourceMaps.test.ts | 139 +++++++++++++++++++++ 3 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 packages/nuxt/test/vite/sourceMaps.test.ts diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 5fbe68bd89cb..bcc14ad1d307 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -1,4 +1,6 @@ import type { init as initNode } from '@sentry/node'; +import type { SentryRollupPluginOptions } from '@sentry/rollup-plugin'; +import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; import type { init as initVue } from '@sentry/vue'; // Omitting 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this) @@ -111,4 +113,12 @@ export type SentryNuxtModuleOptions = { * @default false */ experimental_basicServerTracing?: boolean; + + /** + * Options to be passed directly to the Sentry Rollup Plugin (`@sentry/rollup-plugin`) and Sentry Vite Plugin (`@sentry/vite-plugin`) that ship with the Sentry Nuxt SDK. + * You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin. + * + * Please note that this option is unstable and may change in a breaking way in any release. + */ + unstable_sentryBundlerPluginOptions?: SentryRollupPluginOptions & SentryVitePluginOptions; }; diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index 801d16ab71cf..9abbfe8eaf08 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -58,7 +58,14 @@ function normalizePath(path: string): string { return path.replace(/^(\.\.\/)+/, './'); } -function getPluginOptions(moduleOptions: SentryNuxtModuleOptions): SentryVitePluginOptions | SentryRollupPluginOptions { +/** + * Generates source maps upload options for the Sentry Vite and Rollup plugin. + * + * Only exported for Testing purposes. + */ +export function getPluginOptions( + moduleOptions: SentryNuxtModuleOptions, +): SentryVitePluginOptions | SentryRollupPluginOptions { const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; return { @@ -66,6 +73,14 @@ function getPluginOptions(moduleOptions: SentryNuxtModuleOptions): SentryVitePlu project: sourceMapsUploadOptions.project ?? process.env.SENTRY_PROJECT, authToken: sourceMapsUploadOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN, telemetry: sourceMapsUploadOptions.telemetry ?? true, + debug: moduleOptions.debug ?? false, + _metaOptions: { + telemetry: { + metaFramework: 'nuxt', + }, + }, + ...moduleOptions?.unstable_sentryBundlerPluginOptions, + sourcemaps: { // The server/client files are in different places depending on the nitro preset (e.g. '.output/server' or '.netlify/functions-internal/server') // We cannot determine automatically how the build folder looks like (depends on the preset), so we have to accept that sourcemaps are uploaded multiple times (with the vitePlugin for Nuxt and the rollupPlugin for Nitro). @@ -74,13 +89,8 @@ function getPluginOptions(moduleOptions: SentryNuxtModuleOptions): SentryVitePlu ignore: sourceMapsUploadOptions.sourcemaps?.ignore ?? undefined, filesToDeleteAfterUpload: sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload ?? undefined, rewriteSources: (source: string) => normalizePath(source), + ...moduleOptions?.unstable_sentryBundlerPluginOptions?.sourcemaps, }, - _metaOptions: { - telemetry: { - metaFramework: 'nuxt', - }, - }, - debug: moduleOptions.debug ?? false, }; } diff --git a/packages/nuxt/test/vite/sourceMaps.test.ts b/packages/nuxt/test/vite/sourceMaps.test.ts new file mode 100644 index 000000000000..34c520b96d83 --- /dev/null +++ b/packages/nuxt/test/vite/sourceMaps.test.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { SentryNuxtModuleOptions } from '../../src/common/types'; +import { getPluginOptions } from '../../src/vite/sourceMaps'; + +describe('getPluginOptions', () => { + beforeEach(() => { + vi.resetModules(); + process.env = {}; + }); + + it('uses environment variables when no moduleOptions are provided', () => { + const defaultEnv = { + SENTRY_ORG: 'default-org', + SENTRY_PROJECT: 'default-project', + SENTRY_AUTH_TOKEN: 'default-token', + }; + + process.env = { ...defaultEnv }; + + const options = getPluginOptions({} as SentryNuxtModuleOptions); + + expect(options).toEqual( + expect.objectContaining({ + org: 'default-org', + project: 'default-project', + authToken: 'default-token', + telemetry: true, + sourcemaps: expect.objectContaining({ + rewriteSources: expect.any(Function), + }), + _metaOptions: expect.objectContaining({ + telemetry: expect.objectContaining({ + metaFramework: 'nuxt', + }), + }), + debug: false, + }), + ); + }); + + it('returns default options when no moduleOptions are provided', () => { + const options = getPluginOptions({} as SentryNuxtModuleOptions); + + expect(options.org).toBeUndefined(); + expect(options.project).toBeUndefined(); + expect(options.authToken).toBeUndefined(); + expect(options).toEqual( + expect.objectContaining({ + telemetry: true, + sourcemaps: expect.objectContaining({ + rewriteSources: expect.any(Function), + }), + _metaOptions: expect.objectContaining({ + telemetry: expect.objectContaining({ + metaFramework: 'nuxt', + }), + }), + debug: false, + }), + ); + }); + + it('merges custom moduleOptions with default options', () => { + const customOptions: SentryNuxtModuleOptions = { + sourceMapsUploadOptions: { + org: 'custom-org', + project: 'custom-project', + authToken: 'custom-token', + telemetry: false, + sourcemaps: { + assets: ['custom-assets/**/*'], + ignore: ['ignore-this.js'], + filesToDeleteAfterUpload: ['delete-this.js'], + }, + }, + debug: true, + }; + const options = getPluginOptions(customOptions); + expect(options).toEqual( + expect.objectContaining({ + org: 'custom-org', + project: 'custom-project', + authToken: 'custom-token', + telemetry: false, + sourcemaps: expect.objectContaining({ + assets: ['custom-assets/**/*'], + ignore: ['ignore-this.js'], + filesToDeleteAfterUpload: ['delete-this.js'], + rewriteSources: expect.any(Function), + }), + _metaOptions: expect.objectContaining({ + telemetry: expect.objectContaining({ + metaFramework: 'nuxt', + }), + }), + debug: true, + }), + ); + }); + + it('overrides options that were undefined with options from unstable_sentryRollupPluginOptions', () => { + const customOptions: SentryNuxtModuleOptions = { + sourceMapsUploadOptions: { + org: 'custom-org', + project: 'custom-project', + sourcemaps: { + assets: ['custom-assets/**/*'], + filesToDeleteAfterUpload: ['delete-this.js'], + }, + }, + debug: true, + unstable_sentryBundlerPluginOptions: { + org: 'unstable-org', + sourcemaps: { + assets: ['unstable-assets/**/*'], + }, + release: { + name: 'test-release', + }, + }, + }; + const options = getPluginOptions(customOptions); + expect(options).toEqual( + expect.objectContaining({ + debug: true, + org: 'unstable-org', + project: 'custom-project', + sourcemaps: expect.objectContaining({ + assets: ['unstable-assets/**/*'], + filesToDeleteAfterUpload: ['delete-this.js'], + rewriteSources: expect.any(Function), + }), + release: expect.objectContaining({ + name: 'test-release', + }), + }), + ); + }); +}); From b8f92a12acbe5c751ceff0b5239bda4fc79cf494 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 2 Oct 2024 14:50:20 +0200 Subject: [PATCH 22/22] meta(changelog): Update changelog for 8.33.0 --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b384d4c427..ec58dd36abbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,46 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.33.0 + +### Important Changes + +- **feat(nextjs): Support new async APIs (`headers()`, `params`, `searchParams`) + ([#13828](https://github.com/getsentry/sentry-javascript/pull/13828))** + +Adds support for [new dynamic Next.js APIs](https://github.com/vercel/next.js/pull/68812). + +- **feat(node): Add `lru-memoizer` instrumentation + ([#13796](https://github.com/getsentry/sentry-javascript/pull/13796))** + +Adds integration for lru-memoizer using @opentelemetry/instrumentation-lru-memoizer. + +- **feat(nuxt): Add `unstable_sentryBundlerPluginOptions` to module options + ([#13811](https://github.com/getsentry/sentry-javascript/pull/13811))** + +Allows passing other options from the bundler plugins (vite and rollup) to Nuxt module options. + +### Other Changes + +- fix(browser): Ensure `wrap()` only returns functions + ([#13838](https://github.com/getsentry/sentry-javascript/pull/13838)) +- fix(core): Adapt trpc middleware input attachment + ([#13831](https://github.com/getsentry/sentry-javascript/pull/13831)) +- fix(core): Don't return trace data in `getTraceData` and `getTraceMetaTags` if SDK is disabled + ([#13760](https://github.com/getsentry/sentry-javascript/pull/13760)) +- fix(nuxt): Don't restrict source map assets upload + ([#13800](https://github.com/getsentry/sentry-javascript/pull/13800)) +- fix(nuxt): Use absolute path for client config ([#13798](https://github.com/getsentry/sentry-javascript/pull/13798)) +- fix(replay): Stop global event handling for paused replays + ([#13815](https://github.com/getsentry/sentry-javascript/pull/13815)) +- fix(sveltekit): add url param to source map upload options + ([#13812](https://github.com/getsentry/sentry-javascript/pull/13812)) +- fix(types): Add jsdocs to cron types ([#13776](https://github.com/getsentry/sentry-javascript/pull/13776)) +- fix(nextjs): Loosen @sentry/nextjs webpack peer dependency + ([#13826](https://github.com/getsentry/sentry-javascript/pull/13826)) + +Work in this release was contributed by @joshuajaco. Thank you for your contribution! + ## 8.32.0 ### Important Changes