Skip to content

Commit 292b0b2

Browse files
improve onboarding emails and add conditional Slack follow-up
- Rewrite welcome email to focus on creating a status page - Rewrite follow-up email to promote Slack app installation - Add slack-feedback email variant sent when Slack is already installed - Update cron logic to check integration table and branch accordingly - Change follow-up timing from 2-3 days to 24-48h after signup
1 parent eada667 commit 292b0b2

File tree

7 files changed

+238
-66
lines changed

7 files changed

+238
-66
lines changed
Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import type { NextRequest } from "next/server";
22

3-
import { and, gte, lte } from "@openstatus/db";
3+
import { and, eq, gte, inArray, lte } from "@openstatus/db";
44
import { db } from "@openstatus/db/src/db";
5-
import { user } from "@openstatus/db/src/schema";
6-
import { FollowUpEmail, sendEmail } from "@openstatus/emails";
5+
import {
6+
integration,
7+
user,
8+
usersToWorkspaces,
9+
} from "@openstatus/db/src/schema";
10+
import {
11+
FollowUpEmail,
12+
SlackFeedbackEmail,
13+
sendEmail,
14+
} from "@openstatus/emails";
715

816
export async function GET(request: NextRequest) {
917
const authHeader = request.headers.get("authorization");
@@ -15,23 +23,57 @@ export async function GET(request: NextRequest) {
1523
}
1624

1725
const date1 = new Date();
18-
date1.setDate(date1.getDate() - 3);
26+
date1.setDate(date1.getDate() - 2);
1927
const date2 = new Date();
20-
date2.setDate(date2.getDate() - 2);
28+
date2.setDate(date2.getDate() - 1);
29+
2130
const users = await db
22-
.select()
31+
.select({
32+
email: user.email,
33+
workspaceId: usersToWorkspaces.workspaceId,
34+
})
2335
.from(user)
36+
.innerJoin(usersToWorkspaces, eq(user.id, usersToWorkspaces.userId))
2437
.where(and(gte(user.createdAt, date1), lte(user.createdAt, date2)))
2538
.all();
26-
for (const user of users) {
27-
if (user.email) {
28-
await sendEmail({
29-
from: "Thibault from OpenStatus <thibault@openstatus.dev>",
30-
subject: "How's it going with OpenStatus?",
31-
to: [user.email],
32-
react: FollowUpEmail(),
33-
});
39+
40+
const workspaceIds = [
41+
...new Set(users.map((u) => u.workspaceId).filter(Boolean)),
42+
];
43+
44+
const slackWorkspaceIds = new Set<number>();
45+
if (workspaceIds.length > 0) {
46+
const slackIntegrations = await db
47+
.select({ workspaceId: integration.workspaceId })
48+
.from(integration)
49+
.where(
50+
and(
51+
eq(integration.name, "slack-agent"),
52+
inArray(integration.workspaceId, workspaceIds),
53+
),
54+
)
55+
.all();
56+
for (const row of slackIntegrations) {
57+
if (row.workspaceId) {
58+
slackWorkspaceIds.add(row.workspaceId);
59+
}
3460
}
3561
}
62+
63+
for (const u of users) {
64+
if (!u.email) continue;
65+
const hasSlack = u.workspaceId
66+
? slackWorkspaceIds.has(u.workspaceId)
67+
: false;
68+
69+
await sendEmail({
70+
from: "Thibault from OpenStatus <thibault@openstatus.dev>",
71+
subject: hasSlack
72+
? "How's the Slack app working for you?"
73+
: "Manage incidents from Slack",
74+
to: [u.email],
75+
react: hasSlack ? SlackFeedbackEmail() : FollowUpEmail(),
76+
});
77+
}
3678
return Response.json({ success: true });
3779
}

apps/workflows/src/cron/emails.ts

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,90 @@
1-
import { and, gte, lte } from "@openstatus/db";
1+
import { and, eq, gte, inArray, lte } from "@openstatus/db";
22
import { db } from "@openstatus/db";
3-
import { user } from "@openstatus/db/src/schema";
3+
import {
4+
integration,
5+
user,
6+
usersToWorkspaces,
7+
} from "@openstatus/db/src/schema";
48
import { EmailClient } from "@openstatus/emails";
59
import { env } from "../env";
6-
// import { db } from "../lib/db";
710

811
const email = new EmailClient({ apiKey: env().RESEND_API_KEY });
912

1013
export async function sendFollowUpEmails() {
11-
// Get users created 2-3 days ago
1214
const date1 = new Date();
13-
date1.setDate(date1.getDate() - 3);
15+
date1.setDate(date1.getDate() - 2);
1416
const date2 = new Date();
15-
date2.setDate(date2.getDate() - 2);
17+
date2.setDate(date2.getDate() - 1);
1618

1719
const users = await db
1820
.select({
1921
email: user.email,
22+
workspaceId: usersToWorkspaces.workspaceId,
2023
})
2124
.from(user)
25+
.innerJoin(usersToWorkspaces, eq(user.id, usersToWorkspaces.userId))
2226
.where(and(gte(user.createdAt, date1), lte(user.createdAt, date2)))
2327
.all();
2428

2529
console.log(`Found ${users.length} users to send follow ups.`);
2630

27-
// Filter valid emails
28-
const validEmails = users
29-
.map((u) => u.email)
30-
.filter((email) => email !== null)
31-
// I don't know why but I can't have both filter at the same time
32-
.filter((email) => email.trim() !== "");
31+
const workspaceIds = [
32+
...new Set(users.map((u) => u.workspaceId).filter(Boolean)),
33+
];
34+
35+
const slackWorkspaceIds = new Set<number>();
36+
if (workspaceIds.length > 0) {
37+
const slackIntegrations = await db
38+
.select({ workspaceId: integration.workspaceId })
39+
.from(integration)
40+
.where(
41+
and(
42+
eq(integration.name, "slack-agent"),
43+
inArray(integration.workspaceId, workspaceIds),
44+
),
45+
)
46+
.all();
47+
for (const row of slackIntegrations) {
48+
if (row.workspaceId) {
49+
slackWorkspaceIds.add(row.workspaceId);
50+
}
51+
}
52+
}
53+
54+
const slackEmails: string[] = [];
55+
const noSlackEmails: string[] = [];
56+
57+
for (const u of users) {
58+
if (!u.email || u.email.trim() === "") continue;
59+
const hasSlack = u.workspaceId
60+
? slackWorkspaceIds.has(u.workspaceId)
61+
: false;
62+
if (hasSlack) {
63+
slackEmails.push(u.email);
64+
} else {
65+
noSlackEmails.push(u.email);
66+
}
67+
}
3368

34-
// Chunk emails into batches of 80
3569
const batchSize = 80;
36-
for (let i = 0; i < validEmails.length; i += batchSize) {
37-
const batch = validEmails.slice(i, i + batchSize);
38-
console.log(`Sending batch with ${batch.length} emails...`);
70+
71+
for (let i = 0; i < noSlackEmails.length; i += batchSize) {
72+
const batch = noSlackEmails.slice(i, i + batchSize);
73+
console.log(`Sending follow-up batch with ${batch.length} emails...`);
3974
try {
4075
await email.sendFollowUpBatched({ to: batch });
4176
} catch {
42-
//Stop email send when rate limit error is faced in order to avoid wasteful API calls
77+
console.error("Rate limit exceeded. Stopping further sends.");
78+
break;
79+
}
80+
}
81+
82+
for (let i = 0; i < slackEmails.length; i += batchSize) {
83+
const batch = slackEmails.slice(i, i + batchSize);
84+
console.log(`Sending slack feedback batch with ${batch.length} emails...`);
85+
try {
86+
await email.sendSlackFeedbackBatched({ to: batch });
87+
} catch {
4388
console.error("Rate limit exceeded. Stopping further sends.");
4489
break;
4590
}

packages/emails/emails/followup.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@ const FollowUpEmail = () => {
66
return (
77
<Html>
88
<Head>
9-
<title>How's it going with openstatus?</title>
9+
<title>Manage incidents from Slack</title>
1010
</Head>
11-
<Preview>How's it going with openstatus?</Preview>
11+
<Preview>Update your status page without leaving Slack</Preview>
1212
<Body>
1313
Hey
1414
<br />
1515
<br />
16-
How’s everything going with openstatus so far? Let me know if you run
17-
into any issues, or have any feedback, good or bad!
16+
Quick tip: connect the OpenStatus Slack app and manage incident updates for your status page
17+
directly from Slack — no need to switch tabs during an outage.
1818
<br />
19+
<br />👉{" "}
20+
<a href="https://app.openstatus.dev/agents?ref=email-followup">Install the Slack app</a>
1921
<br />
20-
Thank you,
2122
<br />
23+
When something goes wrong, just mention @openstatus to create or update an incident.
2224
<br />
23-
Thibault Le Ouay Ducasse
25+
<br />
26+
Hit reply if you have questions — happy to help.
27+
<br />
28+
<br />
29+
Thibault Le Ouay Ducasse, co-founder of OpenStatus
2430
<br />
2531
</Body>
2632
</Html>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/** @jsxImportSource react */
2+
3+
import { Body, Head, Html, Preview } from "@react-email/components";
4+
5+
const SlackFeedbackEmail = () => {
6+
return (
7+
<Html>
8+
<Head>
9+
<title>How's the Slack app working for you?</title>
10+
</Head>
11+
<Preview>We'd love your feedback on the OpenStatus Slack app</Preview>
12+
<Body>
13+
Hey
14+
<br />
15+
<br />
16+
I saw you installed the OpenStatus Slack app — thanks for trying it out!
17+
<br />
18+
<br />
19+
Quick question: how's the incident management from Slack going so far?
20+
<br />
21+
<br />
22+
Anything missing or confusing? I'd love to hear what's working and what
23+
we could improve.
24+
<br />
25+
<br />
26+
Just hit reply — I read every response.
27+
<br />
28+
<br />
29+
Thibault Le Ouay Ducasse, co-founder of OpenStatus
30+
<br />
31+
</Body>
32+
</Html>
33+
);
34+
};
35+
36+
export default SlackFeedbackEmail;

packages/emails/emails/welcome.tsx

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,35 @@ const WelcomeEmail = () => {
66
return (
77
<Html>
88
<Head>
9-
<title>Welcome to openstatus</title>
9+
<title>Welcome to OpenStatus</title>
1010
</Head>
11-
<Preview>Few tips to get started with your uptime monitoring</Preview>
11+
<Preview>Set up your status page in under 5 minutes</Preview>
1212

1313
<Body>
1414
Hey 👋
1515
<br />
1616
<br />
17-
Welcome to openstatus <br />
17+
Thanks for signing up for OpenStatus.
1818
<br />
1919
<br />
20-
Openstatus is global uptime monitoring service with status page.
20+
The fastest way to get started: create your status page. It takes under 5 minutes and your
21+
users will have a single place to check if your services are up.
2122
<br />
22-
Here are a few things you can do with openstatus:
23-
<br />- Use our{" "}
24-
<a href="https://docs.openstatus.dev/cli/getting-started/?ref=email-onboarding">
25-
CLI
26-
</a>{" "}
27-
to create, update and trigger your monitors.
28-
<br />- Learn how to monitor a{" "}
29-
<a href="https://docs.openstatus.dev/guides/how-to-monitor-mcp-server?ref=email-onboarding">
30-
MCP server
23+
<br />👉{" "}
24+
<a href="https://app.openstatus.dev/status-pages/create?ref=email-onboarding">
25+
Create your status page
3126
</a>
32-
.
33-
<br />- Explore our uptime monitoring as code{" "}
34-
<a href="https://github.com/openstatusHQ/cli-template/?ref=email-onboarding">
35-
template directory
36-
</a>
37-
.
38-
<br />- Build your own status page with our{" "}
39-
<a href="https://api.openstatus.dev/v1">API</a> and host it where you
40-
want. Here's our{" "}
41-
<a href="https://github.com/openstatusHQ/astro-status-page">
42-
Astro template
43-
</a>{" "}
44-
that you can easily host on CloudFlare.
4527
<br />
4628
<br />
47-
Quick question: How did you learn about us? and why did you sign up?
29+
Want full control? Use our{" "}
30+
<a href="https://www.openstatus.dev/registry?ref=email-onboarding">open source</a> to build
31+
your own status page and host it anywhere.
32+
<br />
4833
<br />
49-
Thank you,
34+
Hit reply if you get stuck — I read every response.
5035
<br />
5136
<br />
52-
Thibault Le Ouay Ducasse, co-founder of openstatus
37+
Thibault Le Ouay Ducasse, co-founder of OpenStatus
5338
<br />
5439
</Body>
5540
</Html>

0 commit comments

Comments
 (0)