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, + ); + }} + > + + +