From ac3e7b480c4906e70ae8be76d8d9202c36713a3f Mon Sep 17 00:00:00 2001 From: Sachin Ranchod Date: Mon, 9 Oct 2023 22:31:22 -0700 Subject: [PATCH 1/2] add a queue for nurture emails --- apps/zipper.dev/src/emails/index.tsx | 1 - .../src/pages/api/auth/[...nextauth].ts | 18 +----- apps/zipper.dev/src/server/queue.ts | 9 ++- .../src/server/utils/nurtureCampaign.utils.ts | 59 +++++++++++++++++++ 4 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 apps/zipper.dev/src/server/utils/nurtureCampaign.utils.ts diff --git a/apps/zipper.dev/src/emails/index.tsx b/apps/zipper.dev/src/emails/index.tsx index ac749b337..12654d517 100644 --- a/apps/zipper.dev/src/emails/index.tsx +++ b/apps/zipper.dev/src/emails/index.tsx @@ -6,7 +6,6 @@ import { Head, Heading, Html, - Link, Preview, Text, Section, diff --git a/apps/zipper.dev/src/pages/api/auth/[...nextauth].ts b/apps/zipper.dev/src/pages/api/auth/[...nextauth].ts index 001be0058..8bfea2214 100644 --- a/apps/zipper.dev/src/pages/api/auth/[...nextauth].ts +++ b/apps/zipper.dev/src/pages/api/auth/[...nextauth].ts @@ -15,6 +15,8 @@ import { createUserSlug } from '~/utils/create-user-slug'; import { resend } from '~/server/resend'; import crypto from 'crypto'; import { trackEvent } from '~/utils/api-analytics'; +import { queues } from '~/server/queue'; +import { startNurtureCampaign } from '~/server/utils/nurtureCampaign.utils'; export function PrismaAdapter(p: PrismaClient): Adapter { return { @@ -390,21 +392,7 @@ export const authOptions: AuthOptions = { }); if (isNewUser && user.email) { - resend.emails.send({ - to: user.email, - from: 'Sachin & Ibu ', - reply_to: ['sachin@zipper.works', 'ibu@zipper.works'], - subject: 'Thank you for checking out Zipper', - text: `Hey there, - -We just wanted to drop you a quick note to say thank you for checking out Zipper. - -If you have any questions, feedback, or general comments, we'd genuinely love to hear them. Feel free to reply to this email at any point - your emails go directly into our inboxes (and not a general mailbox or support queue). - -Regards, -Sachin & Ibu -`, - }); + startNurtureCampaign(user.email); } trackEvent({ diff --git a/apps/zipper.dev/src/server/queue.ts b/apps/zipper.dev/src/server/queue.ts index c6f9f37df..b3fde2c75 100644 --- a/apps/zipper.dev/src/server/queue.ts +++ b/apps/zipper.dev/src/server/queue.ts @@ -5,6 +5,7 @@ import { prisma } from './prisma'; import fetch from 'node-fetch'; import getRunUrl from '../utils/get-run-url'; import { generateAccessToken } from '../utils/jwt-utils'; +import { sendNurtureEmail } from './utils/nurtureCampaign.utils'; export const redis = new IORedis(+env.REDIS_PORT, env.REDIS_HOST, { maxRetriesPerRequest: null, @@ -12,12 +13,15 @@ export const redis = new IORedis(+env.REDIS_PORT, env.REDIS_HOST, { const queueWorkersGlobal = global as typeof global & { workers?: Worker[]; - queues?: Record<'schedule', Queue>; + queues?: Record<'schedule' | 'nurture', Queue>; }; const initializeWorkers = () => { console.log('[BullMQ] Initializing workers'); return [ + new Worker('nurture', async (job) => { + await sendNurtureEmail(job.data.step, job.data.email); + }), new Worker( 'schedule-queue', async (job) => { @@ -92,10 +96,11 @@ const initializeQueues = () => { connection: redis, defaultJobOptions: { removeOnComplete: 1000, removeOnFail: 5000 }, }), + nurture: new Queue('nurture', { connection: redis }), }; }; -export const queues: Record<'schedule', Queue> = +export const queues: Record<'schedule' | 'nurture', Queue> = queueWorkersGlobal.queues || initializeQueues(); export const initializeQueuesAndWorkers = () => { diff --git a/apps/zipper.dev/src/server/utils/nurtureCampaign.utils.ts b/apps/zipper.dev/src/server/utils/nurtureCampaign.utils.ts new file mode 100644 index 000000000..345f12380 --- /dev/null +++ b/apps/zipper.dev/src/server/utils/nurtureCampaign.utils.ts @@ -0,0 +1,59 @@ +import { queues } from '../queue'; +import { resend } from '../resend'; + +export const startNurtureCampaign = (email: string) => { + const hour = 1000 * 60 * 60; + + queues.nurture.addBulk([ + { + name: 'first-nurture-email', + data: { step: 1, email }, + // opts: { delay: hour / 4 }, + opts: { delay: hour / 60 }, + }, + { + name: 'second-nurture-email', + data: { step: 2, email }, + // opts: { delay: hour * 24 }, + opts: { delay: (hour / 60) * 2 }, + }, + ]); +}; + +const STEP_ONE_CONTENT = `Hey there, + +We just wanted to drop you a quick note to say thank you for checking out Zipper. + +If you have any questions, feedback, or general comments, we'd genuinely love to hear them. Feel free to reply to this email at any point - your emails go directly into our inboxes (and not a general mailbox or support queue). + +Regards, +Sachin & Ibu +`; + +const STEP_TWO_CONTENT = `This is the second email!`; + +export const sendNurtureEmail = async (step: 1 | 2, email: string) => { + switch (step) { + case 1: + await resend.emails.send({ + to: email, + from: 'Sachin & Ibu ', + reply_to: ['sachin@zipper.works', 'ibu@zipper.works'], + subject: 'Thank you for checking out Zipper', + text: STEP_ONE_CONTENT, + }); + + break; + case 2: + await resend.emails.send({ + to: email, + from: 'Zipper ', + reply_to: ['sachin@zipper.works', 'ibu@zipper.works'], + subject: 'What can you build?', + text: STEP_TWO_CONTENT, + }); + break; + default: + throw new Error('invalid nurture step'); + } +}; From 831e3f3233f5ef0f9119a3c47d8ec75511ee60c8 Mon Sep 17 00:00:00 2001 From: Sachin Ranchod Date: Mon, 9 Oct 2023 22:44:15 -0700 Subject: [PATCH 2/2] add the connection to the worker too --- apps/zipper.dev/src/server/queue.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/zipper.dev/src/server/queue.ts b/apps/zipper.dev/src/server/queue.ts index b3fde2c75..b775b5beb 100644 --- a/apps/zipper.dev/src/server/queue.ts +++ b/apps/zipper.dev/src/server/queue.ts @@ -19,9 +19,21 @@ const queueWorkersGlobal = global as typeof global & { const initializeWorkers = () => { console.log('[BullMQ] Initializing workers'); return [ - new Worker('nurture', async (job) => { - await sendNurtureEmail(job.data.step, job.data.email); - }), + new Worker( + 'nurture', + async (job) => { + await sendNurtureEmail(job.data.step, job.data.email); + }, + { connection: redis }, + ) + ?.on('completed', (job) => { + console.log(`[Job Queue] Completed nurture job ID ${job?.id}`); + }) + ?.on('failed', (job, err) => { + console.log( + `[Job Queue] Failed nurture job ID ${job?.id} with error ${err}`, + ); + }), new Worker( 'schedule-queue', async (job) => { @@ -96,7 +108,10 @@ const initializeQueues = () => { connection: redis, defaultJobOptions: { removeOnComplete: 1000, removeOnFail: 5000 }, }), - nurture: new Queue('nurture', { connection: redis }), + nurture: new Queue('nurture', { + connection: redis, + defaultJobOptions: { removeOnComplete: 1000, removeOnFail: 5000 }, + }), }; };