diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d65491640642..5c26bded3ddf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -572,6 +572,7 @@ jobs: - bundle_tracing_replay - bundle_tracing_replay_feedback - bundle_tracing_replay_feedback_min + - bundle_tracing_replay_feedback_logs_metrics project: - chromium include: diff --git a/.github/workflows/external-contributors.yml b/.github/workflows/external-contributors.yml index b4678af2eb56..e86532cfbf31 100644 --- a/.github/workflows/external-contributors.yml +++ b/.github/workflows/external-contributors.yml @@ -35,12 +35,17 @@ jobs: name: ${{ github.event.pull_request.user.login }} author_association: ${{ github.event.pull_request.author_association }} + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.GITFLOW_APP_ID }} + private-key: ${{ secrets.GITFLOW_APP_PRIVATE_KEY }} + - name: Create PR with changes uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 with: - # This token is scoped to Daniel Griesser - # If we used the default GITHUB_TOKEN, the resulting PR would not trigger CI :( - token: ${{ secrets.REPO_SCOPED_TOKEN }} + token: ${{ steps.app-token.outputs.token }} commit-message: 'chore: Add external contributor to CHANGELOG.md' title: 'chore: Add external contributor to CHANGELOG.md' branch: 'external-contributor/patch-${{ github.event.pull_request.user.login }}' diff --git a/.github/workflows/gitflow-sync-develop.yml b/.github/workflows/gitflow-sync-develop.yml index ff649d6ee204..0ed510f80178 100644 --- a/.github/workflows/gitflow-sync-develop.yml +++ b/.github/workflows/gitflow-sync-develop.yml @@ -25,6 +25,13 @@ jobs: - name: git checkout uses: actions/checkout@v6 + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.GITFLOW_APP_ID }} + private-key: ${{ secrets.GITFLOW_APP_PRIVATE_KEY }} + # https://github.com/marketplace/actions/github-pull-request-action - name: Create Pull Request id: open-pr @@ -35,8 +42,7 @@ jobs: pr_title: '[Gitflow] Merge ${{ env.SOURCE_BRANCH }} into ${{ env.TARGET_BRANCH }}' pr_body: 'Merge ${{ env.SOURCE_BRANCH }} branch into ${{ env.TARGET_BRANCH }}' pr_label: 'Dev: Gitflow' - # This token is scoped to Daniel Griesser - github_token: ${{ secrets.REPO_SCOPED_TOKEN }} + github_token: ${{ steps.app-token.outputs.token }} - name: Enable automerge for PR if: steps.open-pr.outputs.pr_number != '' diff --git a/.size-limit.js b/.size-limit.js index a43d4d61e527..207fd6a2b85b 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -204,6 +204,12 @@ module.exports = [ gzip: true, limit: '86 KB', }, + { + name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)', + path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), + gzip: true, + limit: '86 KB', + }, // browser CDN bundles (non-gzipped) { name: 'CDN Bundle - uncompressed', @@ -240,6 +246,13 @@ module.exports = [ brotli: false, limit: '264 KB', }, + { + name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed', + path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), + gzip: false, + brotli: false, + limit: '264 KB', + }, // Next.js SDK (ESM) { name: '@sentry/nextjs (client)', diff --git a/CHANGELOG.md b/CHANGELOG.md index aeaf887eded2..2d67c0238bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,62 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 10.35.0 + +### Important Changes + +- **feat(tanstackstart-react): Add `sentryTanstackStart` vite plugin to manage automatic source map uploads ([#18712](https://github.com/getsentry/sentry-javascript/pull/18712))** + + You can now configure source maps upload for TanStack Start using the `sentryTanstackStart` Vite plugin: + + ```ts + // vite.config.ts + import { defineConfig } from 'vite'; + import { sentryTanstackStart } from '@sentry/tanstackstart-react'; + import { tanstackStart } from '@tanstack/react-start/plugin/vite'; + + export default defineConfig({ + plugins: [ + sentryTanstackStart({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: 'your-org', + project: 'your-project', + }), + tanstackStart(), + ], + }); + ``` + +### Other Changes + +- feat(browser): Add CDN bundle for `tracing.replay.feedback.logs.metrics` ([#18785](https://github.com/getsentry/sentry-javascript/pull/18785)) +- feat(browser): Add shim package for logs ([#18831](https://github.com/getsentry/sentry-javascript/pull/18831)) +- feat(cloudflare): Automatically set the release id when CF_VERSION_METADATA is enabled ([#18855](https://github.com/getsentry/sentry-javascript/pull/18855)) +- feat(core): Add `ignored` client report event drop reason ([#18815](https://github.com/getsentry/sentry-javascript/pull/18815)) +- feat(logs): Add `Log` exports to browser and node packages ([#18857](https://github.com/getsentry/sentry-javascript/pull/18857)) +- feat(node-core,bun): Export processSessionIntegration from node-core and add it to bun ([#18852](https://github.com/getsentry/sentry-javascript/pull/18852)) +- fix(core): Find the correct IP address regardless their case ([#18880](https://github.com/getsentry/sentry-javascript/pull/18880)) +- fix(core): Check for AI operation id to detect a vercelai span ([#18823](https://github.com/getsentry/sentry-javascript/pull/18823)) +- fix(ember): Use ES5 syntax in inline vendor scripts ([#18858](https://github.com/getsentry/sentry-javascript/pull/18858)) +- fix(fetch): Shallow-clone fetch options to prevent mutation ([#18867](https://github.com/getsentry/sentry-javascript/pull/18867)) + +
+ Internal Changes + +- chore(ci): Use javascript-sdk-gitflow app instead of personal token ([#18829](https://github.com/getsentry/sentry-javascript/pull/18829)) +- chore(deps): Bump `@sveltejs/kit` devDependency to `2.49.5` ([#18848](https://github.com/getsentry/sentry-javascript/pull/18848)) +- chore(deps): Bump bundler plugins to ^4.6.2 ([#18822](https://github.com/getsentry/sentry-javascript/pull/18822)) +- chore(deps): bump hono from 4.10.3 to 4.11.4 in /dev-packages/e2e-tests/test-applications/cloudflare-hono ([#18806](https://github.com/getsentry/sentry-javascript/pull/18806)) +- chore(test): Bump svelte dependencies ([#18850](https://github.com/getsentry/sentry-javascript/pull/18850)) +- chore(core): Comment out Error tests in langchain ([#18837](https://github.com/getsentry/sentry-javascript/pull/18837)) +- meta(changelog): Fix entry for tanstack start vite plugin ([#18883](https://github.com/getsentry/sentry-javascript/pull/18883)) +- test(e2e): Add testing app for User Feedback ([#18877](https://github.com/getsentry/sentry-javascript/pull/18877)) +- test(fastify): Verify if upstream error is fixed and won't regress ([#18838](https://github.com/getsentry/sentry-javascript/pull/18838)) + +
+ +Work in this release was contributed by @rreckonerr. Thank you for your contribution! + ## 10.34.0 ### Important Changes diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 16850fdbb96f..a83513f0d20d 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -28,6 +28,9 @@ "test:bundle:tracing_logs_metrics:debug_min": "PW_BUNDLE=bundle_tracing_logs_metrics_debug_min yarn test", "test:bundle:full": "PW_BUNDLE=bundle_tracing_replay_feedback yarn test", "test:bundle:full:min": "PW_BUNDLE=bundle_tracing_replay_feedback_min yarn test", + "test:bundle:tracing_replay_feedback_logs_metrics": "PW_BUNDLE=bundle_tracing_replay_feedback_logs_metrics yarn test", + "test:bundle:tracing_replay_feedback_logs_metrics:min": "PW_BUNDLE=bundle_tracing_replay_feedback_logs_metrics_min yarn test", + "test:bundle:tracing_replay_feedback_logs_metrics:debug_min": "PW_BUNDLE=bundle_tracing_replay_feedback_logs_metrics_debug_min yarn test", "test:cjs": "PW_BUNDLE=cjs yarn test", "test:esm": "PW_BUNDLE=esm yarn test", "test:loader": "npx playwright test -c playwright.loader.config.ts --project='chromium'", diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js new file mode 100644 index 000000000000..7b5ee5690589 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +// consoleLoggingIntegration should not actually work, but still not error out +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1, + integrations: [Sentry.consoleLoggingIntegration()], +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/test.ts new file mode 100644 index 000000000000..fa0f77d97643 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/test.ts @@ -0,0 +1,37 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest('exports a shim consoleLoggingIntegration for non-logs bundles', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE; + + // Only run this for CDN bundles that do NOT include logs + // Skip minified bundles because DEBUG_BUILD is false and warnings won't appear + if (!bundle?.startsWith('bundle') || bundle.includes('logs') || bundle.includes('min')) { + sentryTest.skip(); + } + + const consoleMessages: string[] = []; + page.on('console', msg => consoleMessages.push(msg.text())); + + let requestCount = 0; + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { + requestCount++; + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + + await page.goto(url); + + // Wait a bit to ensure no requests are made + await page.waitForTimeout(500); + + expect(requestCount).toBe(0); + expect(consoleMessages).toEqual([ + 'You are using consoleLoggingIntegration() even though this bundle does not include logs.', + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js new file mode 100644 index 000000000000..c4b3013773c3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1, +}); + +// These should not actually work, but still not error out +Sentry.logger.trace('test trace'); +Sentry.logger.debug('test debug'); +Sentry.logger.info('test info'); +Sentry.logger.warn('test warn'); +Sentry.logger.error('test error'); +Sentry.logger.fatal('test fatal'); +const testVar = 'test'; +Sentry.logger.info(Sentry.logger.fmt`formatted ${testVar}`); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/test.ts new file mode 100644 index 000000000000..58f74976f27d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/test.ts @@ -0,0 +1,37 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest('exports a shim logger for non-logs bundles', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE; + + // Only run this for CDN bundles that do NOT include logs + // Skip minified bundles because DEBUG_BUILD is false and warnings won't appear + if (!bundle?.startsWith('bundle') || bundle.includes('logs') || bundle.includes('min')) { + sentryTest.skip(); + } + + const consoleMessages: string[] = []; + page.on('console', msg => consoleMessages.push(msg.text())); + + let requestCount = 0; + await page.route(/^https:\/\/dsn\.ingest\.sentry\.io\//, route => { + requestCount++; + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + + await page.goto(url); + + // Wait a bit to ensure no requests are made + await page.waitForTimeout(500); + + expect(requestCount).toBe(0); + + expect(consoleMessages).toContain('You are using Sentry.logger.* even though this bundle does not include logs.'); + expect(consoleMessages).toContain('You are using Sentry.logger.fmt even though this bundle does not include logs.'); +}); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index 64a82f5f0e62..5833fe15671f 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -63,6 +63,10 @@ const BUNDLE_PATHS: Record> = { bundle_tracing_replay_min: 'build/bundles/bundle.tracing.replay.min.js', bundle_tracing_replay_feedback: 'build/bundles/bundle.tracing.replay.feedback.js', bundle_tracing_replay_feedback_min: 'build/bundles/bundle.tracing.replay.feedback.min.js', + bundle_tracing_replay_feedback_logs_metrics: 'build/bundles/bundle.tracing.replay.feedback.logs.metrics.js', + bundle_tracing_replay_feedback_logs_metrics_min: 'build/bundles/bundle.tracing.replay.feedback.logs.metrics.min.js', + bundle_tracing_replay_feedback_logs_metrics_debug_min: + 'build/bundles/bundle.tracing.replay.feedback.logs.metrics.debug.min.js', loader_base: 'build/bundles/bundle.min.js', loader_eager: 'build/bundles/bundle.min.js', loader_debug: 'build/bundles/bundle.debug.min.js', diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json index f6eddbbdeb58..bdf1fc2e7e14 100644 --- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@sentry/browser": "latest || *", - "@sentry/vite-plugin": "^4.6.1" + "@sentry/vite-plugin": "^4.6.2" }, "volta": { "node": "20.19.2", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json index 5a582b0aa127..ae1b8f445de3 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@sentry/cloudflare": "latest || *", - "hono": "4.10.3" + "hono": "4.11.4" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.31", diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index 0230683d8e5d..059202bac687 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -15,7 +15,7 @@ "devDependencies": { "rollup": "^4.35.0", "vitest": "^0.34.6", - "@sentry/rollup-plugin": "^4.6.1" + "@sentry/rollup-plugin": "^4.6.2" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json index 92bf1dc70c14..f564462c7779 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json @@ -16,7 +16,7 @@ "dependencies": { "@sentry/cloudflare": "latest || *", "@sentry/react-router": "latest || *", - "@sentry/vite-plugin": "^3.1.2", + "@sentry/vite-plugin": "^4.6.2", "@shopify/hydrogen": "2025.5.0", "@shopify/remix-oxygen": "^3.0.0", "graphql": "^16.10.0", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.gitignore new file mode 100644 index 000000000000..0c60c8eeaee8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.gitignore @@ -0,0 +1,48 @@ +# 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 +event-dumps + +.tmp_dev_server_logs diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.npmrc new file mode 100644 index 000000000000..a3160f4de175 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.npmrc @@ -0,0 +1,4 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 +public-hoist-pattern[]=*import-in-the-middle* +public-hoist-pattern[]=*require-in-the-middle* diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/README.md b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/README.md new file mode 100644 index 000000000000..0bd38af41162 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/README.md @@ -0,0 +1,18 @@ +# Next.js 16 User Feedback E2E Tests + +This test application verifies the Sentry User Feedback SDK functionality with Next.js 16. + +## Tests + +The tests cover various feedback APIs: + +- `attachTo()` - Attaching feedback to custom buttons +- `createWidget()` - Creating/removing feedback widget triggers +- `createForm()` - Creating feedback forms with custom labels +- `captureFeedback()` - Programmatic feedback submission +- ThumbsUp/ThumbsDown sentiment tagging +- Dialog cancellation + +## Credits + +Shoutout to [Ryan Albrecht](https://github.com/ryan953) for the underlying [testing app](https://github.com/ryan953/nextjs-test-feedback)! diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/attachToFeedbackButton.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/attachToFeedbackButton.tsx new file mode 100644 index 000000000000..3511e5f73e0f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/attachToFeedbackButton.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useEffect, useState, useRef } from 'react'; +import * as Sentry from '@sentry/nextjs'; + +export default function AttachToFeedbackButton() { + const [feedback, setFeedback] = useState>(); + // Read `getFeedback` on the client only, to avoid hydration errors when server rendering + useEffect(() => { + setFeedback(Sentry.getFeedback()); + }, []); + + const buttonRef = useRef(null); + useEffect(() => { + if (feedback && buttonRef.current) { + const unsubscribe = feedback.attachTo(buttonRef.current, { + tags: { component: 'AttachToFeedbackButton' }, + onSubmitSuccess: data => { + console.log('onSubmitSuccess', data); + }, + }); + return unsubscribe; + } + return () => {}; + }, [feedback]); + + return ( + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/crashReportButton.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/crashReportButton.tsx new file mode 100644 index 000000000000..5a85c441e994 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/crashReportButton.tsx @@ -0,0 +1,20 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; + +export default function CrashReportButton() { + return ( + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/createFeedbackFormButton.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/createFeedbackFormButton.tsx new file mode 100644 index 000000000000..3514b1baa95c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/createFeedbackFormButton.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import * as Sentry from '@sentry/nextjs'; + +type FeedbackIntegration = ReturnType; + +export default function CreateFeedbackFormButton() { + const [feedback, setFeedback] = useState(); + // Read `getFeedback` on the client only, to avoid hydration errors when server rendering + useEffect(() => { + setFeedback(Sentry.getFeedback()); + }, []); + + // Don't render custom feedback button if Feedback integration isn't installed + if (!feedback) { + return null; + } + + return ( + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/feedbackButton.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/feedbackButton.tsx new file mode 100644 index 000000000000..d77bd6199069 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/feedbackButton.tsx @@ -0,0 +1,60 @@ +'use client'; + +import type { RefObject } from 'react'; +import * as Sentry from '@sentry/nextjs'; +import { useEffect, useRef, useState } from 'react'; + +export default function FeedbackButton() { + const buttonRef = useRef(null); + useFeedbackWidget({ + buttonRef, + options: { + tags: { + component: 'FeedbackButton', + }, + }, + }); + + return ( + + ); +} + +function useFeedbackWidget({ + buttonRef, + options = {}, +}: { + buttonRef?: RefObject | RefObject; + options?: { + tags?: Record; + }; +}) { + const [feedback, setFeedback] = useState>(); + // Read `getFeedback` on the client only, to avoid hydration errors when server rendering + useEffect(() => { + setFeedback(Sentry.getFeedback()); + }, []); + + useEffect(() => { + if (!feedback) { + return undefined; + } + + if (buttonRef) { + if (buttonRef.current) { + return feedback.attachTo(buttonRef.current, options); + } + } else { + const widget = feedback.createWidget(options); + return () => { + widget.removeFromDom(); + }; + } + + return undefined; + }, [buttonRef, feedback, options]); + + return feedback; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/myFeedbackForm.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/myFeedbackForm.tsx new file mode 100644 index 000000000000..8f693f7e4fb0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/app/examples/myFeedbackForm.tsx @@ -0,0 +1,48 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; + +export default function MyFeedbackForm() { + return ( +
{ + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + const attachment = async () => { + const attachmentField = formData.get('attachment') as File; + if (!attachmentField || attachmentField.size === 0) { + return null; + } + const data = new Uint8Array(await attachmentField.arrayBuffer()); + const attachmentData = { + data, + filename: 'upload', + }; + return attachmentData; + }; + + Sentry.getCurrentScope().setTags({ component: 'MyFeedbackForm' }); + const attachmentData = await attachment(); + Sentry.captureFeedback( + { + name: String(formData.get('name')), + email: String(formData.get('email')), + message: String(formData.get('message')), + }, + attachmentData ? { attachments: [attachmentData] } : undefined, + ); + }} + > + + +