diff --git a/.changeset/quiet-feedback-errors.md b/.changeset/quiet-feedback-errors.md new file mode 100644 index 000000000..6ce274a1d --- /dev/null +++ b/.changeset/quiet-feedback-errors.md @@ -0,0 +1,5 @@ +--- +"@reflag/browser-sdk": patch +--- + +Treat unsuccessful feedback form submissions as errors instead of showing success and invoking `onAfterSubmit` with unsaved feedback. Applies to manual and automated forms, and allows retrying Send. diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts index 4f63bbbef..17fad6e52 100644 --- a/packages/browser-sdk/src/client.ts +++ b/packages/browser-sdk/src/client.ts @@ -1199,11 +1199,13 @@ export class ReflagClient { return { feedbackId: undefined }; }, onSubmit: async (data) => { - // Default onSubmit handler - await this.feedback({ + const res = await this.feedback({ ...feedbackData, ...data, }); + if (!res?.ok) { + throw new Error("Couldn't submit feedback. Please try again."); + } options.onAfterSubmit?.(data); }, diff --git a/packages/browser-sdk/src/feedback/feedback.ts b/packages/browser-sdk/src/feedback/feedback.ts index 1a68eeed8..dd7146bc3 100644 --- a/packages/browser-sdk/src/feedback/feedback.ts +++ b/packages/browser-sdk/src/feedback/feedback.ts @@ -435,7 +435,10 @@ export class AutoFeedback { return { feedbackId: res.feedbackId }; }, onSubmit: async (data) => { - await replyCallback(data); + const res = await replyCallback(data); + if (!res?.feedbackId) { + throw new Error("Couldn't submit feedback. Please try again."); + } options.onAfterSubmit?.(data); }, onDismiss: () => replyCallback(null), diff --git a/packages/browser-sdk/test/feedbackSubmitErrors.test.ts b/packages/browser-sdk/test/feedbackSubmitErrors.test.ts new file mode 100644 index 000000000..aecd4a847 --- /dev/null +++ b/packages/browser-sdk/test/feedbackSubmitErrors.test.ts @@ -0,0 +1,65 @@ +import { afterEach, expect, test, vi } from "vitest"; + +import { ReflagClient } from "../src/client"; +import { AutoFeedback } from "../src/feedback/feedback"; +import * as feedbackUi from "../src/feedback/ui"; +import { HttpClient } from "../src/httpClient"; +import { testLogger } from "./testLogger"; + +const data = { question: "How was it?", score: 4, comment: "Useful!" }; + +afterEach(() => vi.restoreAllMocks()); + +test.each(["manual", "automated"] as const)( + "%s feedback rejects a failed Send without notifying, then allows retry", + async (source) => { + const open = vi + .spyOn(feedbackUi, "openFeedbackForm") + .mockImplementation(() => undefined); + const onAfterSubmit = vi.fn(); + let status = 200; + vi.spyOn(HttpClient.prototype, "post").mockImplementation(async () => + Response.json( + status === 200 ? { feedbackId: "123" } : { message: "Unavailable" }, + { status }, + ), + ); + + if (source === "manual") { + const client = new ReflagClient({ + publishableKey: "test-key", + user: { id: "user" }, + logger: testLogger, + }); + client.requestFeedback({ flagKey: "flag", onAfterSubmit }); + await vi.waitFor(() => expect(open).toHaveBeenCalledOnce()); + } else { + const auto = new AutoFeedback( + "https://front.reflag.com", + testLogger, + new HttpClient("test-key"), + (_prompt, handlers) => handlers.openFeedbackForm({ onAfterSubmit }), + "user", + ); + await auto.triggerFeedbackPrompt( + "user", + { + question: data.question, + promptId: "prompt", + featureId: "feature", + showAfter: new Date(), + showBefore: new Date(Date.now() + 60000), + }, + vi.fn(), + ); + } + + const { onSubmit } = open.mock.calls[0]![0]; + status = 500; + await expect(onSubmit(data)).rejects.toThrow("Couldn't submit feedback"); + expect(onAfterSubmit).not.toHaveBeenCalled(); + + status = 200; + await expect(onSubmit(data)).resolves.toBeUndefined(); + }, +);