Skip to content

Commit

Permalink
Fix Email Notifications (formbricks#583)
Browse files Browse the repository at this point in the history
* Fix email notifications not working properly

* Fix response notification not working

* fix response meta schema

* fix typo in docs

* improve error message in webhooks
  • Loading branch information
mattinannt authored Jul 19, 2023
1 parent 503e764 commit c52df00
Show file tree
Hide file tree
Showing 18 changed files with 112 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const meta = {
},
]}
example={`{
"personId: "clfqjny0v000ayzgsycx54a2c",
"personId": "clfqjny0v000ayzgsycx54a2c",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"data": {
Expand Down
93 changes: 29 additions & 64 deletions apps/web/app/api/pipeline/route.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,43 @@
import { INTERNAL_SECRET } from "@formbricks/lib/constants";
import { prisma } from "@formbricks/database";
import { NextResponse } from "next/server";
import { AttributeClass } from "@prisma/client";
import { responses } from "@/lib/api/response";
import { transformErrorToDetails } from "@/lib/api/validator";
import { sendResponseFinishedEmail } from "@/lib/email";
import { prisma } from "@formbricks/database";
import { INTERNAL_SECRET } from "@formbricks/lib/constants";
import { convertDatesInObject } from "@formbricks/lib/time";
import { Question } from "@formbricks/types/questions";
import { NotificationSettings } from "@formbricks/types/users";
import { ZPipelineInput } from "@formbricks/types/v1/pipelines";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
const { internalSecret, environmentId, surveyId, event, data } = await request.json();
if (!internalSecret) {
console.error("Pipeline: Missing internalSecret");
return new Response("Missing internalSecret", {
status: 400,
});
// check authentication with x-api-key header and CRON_SECRET env variable
if (headers().get("x-api-key") !== INTERNAL_SECRET) {
return responses.notAuthenticatedResponse();
}
if (!environmentId) {
console.error("Pipeline: Missing environmentId");
return new Response("Missing environmentId", {
status: 400,
});
}
if (!surveyId) {
console.error("Pipeline: Missing surveyId");
return new Response("Missing surveyId", {
status: 400,
});
}
if (!event) {
console.error("Pipeline: Missing event");
return new Response("Missing event", {
status: 400,
});
}
if (!data) {
console.error("Pipeline: Missing data");
return new Response("Missing data", {
status: 400,
});
}
if (internalSecret !== INTERNAL_SECRET) {
console.error("Pipeline: internalSecret doesn't match");
return new Response("Invalid internalSecret", {
status: 401,
});
const jsonInput = await request.json();

convertDatesInObject(jsonInput);

const inputValidation = ZPipelineInput.safeParse(jsonInput);

if (!inputValidation.success) {
console.error(inputValidation.error);
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}

const { environmentId, surveyId, event, response } = inputValidation.data;

// get all webhooks of this environment where event in triggers
const webhooks = await prisma.webhook.findMany({
where: {
environmentId,
triggers: {
hasSome: event,
has: event,
},
OR: [
{
Expand All @@ -75,7 +62,7 @@ export async function POST(request: Request) {
body: JSON.stringify({
webhookId: webhook.id,
event,
data,
data: response,
}),
});
})
Expand Down Expand Up @@ -136,32 +123,10 @@ export async function POST(request: Request) {
name: surveyData.name,
questions: JSON.parse(JSON.stringify(surveyData.questions)) as Question[],
};
// get person for response
let person: {
id: string;
attributes: { id: string; value: string; attributeClass: AttributeClass }[];
} | null;
if (data.personId) {
person = await prisma.person.findUnique({
where: {
id: data.personId,
},
select: {
id: true,
attributes: {
select: {
id: true,
value: true,
attributeClass: true,
},
},
},
});
}
// send email to all users
await Promise.all(
usersWithNotifications.map(async (user) => {
await sendResponseFinishedEmail(user.email, environmentId, survey, data, person);
await sendResponseFinishedEmail(user.email, environmentId, survey, response);
})
);
}
Expand Down
8 changes: 2 additions & 6 deletions apps/web/app/api/v1/client/responses/[responseId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,7 @@ export async function PUT(
event: "responseUpdated",
environmentId: survey.environmentId,
surveyId: survey.id,
// only send the updated fields
data: {
...response,
data: inputValidation.data.data,
},
response,
});

if (response.finished) {
Expand All @@ -82,7 +78,7 @@ export async function PUT(
event: "responseFinished",
environmentId: survey.environmentId,
surveyId: survey.id,
data: response,
response: response,
});
}

Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/v1/client/responses/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ export async function POST(request: Request): Promise<NextResponse> {
event: "responseCreated",
environmentId: survey.environmentId,
surveyId: response.surveyId,
data: response,
response: response,
});

if (responseInput.finished) {
sendToPipeline({
event: "responseFinished",
environmentId: survey.environmentId,
surveyId: response.surveyId,
data: response,
response: response,
});
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/v1/webhooks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export async function GET() {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
return responses.internalServerErrorResponse(error.message);
}
}

Expand Down
10 changes: 4 additions & 6 deletions apps/web/lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { env } from "@/env.mjs";
import { getQuestionResponseMapping } from "@/lib/responses/questionResponseMapping";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { Question } from "@formbricks/types/questions";
import { Response } from "@formbricks/types/responses";
import { AttributeClass } from "@prisma/client";
import { TResponse } from "@formbricks/types/v1/responses";
import { withEmailTemplate } from "./email-template";
import { createInviteToken, createToken } from "./jwt";

Expand Down Expand Up @@ -120,16 +119,15 @@ export const sendResponseFinishedEmail = async (
email: string,
environmentId: string,
survey: { id: string; name: string; questions: Question[] },
response: Response,
person: { id: string; attributes: { id: string; value: string; attributeClass: AttributeClass }[] } | null
response: TResponse
) => {
const personEmail = person?.attributes?.find((a) => a.attributeClass?.name === "email")?.value;
const personEmail = response.person?.attributes["email"];
await sendEmail({
to: email,
subject: personEmail
? `${personEmail} just completed your ${survey.name} survey ✅`
: `A response for ${survey.name} was completed ✅`,
replyTo: personEmail || env.MAIL_FROM,
replyTo: personEmail?.toString() || env.MAIL_FROM,
html: withEmailTemplate(`<h1>Hey 👋</h1>Someone just completed your survey <strong>${
survey.name
}</strong><br/>
Expand Down
18 changes: 4 additions & 14 deletions apps/web/lib/pipelines.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
import { TPipelineInput } from "@formbricks/types/v1/pipelines";

export async function sendToPipeline({
event,
surveyId,
environmentId,
data,
}: {
event: TPipelineTrigger;
surveyId: string;
environmentId: string;
data: any;
}) {
export async function sendToPipeline({ event, surveyId, environmentId, response }: TPipelineInput) {
return fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": INTERNAL_SECRET,
},
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId: environmentId,
surveyId: surveyId,
event,
data,
response,
}),
}).catch((error) => {
console.error(`Error sending event to pipeline: ${error}`);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/lib/responses/questionResponseMapping.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Question } from "@formbricks/types/questions";
import { Response } from "@formbricks/types/responses";
import { TResponse } from "@formbricks/types/v1/responses";

export const getQuestionResponseMapping = (
survey: { questions: Question[] },
response: Response
response: TResponse
): { question: string; answer: string }[] => {
const questionResponseMapping: { question: string; answer: string }[] = [];

Expand All @@ -12,7 +12,7 @@ export const getQuestionResponseMapping = (

questionResponseMapping.push({
question: question.headline,
answer,
answer: answer.toString(),
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
import { TPipelineInput } from "@formbricks/types/v1/pipelines";
import { updateResponse } from "@formbricks/lib/services/response";
import { sendToPipeline } from "@/lib/pipelines";

export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const environmentId = req.query.environmentId?.toString();
Expand Down Expand Up @@ -42,15 +45,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
...response.data,
};

// update response
const responseData = await prisma.response.update({
where: {
id: responseId,
},
data: {
...{ ...response, data: newResponseData },
},
});
const updatedResponse = await updateResponse(responseId, { ...response, data: newResponseData });

// send response update to pipeline
// don't await to not block the response
Expand All @@ -62,31 +57,24 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId,
surveyId: responseData.surveyId,
surveyId: updatedResponse.surveyId,
event: "responseUpdated",
data: { id: responseId, ...response },
}),
response: updatedResponse,
} as TPipelineInput),
});

if (response.finished) {
// send response to pipeline
// don't await to not block the response
fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId,
surveyId: responseData.surveyId,
event: "responseFinished",
data: responseData,
}),
sendToPipeline({
environmentId,
surveyId: updatedResponse.surveyId,
event: "responseFinished",
response: updatedResponse,
});
}

return res.json(responseData);
return res.json({ message: "Response updated" });
}

// Unknown HTTP Method
Expand Down
Loading

0 comments on commit c52df00

Please sign in to comment.