Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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*
Original file line number Diff line number Diff line change
@@ -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)!
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof Sentry.getFeedback>>();
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
useEffect(() => {
setFeedback(Sentry.getFeedback());
}, []);

const buttonRef = useRef<HTMLButtonElement>(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 (
<button
className="hover:bg-hover px-4 py-2 rounded-md"
type="button"
ref={buttonRef}
data-testid="attach-to-button"
>
Give me feedback (attachTo)
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import * as Sentry from '@sentry/nextjs';

export default function CrashReportButton() {
return (
<button
className="hover:bg-hover px-4 py-2 rounded-md"
type="button"
data-testid="crash-report-button"
onClick={() => {
Sentry.captureException(new Error('Crash Report Button Clicked'), {
data: { useCrashReport: true },
});
}}
>
Crash Report
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';

import { useEffect, useState } from 'react';
import * as Sentry from '@sentry/nextjs';

type FeedbackIntegration = ReturnType<typeof Sentry.getFeedback>;

export default function CreateFeedbackFormButton() {
const [feedback, setFeedback] = useState<FeedbackIntegration>();
// 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 (
<button
className="hover:bg-hover px-4 py-2 rounded-md"
type="button"
data-testid="create-form-button"
onClick={async () => {
const form = await feedback.createForm({
tags: { component: 'CreateFeedbackFormButton' },
});
form.appendToDom();
form.open();
}}
>
Give me feedback (createForm)
</button>
);
}
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement | null>(null);
useFeedbackWidget({
buttonRef,
options: {
tags: {
component: 'FeedbackButton',
},
},
});

return (
<button ref={buttonRef} data-testid="feedback-button">
Give Feedback
</button>
);
}

function useFeedbackWidget({
buttonRef,
options = {},
}: {
buttonRef?: RefObject<HTMLButtonElement | null> | RefObject<HTMLAnchorElement | null>;
options?: {
tags?: Record<string, string>;
};
}) {
const [feedback, setFeedback] = useState<ReturnType<typeof Sentry.getFeedback>>();
// 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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client';

import * as Sentry from '@sentry/nextjs';

export default function MyFeedbackForm() {
return (
<form
id="my-feedback-form"
data-testid="my-feedback-form"
onSubmit={async event => {
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,
);
}}
>
<input name="name" placeholder="Your Name" data-testid="my-form-name" />
<input name="email" placeholder="Your Email" data-testid="my-form-email" />
<textarea name="message" placeholder="What's the issue?" data-testid="my-form-message" />
<input type="file" name="attachment" data-testid="my-form-attachment" />
<button type="submit" data-testid="my-form-submit">
Submit
</button>
</form>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import * as Sentry from '@sentry/nextjs';
import { Fragment, useEffect, useState } from 'react';

export default function ThumbsUpDownButtons() {
const [feedback, setFeedback] = useState<ReturnType<typeof Sentry.getFeedback>>();
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
useEffect(() => {
setFeedback(Sentry.getFeedback());
}, []);

return (
<Fragment>
<strong>Was this helpful?</strong>
<button
title="I like this"
data-testid="thumbs-up-button"
onClick={async () => {
const form = await feedback?.createForm({
messagePlaceholder: 'What did you like most?',
tags: {
component: 'ThumbsUpDownButtons',
'feedback.type': 'positive',
},
});
form?.appendToDom();
form?.open();
}}
>
Yes
</button>

<button
title="I don't like this"
data-testid="thumbs-down-button"
onClick={async () => {
const form = await feedback?.createForm({
messagePlaceholder: 'How can we improve?',
tags: {
component: 'ThumbsUpDownButtons',
'feedback.type': 'negative',
},
});
form?.appendToDom();
form?.open();
}}
>
No
</button>
</Fragment>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import { useEffect, useState } from 'react';
import * as Sentry from '@sentry/nextjs';

export default function ToggleFeedbackButton() {
const [feedback, setFeedback] = useState<ReturnType<typeof Sentry.getFeedback>>();
// Read `getFeedback` on the client only, to avoid hydration errors when server rendering
useEffect(() => {
setFeedback(Sentry.getFeedback());
}, []);

const [widget, setWidget] = useState<null | { removeFromDom: () => void }>();
return (
<button
className="hover:bg-hover px-4 py-2 rounded-md"
type="button"
data-testid="toggle-feedback-button"
onClick={async () => {
if (widget) {
widget.removeFromDom();
setWidget(null);
} else if (feedback) {
setWidget(
feedback.createWidget({
tags: { component: 'ToggleFeedbackButton' },
}),
);
}
}}
>
{widget ? 'Remove Widget' : 'Create Widget'}
</button>
);
}
Loading
Loading