add new pipeline events submissionUpdated & submissionFinished

This commit is contained in:
Matthias Nannt
2023-01-20 12:25:45 +01:00
parent e5ce84b03d
commit dbc68c810f
12 changed files with 237 additions and 53 deletions

View File

@@ -72,6 +72,16 @@ export default function NewFormModal({ open, setOpen, workspaceId }: FormOnboard
},
],
},
{
id: "thankYouPage",
endScreen: true,
elements: [
{
type: "html",
name: "thankYou",
},
],
},
],
},
};

View File

@@ -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 }) {

View File

@@ -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",
},
];

View File

@@ -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>

View File

@@ -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>

View File

@@ -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." : ""}`,
});
};

View File

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

View File

@@ -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";
}
}
}

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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';

View File

@@ -19,6 +19,8 @@ enum PipelineType {
enum PipelineEvent {
submissionCreated
submissionUpdated
submissionFinished
}
model Pipeline {