mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-25 18:48:58 -06:00
add new pipeline events submissionUpdated & submissionFinished
This commit is contained in:
@@ -72,6 +72,16 @@ export default function NewFormModal({ open, setOpen, workspaceId }: FormOnboard
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "thankYouPage",
|
||||
endScreen: true,
|
||||
elements: [
|
||||
{
|
||||
type: "html",
|
||||
name: "thankYou",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { webhook } from "./webhook";
|
||||
const availablePipelines = [webhook, emailNotification, slackNotification];
|
||||
|
||||
const getEmptyPipeline = () => {
|
||||
return { label: "", type: null, events: ["submissionCreated"], config: {} };
|
||||
return { label: "", type: null, events: ["submissionFinished"], config: {} };
|
||||
};
|
||||
|
||||
export default function AddPipelineModal({ open, setOpen }) {
|
||||
|
||||
@@ -8,7 +8,18 @@ const eventTypes = [
|
||||
{
|
||||
id: "submissionCreated",
|
||||
name: "Submission Created",
|
||||
description: "Every time a new submission is created",
|
||||
description:
|
||||
"Every time a new submission is created in Formbricks (e.g. a new submission or first step in a multi-step form)",
|
||||
},
|
||||
{
|
||||
id: "submissionUpdated",
|
||||
name: "Submission Updated",
|
||||
description: "Every time a submission is updated, e.g. one step in a multi-step form",
|
||||
},
|
||||
{
|
||||
id: "submissionFinished",
|
||||
name: "Submission Finished",
|
||||
description: "Every time a submission is finished, e.g. a multi-step form is completed",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -10,7 +10,18 @@ const eventTypes = [
|
||||
{
|
||||
id: "submissionCreated",
|
||||
name: "Submission Created",
|
||||
description: "Every time a new submission is created",
|
||||
description:
|
||||
"Every time a new submission is created in Formbricks (e.g. a new submission or first step in a multi-step form)",
|
||||
},
|
||||
{
|
||||
id: "submissionUpdated",
|
||||
name: "Submission Updated",
|
||||
description: "Every time a submission is updated, e.g. one step in a multi-step form",
|
||||
},
|
||||
{
|
||||
id: "submissionFinished",
|
||||
name: "Submission Finished",
|
||||
description: "Every time a submission is finished, e.g. a multi-step form is completed",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -111,8 +122,8 @@ export function SlackNotificationSettings({ pipeline, setPipeline }) {
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
id="comments"
|
||||
name="comments"
|
||||
id={eventType.id}
|
||||
name={eventType.id}
|
||||
type="checkbox"
|
||||
checked={pipeline.events.includes(eventType.id)}
|
||||
onChange={() => toggleEvent(eventType.id)}
|
||||
@@ -120,7 +131,7 @@ export function SlackNotificationSettings({ pipeline, setPipeline }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="comments" className="font-medium text-gray-700">
|
||||
<label htmlFor={eventType.id} className="font-medium text-gray-700">
|
||||
{eventType.name}
|
||||
</label>
|
||||
<p className="text-gray-500">{eventType.description}</p>
|
||||
|
||||
@@ -8,7 +8,18 @@ const eventTypes = [
|
||||
{
|
||||
id: "submissionCreated",
|
||||
name: "Submission Created",
|
||||
description: "Every time a new submission is created",
|
||||
description:
|
||||
"Every time a new submission is created in Formbricks (e.g. a new submission or first step in a multi-step form)",
|
||||
},
|
||||
{
|
||||
id: "submissionUpdated",
|
||||
name: "Submission Updated",
|
||||
description: "Every time a submission is updated, e.g. one step in a multi-step form",
|
||||
},
|
||||
{
|
||||
id: "submissionFinished",
|
||||
name: "Submission Finished",
|
||||
description: "Every time a submission is finished, e.g. a multi-step form is completed",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -124,8 +135,8 @@ export function WebhookSettings({ pipeline, setPipeline }) {
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
id="comments"
|
||||
name="comments"
|
||||
id={eventType.id}
|
||||
name={eventType.id}
|
||||
type="checkbox"
|
||||
checked={pipeline.events.includes(eventType.id)}
|
||||
onChange={() => toggleEvent(eventType.id)}
|
||||
@@ -133,7 +144,7 @@ export function WebhookSettings({ pipeline, setPipeline }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="comments" className="font-medium text-gray-700">
|
||||
<label htmlFor={eventType.id} className="font-medium text-gray-700">
|
||||
{eventType.name}
|
||||
</label>
|
||||
<p className="text-gray-500">{eventType.description}</p>
|
||||
|
||||
@@ -82,6 +82,7 @@ export const sendPasswordResetNotifyEmail = async (user) => {
|
||||
|
||||
export const sendSubmissionEmail = async (
|
||||
email: string,
|
||||
event: "created" | "updated" | "finished",
|
||||
workspaceId,
|
||||
formId,
|
||||
formLabel: string,
|
||||
@@ -90,9 +91,24 @@ export const sendSubmissionEmail = async (
|
||||
) => {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `${formLabel} new submission`,
|
||||
subject:
|
||||
event === "created"
|
||||
? `${formLabel} new submission created`
|
||||
: event === "updated"
|
||||
? `${formLabel} submission updated`
|
||||
: event === "finished"
|
||||
? `${formLabel} submission finished`
|
||||
: `${formLabel} submission update`,
|
||||
replyTo: submission.customer?.email || process.env.MAIL_FROM,
|
||||
html: `Hey, someone just filled out your form ${formLabel} in Formbricks.<br/>
|
||||
html: `${
|
||||
event === "created"
|
||||
? `Hey, someone just filled out your form "${formLabel}" in Formbricks.`
|
||||
: event === "updated"
|
||||
? `Hey, a submission in "${formLabel}" in Formbricks just received an update.`
|
||||
: event === "finished"
|
||||
? `Hey, someone just finished your form "${formLabel}" in Formbricks.`
|
||||
: ""
|
||||
}<br/>
|
||||
|
||||
<hr/>
|
||||
|
||||
@@ -104,7 +120,7 @@ export const sendSubmissionEmail = async (
|
||||
|
||||
Click <a href="${
|
||||
process.env.NEXTAUTH_URL
|
||||
}/workspaces/${workspaceId}/forms/${formId}/feedback">here</a> to see new submission.
|
||||
}/workspaces/${workspaceId}/forms/${formId}/feedback">here</a> to see the submission.
|
||||
${submission.customer?.email ? "<hr/>You can reply to this email to contact the user directly." : ""}`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import crypto from "crypto";
|
||||
import { sendSubmissionEmail } from "./email";
|
||||
import { MergeWithSchema } from "./submissions";
|
||||
|
||||
export const runPipelines = async (form, submission) => {
|
||||
export const runPipelines = async (triggeredEvents, form, submissionReceived, submissionFull) => {
|
||||
// handle integrations
|
||||
const pipelines = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
@@ -13,51 +13,95 @@ export const runPipelines = async (form, submission) => {
|
||||
});
|
||||
for (const pipeline of pipelines) {
|
||||
if (pipeline.type === "emailNotification") {
|
||||
await handleEmailNotification(pipeline, form, submission);
|
||||
await handleEmailNotification(triggeredEvents, pipeline, form, submissionReceived, submissionFull);
|
||||
}
|
||||
if (pipeline.type === "slackNotification") {
|
||||
await handleSlackNotification(pipeline, form, submission);
|
||||
await handleSlackNotification(triggeredEvents, pipeline, form, submissionReceived, submissionFull);
|
||||
}
|
||||
if (pipeline.type === "webhook") {
|
||||
await handleWebhook(pipeline, submission);
|
||||
await handleWebhook(triggeredEvents, pipeline, submissionReceived, submissionFull);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function handleWebhook(pipeline, submission) {
|
||||
async function handleWebhook(triggeredEvents, pipeline, submissionReceived, submissionFull) {
|
||||
if (pipeline.config.hasOwnProperty("endpointUrl") && pipeline.config.hasOwnProperty("secret")) {
|
||||
if (pipeline.events.includes("submissionCreated")) {
|
||||
const webhookData = pipeline.config;
|
||||
const body = { time: Math.floor(Date.now() / 1000), submission };
|
||||
fetch(webhookData.endpointUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Hub-Signature-256": `sha256=${crypto
|
||||
.createHmac("sha256", webhookData.secret.toString())
|
||||
.update(JSON.stringify(body))
|
||||
.digest("base64")}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
let body;
|
||||
if (triggeredEvents.includes("submissionCreated") && pipeline.events.includes("submissionCreated")) {
|
||||
body = { time: Math.floor(Date.now() / 1000), submissionFull };
|
||||
await sendWebhook(pipeline, body);
|
||||
}
|
||||
if (triggeredEvents.includes("submissionUpdated") && pipeline.events.includes("submissionUpdated")) {
|
||||
body = { time: Math.floor(Date.now() / 1000), submissionReceived };
|
||||
await sendWebhook(pipeline, body);
|
||||
}
|
||||
if (triggeredEvents.includes("submissionUpdated") && pipeline.events.includes("submissionUpdated")) {
|
||||
body = { time: Math.floor(Date.now() / 1000), submissionFull };
|
||||
await sendWebhook(pipeline, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEmailNotification(pipeline, form, submission) {
|
||||
const sendWebhook = async (pipeline, body) => {
|
||||
const webhookData = pipeline.config;
|
||||
fetch(webhookData.endpointUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Hub-Signature-256": `sha256=${crypto
|
||||
.createHmac("sha256", webhookData.secret.toString())
|
||||
.update(JSON.stringify(body))
|
||||
.digest("base64")}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
|
||||
async function handleEmailNotification(triggeredEvents, pipeline, form, submissionReceived, submissionFull) {
|
||||
if (!pipeline.config.hasOwnProperty("email")) return;
|
||||
|
||||
const { email } = pipeline.config.valueOf() as { email: string };
|
||||
|
||||
if (pipeline.events.includes("submissionCreated")) {
|
||||
await sendSubmissionEmail(email, form.workspaceId, form.id, form.label, form.schema, submission);
|
||||
if (triggeredEvents.includes("submissionCreated") && pipeline.events.includes("submissionCreated")) {
|
||||
await sendSubmissionEmail(
|
||||
email,
|
||||
"created",
|
||||
form.workspaceId,
|
||||
form.id,
|
||||
form.label,
|
||||
form.schema,
|
||||
submissionFull
|
||||
);
|
||||
}
|
||||
if (triggeredEvents.includes("submissionUpdated") && pipeline.events.includes("submissionUpdated")) {
|
||||
await sendSubmissionEmail(
|
||||
email,
|
||||
"updated",
|
||||
form.workspaceId,
|
||||
form.id,
|
||||
form.label,
|
||||
form.schema,
|
||||
submissionReceived
|
||||
);
|
||||
}
|
||||
if (triggeredEvents.includes("submissionFinished") && pipeline.events.includes("submissionFinished")) {
|
||||
await sendSubmissionEmail(
|
||||
email,
|
||||
"finished",
|
||||
form.workspaceId,
|
||||
form.id,
|
||||
form.label,
|
||||
form.schema,
|
||||
submissionFull
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSlackNotification(pipeline, form, submission) {
|
||||
async function handleSlackNotification(triggeredEvents, pipeline, form, submissionReceived, submissionFull) {
|
||||
if (pipeline.config.hasOwnProperty("endpointUrl")) {
|
||||
if (pipeline.events.includes("submissionCreated")) {
|
||||
const body = {
|
||||
let body;
|
||||
if (triggeredEvents.includes("submissionCreated") && pipeline.events.includes("submissionCreated")) {
|
||||
body = {
|
||||
text: `Someone just filled out your form "${form.label}" in Formbricks.`,
|
||||
blocks: [
|
||||
{
|
||||
@@ -71,20 +115,72 @@ async function handleSlackNotification(pipeline, form, submission) {
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${Object.entries(MergeWithSchema(submission.data, form.schema))
|
||||
text: `${Object.entries(MergeWithSchema(submissionFull.data, form.schema))
|
||||
.map(([key, value]) => `*${key}*\n${value}\n`)
|
||||
.join("")}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
fetch(pipeline.config.endpointUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
await sendSlackMessage(pipeline, body);
|
||||
}
|
||||
if (triggeredEvents.includes("submissionUpdated") && pipeline.events.includes("submissionUpdated")) {
|
||||
body = {
|
||||
text: `Someone just updated a submission in your form "${form.label}" in Formbricks.`,
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `Someone just updated a submission in your form "${form.label}". <${process.env.NEXTAUTH_URL}/workspaces/${form.workspaceId}/forms/${form.id}/feedback|View in Formbricks>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${Object.entries(MergeWithSchema(submissionReceived.data, form.schema))
|
||||
.map(([key, value]) => `*${key}*\n${value}\n`)
|
||||
.join("")}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await sendSlackMessage(pipeline, body);
|
||||
}
|
||||
if (triggeredEvents.includes("submissionFinished") && pipeline.events.includes("submissionFinished")) {
|
||||
body = {
|
||||
text: `Someone just finished your form "${form.label}" in Formbricks.`,
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `Someone just finished your form "${form.label}". <${process.env.NEXTAUTH_URL}/workspaces/${form.workspaceId}/forms/${form.id}/feedback|View in Formbricks>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${Object.entries(MergeWithSchema(submissionFull.data, form.schema))
|
||||
.map(([key, value]) => `*${key}*\n${value}\n`)
|
||||
.join("")}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await sendSlackMessage(pipeline, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sendSlackMessage = async (pipeline, body) => {
|
||||
fetch(pipeline.config.endpointUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,6 +44,9 @@ export const MergeWithSchema = (submissionData, schema) => {
|
||||
return submissionData;
|
||||
}
|
||||
const mergedData = {};
|
||||
if (!submissionData) {
|
||||
return mergedData;
|
||||
}
|
||||
const optionLabelMap = getOptionLabelMap(schema);
|
||||
for (const page of schema.pages) {
|
||||
for (const elem of page.elements) {
|
||||
@@ -75,7 +78,7 @@ export const MergeWithSchema = (submissionData, schema) => {
|
||||
mergedData[elem.label] = submissionData[elem.name];
|
||||
}
|
||||
} else {
|
||||
mergedData[elem.label] = "not provided";
|
||||
// mergedData[elem.label] = "not provided";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +79,26 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// create form in db
|
||||
const submissionResult = await prisma.submission.update(event);
|
||||
await runPipelines(form, submission);
|
||||
const pipelineEvents = [];
|
||||
if (submission.data) {
|
||||
pipelineEvents.push("submissionUpdated");
|
||||
}
|
||||
if (submission.finished) {
|
||||
pipelineEvents.push("submissionFinished");
|
||||
}
|
||||
await runPipelines(pipelineEvents, form, submission, submissionResult);
|
||||
// tracking
|
||||
capturePosthogEvent(form.workspaceId, "submission received", {
|
||||
formId,
|
||||
});
|
||||
captureTelemetry("submission received");
|
||||
if (submission.finished) {
|
||||
capturePosthogEvent(form.workspaceId, "submission finished", {
|
||||
formId,
|
||||
});
|
||||
captureTelemetry("submission finished");
|
||||
} else {
|
||||
capturePosthogEvent(form.workspaceId, "submission updated", {
|
||||
formId,
|
||||
});
|
||||
captureTelemetry("submission updated");
|
||||
}
|
||||
res.json(submissionResult);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// create form in db
|
||||
const submissionResult = await prisma.submission.create(event);
|
||||
await runPipelines(form, submission);
|
||||
await runPipelines(["submissionCreated"], form, submission, submissionResult);
|
||||
// tracking
|
||||
capturePosthogEvent(form.workspaceId, "submission received", {
|
||||
formId,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "PipelineEvent" ADD VALUE 'submissionUpdated';
|
||||
ALTER TYPE "PipelineEvent" ADD VALUE 'submissionFinished';
|
||||
@@ -19,6 +19,8 @@ enum PipelineType {
|
||||
|
||||
enum PipelineEvent {
|
||||
submissionCreated
|
||||
submissionUpdated
|
||||
submissionFinished
|
||||
}
|
||||
|
||||
model Pipeline {
|
||||
|
||||
Reference in New Issue
Block a user