wip
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"@openpanel/queue": "workspace:*",
|
||||
"@openpanel/redis": "workspace:*",
|
||||
"bullmq": "^5.63.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"express": "^4.18.2",
|
||||
"groupmq": "catalog:",
|
||||
"prom-client": "^15.1.3",
|
||||
|
||||
@@ -39,6 +39,11 @@ export async function bootCron() {
|
||||
type: 'insightsDaily',
|
||||
pattern: '0 2 * * *',
|
||||
},
|
||||
{
|
||||
name: 'onboarding',
|
||||
type: 'onboarding',
|
||||
pattern: '0 10 * * *',
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.SELF_HOSTED && process.env.NODE_ENV === 'production') {
|
||||
|
||||
180
apps/worker/src/jobs/cron.onboarding.ts
Normal file
180
apps/worker/src/jobs/cron.onboarding.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import type { Job } from 'bullmq';
|
||||
|
||||
import { db } from '@openpanel/db';
|
||||
import { sendEmail } from '@openpanel/email';
|
||||
import type { CronQueuePayload } from '@openpanel/queue';
|
||||
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const EMAIL_SCHEDULE = {
|
||||
1: 0, // Welcome email - Day 0
|
||||
2: 2, // What to track - Day 2
|
||||
3: 6, // Dashboards - Day 6
|
||||
4: 14, // Replace stack - Day 14
|
||||
5: 26, // Trial ending - Day 26
|
||||
} as const;
|
||||
|
||||
export async function onboardingJob(job: Job<CronQueuePayload>) {
|
||||
logger.info('Starting onboarding email job');
|
||||
|
||||
// Fetch organizations with their creators who are in onboarding
|
||||
const organizations = await db.organization.findMany({
|
||||
where: {
|
||||
createdByUserId: {
|
||||
not: null,
|
||||
},
|
||||
createdBy: {
|
||||
onboarding: {
|
||||
not: null,
|
||||
gte: 1,
|
||||
lte: 5,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
createdBy: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
onboarding: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Found ${organizations.length} organizations with creators in onboarding`);
|
||||
|
||||
let emailsSent = 0;
|
||||
let usersCompleted = 0;
|
||||
let usersSkipped = 0;
|
||||
|
||||
for (const org of organizations) {
|
||||
if (!org.createdBy || !org.createdByUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const user = org.createdBy;
|
||||
|
||||
// Check if organization has active subscription
|
||||
if (org.subscriptionStatus === 'active') {
|
||||
// Stop onboarding for users with active subscriptions
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { onboarding: null },
|
||||
});
|
||||
usersCompleted++;
|
||||
logger.info(`Stopped onboarding for user ${user.id} (active subscription)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!user.onboarding || user.onboarding < 1 || user.onboarding > 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use organization creation date instead of user registration date
|
||||
const daysSinceOrgCreation = differenceInDays(new Date(), org.createdAt);
|
||||
const requiredDays = EMAIL_SCHEDULE[user.onboarding as keyof typeof EMAIL_SCHEDULE];
|
||||
|
||||
if (daysSinceOrgCreation < requiredDays) {
|
||||
usersSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const dashboardUrl = `${process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL || 'https://dashboard.openpanel.dev'}/${org.id}`;
|
||||
const billingUrl = `${process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL || 'https://dashboard.openpanel.dev'}/${org.id}/billing`;
|
||||
|
||||
try {
|
||||
// Send appropriate email based on onboarding step
|
||||
switch (user.onboarding) {
|
||||
case 1: {
|
||||
// Welcome email
|
||||
await sendEmail('onboarding-welcome', {
|
||||
to: user.email,
|
||||
data: {
|
||||
firstName: user.firstName || undefined,
|
||||
dashboardUrl,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// What to track email
|
||||
await sendEmail('onboarding-what-to-track', {
|
||||
to: user.email,
|
||||
data: {
|
||||
firstName: user.firstName || undefined,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// Dashboards email
|
||||
await sendEmail('onboarding-dashboards', {
|
||||
to: user.email,
|
||||
data: {
|
||||
firstName: user.firstName || undefined,
|
||||
dashboardUrl,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
// Replace stack email
|
||||
await sendEmail('onboarding-replace-stack', {
|
||||
to: user.email,
|
||||
data: {
|
||||
firstName: user.firstName || undefined,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
// Trial ending email
|
||||
await sendEmail('onboarding-trial-ending', {
|
||||
to: user.email,
|
||||
data: {
|
||||
firstName: user.firstName || undefined,
|
||||
organizationName: org.name,
|
||||
billingUrl,
|
||||
recommendedPlan: undefined, // TODO: Calculate based on usage
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Increment onboarding state
|
||||
const nextOnboardingState = user.onboarding + 1;
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
onboarding: nextOnboardingState > 5 ? null : nextOnboardingState,
|
||||
},
|
||||
});
|
||||
|
||||
emailsSent++;
|
||||
logger.info(`Sent onboarding email ${user.onboarding} to user ${user.id} for org ${org.id}`);
|
||||
|
||||
if (nextOnboardingState > 5) {
|
||||
usersCompleted++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to send onboarding email to user ${user.id}`, {
|
||||
error,
|
||||
onboardingStep: user.onboarding,
|
||||
organizationId: org.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Completed onboarding email job', {
|
||||
totalOrganizations: organizations.length,
|
||||
emailsSent,
|
||||
usersCompleted,
|
||||
usersSkipped,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { eventBuffer, profileBuffer, sessionBuffer } from '@openpanel/db';
|
||||
import type { CronQueuePayload } from '@openpanel/queue';
|
||||
|
||||
import { jobdeleteProjects } from './cron.delete-projects';
|
||||
import { onboardingJob } from './cron.onboarding';
|
||||
import { ping } from './cron.ping';
|
||||
import { salt } from './cron.salt';
|
||||
import { insightsDailyJob } from './insights';
|
||||
@@ -31,5 +32,8 @@ export async function cronJob(job: Job<CronQueuePayload>) {
|
||||
case 'insightsDaily': {
|
||||
return await insightsDailyJob(job);
|
||||
}
|
||||
case 'onboarding': {
|
||||
return await onboardingJob(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."users" ADD COLUMN "onboarding" INTEGER;
|
||||
@@ -94,6 +94,7 @@ model User {
|
||||
email String @unique
|
||||
firstName String?
|
||||
lastName String?
|
||||
onboarding Int? // null = disabled/completed, 1-5 = next email step
|
||||
createdOrganizations Organization[] @relation("organizationCreatedBy")
|
||||
subscriptions Organization[] @relation("subscriptionCreatedBy")
|
||||
membership Member[]
|
||||
|
||||
104
packages/email/onboarding-emails.md
Normal file
104
packages/email/onboarding-emails.md
Normal file
@@ -0,0 +1,104 @@
|
||||
These emails have good bones but yeah, they have that ChatGPT sheen. The structure is too neat, the bullet points feel mechanical, and phrases like "really opens up" and "usually clicks" are dead giveaways.
|
||||
|
||||
Here's my pass at them:
|
||||
|
||||
---
|
||||
|
||||
**Email 1 - Welcome (Day 0)**
|
||||
|
||||
Subject: You're in
|
||||
|
||||
Hi,
|
||||
|
||||
Thanks for trying OpenPanel.
|
||||
|
||||
We built OpenPanel because most analytics tools are either too expensive, too complicated, or both. OpenPanel is different.
|
||||
|
||||
If you already have setup your tracking you should see your dashboard getting filled up. If you come from another provider and want to import your old events you can do that in our project settings.
|
||||
|
||||
If you can't find your provider just reach out and we'll help you out.
|
||||
|
||||
Reach out if you have any questions. I answer all emails.
|
||||
|
||||
Carl
|
||||
|
||||
---
|
||||
|
||||
**Email 2 - What to track (Day 2)**
|
||||
|
||||
Subject: What's actually worth tracking
|
||||
|
||||
Hi,
|
||||
|
||||
Track the moments that tell you whether your product is working. Track things that matters to your product the most and then you can easily create funnels or conversions reports to understand what happening.
|
||||
|
||||
For most products, that's something like:
|
||||
- Signups
|
||||
- The first meaningful action (create something, send something, buy something)
|
||||
- Return visits
|
||||
|
||||
You don't need 50 events. Five good ones will tell you more than fifty random ones.
|
||||
|
||||
If you're not sure whether something's worth tracking, just ask. I'm happy to look at your setup.
|
||||
|
||||
Carl
|
||||
|
||||
---
|
||||
|
||||
**Email 3 - Dashboards (Day 6)**
|
||||
|
||||
Subject: The part most people skip
|
||||
|
||||
Hi,
|
||||
|
||||
Tracking events is the easy part. The value comes from actually looking at them.
|
||||
|
||||
If you haven't yet, try building a simple dashboard. Pick one thing you care about and visualize it. Could be:
|
||||
|
||||
- How many people sign up and then actually do something
|
||||
- Where users drop off in a flow (funnel)
|
||||
- Which pages lead to conversions (entry page -> CTA)
|
||||
|
||||
This is usually when people go from "I have analytics" to "I understand what's happening." It's a different feeling.
|
||||
|
||||
Takes maybe 10 minutes to set up. Worth it.
|
||||
|
||||
Carl
|
||||
|
||||
---
|
||||
|
||||
**Email 4 - Replace the stack (Day 14)**
|
||||
|
||||
Subject: One provider to rule them all
|
||||
|
||||
Hi,
|
||||
|
||||
A lot of people who sign up are using multiple tools: something for traffic, something for product analytics and something else for seeing raw events.
|
||||
|
||||
OpenPanel can replace that whole setup.
|
||||
|
||||
If you're still thinking of web analytics and product analytics as separate things, try combining them in a single dashboard. Traffic sources on top, user behavior below. That view tends to be more useful than either one alone.
|
||||
|
||||
OpenPanel should be able to replace all of them, you can just reach out if you feel like something is missing.
|
||||
|
||||
Carl
|
||||
|
||||
---
|
||||
|
||||
**Email 5 - Trial ending (Day 26)**
|
||||
|
||||
Subject: Your trial ends in a few days
|
||||
|
||||
Hi,
|
||||
|
||||
Quick heads up: your OpenPanel trial ends soon.
|
||||
|
||||
Your tracking will keep working, but you won't be able to see new data until you upgrade. Everything you've built so far (dashboards, reports, event history) stays intact.
|
||||
|
||||
If OpenPanel has been useful, upgrading just keeps it going. Plans start at $2.50/month and based on your usage we recommend {xxxx}.
|
||||
|
||||
If something's holding you back, I'd like to hear about it. Just reply.
|
||||
|
||||
Your project will recieve events for the next 30 days, if you haven't upgraded by then we'll remove your workspace and projects.
|
||||
|
||||
Carl
|
||||
@@ -4,6 +4,21 @@ import EmailResetPassword, {
|
||||
zEmailResetPassword,
|
||||
} from './email-reset-password';
|
||||
import TrailEndingSoon, { zTrailEndingSoon } from './trial-ending-soon';
|
||||
import OnboardingWelcome, {
|
||||
zOnboardingWelcome,
|
||||
} from './onboarding-welcome';
|
||||
import OnboardingWhatToTrack, {
|
||||
zOnboardingWhatToTrack,
|
||||
} from './onboarding-what-to-track';
|
||||
import OnboardingDashboards, {
|
||||
zOnboardingDashboards,
|
||||
} from './onboarding-dashboards';
|
||||
import OnboardingReplaceStack, {
|
||||
zOnboardingReplaceStack,
|
||||
} from './onboarding-replace-stack';
|
||||
import OnboardingTrialEnding, {
|
||||
zOnboardingTrialEnding,
|
||||
} from './onboarding-trial-ending';
|
||||
|
||||
export const templates = {
|
||||
invite: {
|
||||
@@ -24,6 +39,31 @@ export const templates = {
|
||||
Component: TrailEndingSoon,
|
||||
schema: zTrailEndingSoon,
|
||||
},
|
||||
'onboarding-welcome': {
|
||||
subject: () => "You're in",
|
||||
Component: OnboardingWelcome,
|
||||
schema: zOnboardingWelcome,
|
||||
},
|
||||
'onboarding-what-to-track': {
|
||||
subject: () => "What's actually worth tracking",
|
||||
Component: OnboardingWhatToTrack,
|
||||
schema: zOnboardingWhatToTrack,
|
||||
},
|
||||
'onboarding-dashboards': {
|
||||
subject: () => 'The part most people skip',
|
||||
Component: OnboardingDashboards,
|
||||
schema: zOnboardingDashboards,
|
||||
},
|
||||
'onboarding-replace-stack': {
|
||||
subject: () => 'One provider to rule them all',
|
||||
Component: OnboardingReplaceStack,
|
||||
schema: zOnboardingReplaceStack,
|
||||
},
|
||||
'onboarding-trial-ending': {
|
||||
subject: () => 'Your trial ends in a few days',
|
||||
Component: OnboardingTrialEnding,
|
||||
schema: zOnboardingTrialEnding,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Templates = typeof templates;
|
||||
|
||||
49
packages/email/src/emails/onboarding-dashboards.tsx
Normal file
49
packages/email/src/emails/onboarding-dashboards.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Link, Text } from '@react-email/components';
|
||||
import React from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export const zOnboardingDashboards = z.object({
|
||||
firstName: z.string().optional(),
|
||||
dashboardUrl: z.string(),
|
||||
});
|
||||
|
||||
export type Props = z.infer<typeof zOnboardingDashboards>;
|
||||
export default OnboardingDashboards;
|
||||
export function OnboardingDashboards({
|
||||
firstName,
|
||||
dashboardUrl = 'https://dashboard.openpanel.dev',
|
||||
}: Props) {
|
||||
const newUrl = new URL(dashboardUrl);
|
||||
newUrl.searchParams.set('utm_source', 'email');
|
||||
newUrl.searchParams.set('utm_medium', 'email');
|
||||
newUrl.searchParams.set('utm_campaign', 'onboarding-dashboards');
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||
<Text>
|
||||
Tracking events is the easy part. The value comes from actually looking
|
||||
at them.
|
||||
</Text>
|
||||
<Text>
|
||||
If you haven't yet, try building a simple dashboard. Pick one thing you
|
||||
care about and visualize it. Could be:
|
||||
</Text>
|
||||
<Text>
|
||||
- How many people sign up and then actually do something
|
||||
</Text>
|
||||
<Text>- Where users drop off in a flow (funnel)</Text>
|
||||
<Text>- Which pages lead to conversions (entry page → CTA)</Text>
|
||||
<Text>
|
||||
This is usually when people go from "I have analytics" to "I understand
|
||||
what's happening." It's a different feeling.
|
||||
</Text>
|
||||
<Text>Takes maybe 10 minutes to set up. Worth it.</Text>
|
||||
<Text>
|
||||
<Link href={newUrl.toString()}>Create your first dashboard</Link>
|
||||
</Text>
|
||||
<Text>Carl</Text>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
37
packages/email/src/emails/onboarding-replace-stack.tsx
Normal file
37
packages/email/src/emails/onboarding-replace-stack.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Text } from '@react-email/components';
|
||||
import React from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export const zOnboardingReplaceStack = z.object({
|
||||
firstName: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Props = z.infer<typeof zOnboardingReplaceStack>;
|
||||
export default OnboardingReplaceStack;
|
||||
export function OnboardingReplaceStack({
|
||||
firstName,
|
||||
}: Props) {
|
||||
return (
|
||||
<Layout>
|
||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||
<Text>
|
||||
A lot of people who sign up are using multiple tools: something for
|
||||
traffic, something for product analytics and something else for seeing
|
||||
raw events.
|
||||
</Text>
|
||||
<Text>OpenPanel can replace that whole setup.</Text>
|
||||
<Text>
|
||||
If you're still thinking of web analytics and product analytics as
|
||||
separate things, try combining them in a single dashboard. Traffic
|
||||
sources on top, user behavior below. That view tends to be more useful
|
||||
than either one alone.
|
||||
</Text>
|
||||
<Text>
|
||||
OpenPanel should be able to replace all of them, you can just reach out
|
||||
if you feel like something is missing.
|
||||
</Text>
|
||||
<Text>Carl</Text>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
66
packages/email/src/emails/onboarding-trial-ending.tsx
Normal file
66
packages/email/src/emails/onboarding-trial-ending.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Button, Link, Text } from '@react-email/components';
|
||||
import React from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export const zOnboardingTrialEnding = z.object({
|
||||
firstName: z.string().optional(),
|
||||
organizationName: z.string(),
|
||||
billingUrl: z.string(),
|
||||
recommendedPlan: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Props = z.infer<typeof zOnboardingTrialEnding>;
|
||||
export default OnboardingTrialEnding;
|
||||
export function OnboardingTrialEnding({
|
||||
firstName,
|
||||
organizationName = 'your organization',
|
||||
billingUrl = 'https://dashboard.openpanel.dev',
|
||||
recommendedPlan,
|
||||
}: Props) {
|
||||
const newUrl = new URL(billingUrl);
|
||||
newUrl.searchParams.set('utm_source', 'email');
|
||||
newUrl.searchParams.set('utm_medium', 'email');
|
||||
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ending');
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||
<Text>Quick heads up: your OpenPanel trial ends soon.</Text>
|
||||
<Text>
|
||||
Your tracking will keep working, but you won't be able to see new data
|
||||
until you upgrade. Everything you've built so far (dashboards, reports,
|
||||
event history) stays intact.
|
||||
</Text>
|
||||
<Text>
|
||||
If OpenPanel has been useful, upgrading just keeps it going. Plans
|
||||
start at $2.50/month
|
||||
{recommendedPlan ? ` and based on your usage we recommend ${recommendedPlan}` : ''}
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
If something's holding you back, I'd like to hear about it. Just
|
||||
reply.
|
||||
</Text>
|
||||
<Text>
|
||||
Your project will recieve events for the next 30 days, if you haven't
|
||||
upgraded by then we'll remove your workspace and projects.
|
||||
</Text>
|
||||
<Text>
|
||||
<Button
|
||||
href={newUrl.toString()}
|
||||
style={{
|
||||
backgroundColor: '#0070f3',
|
||||
color: 'white',
|
||||
padding: '12px 20px',
|
||||
borderRadius: '5px',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Upgrade Now
|
||||
</Button>
|
||||
</Text>
|
||||
<Text>Carl</Text>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
43
packages/email/src/emails/onboarding-welcome.tsx
Normal file
43
packages/email/src/emails/onboarding-welcome.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Link, Text } from '@react-email/components';
|
||||
import React from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export const zOnboardingWelcome = z.object({
|
||||
firstName: z.string().optional(),
|
||||
dashboardUrl: z.string(),
|
||||
});
|
||||
|
||||
export type Props = z.infer<typeof zOnboardingWelcome>;
|
||||
export default OnboardingWelcome;
|
||||
export function OnboardingWelcome({
|
||||
firstName,
|
||||
dashboardUrl = 'https://dashboard.openpanel.dev',
|
||||
}: Props) {
|
||||
const newUrl = new URL(dashboardUrl);
|
||||
newUrl.searchParams.set('utm_source', 'email');
|
||||
newUrl.searchParams.set('utm_medium', 'email');
|
||||
newUrl.searchParams.set('utm_campaign', 'onboarding-welcome');
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||
<Text>Thanks for trying OpenPanel.</Text>
|
||||
<Text>
|
||||
We built OpenPanel because most analytics tools are either too expensive,
|
||||
too complicated, or both. OpenPanel is different.
|
||||
</Text>
|
||||
<Text>
|
||||
If you already have setup your tracking you should see your dashboard
|
||||
getting filled up. If you come from another provider and want to import
|
||||
your old events you can do that in our{' '}
|
||||
<Link href={newUrl.toString()}>project settings</Link>.
|
||||
</Text>
|
||||
<Text>
|
||||
If you can't find your provider just reach out and we'll help you out.
|
||||
</Text>
|
||||
<Text>Reach out if you have any questions. I answer all emails.</Text>
|
||||
<Text>Carl</Text>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
41
packages/email/src/emails/onboarding-what-to-track.tsx
Normal file
41
packages/email/src/emails/onboarding-what-to-track.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Text } from '@react-email/components';
|
||||
import React from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export const zOnboardingWhatToTrack = z.object({
|
||||
firstName: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Props = z.infer<typeof zOnboardingWhatToTrack>;
|
||||
export default OnboardingWhatToTrack;
|
||||
export function OnboardingWhatToTrack({
|
||||
firstName,
|
||||
}: Props) {
|
||||
return (
|
||||
<Layout>
|
||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||
<Text>
|
||||
Track the moments that tell you whether your product is working. Track
|
||||
things that matters to your product the most and then you can easily
|
||||
create funnels or conversions reports to understand what happening.
|
||||
</Text>
|
||||
<Text>For most products, that's something like:</Text>
|
||||
<Text>- Signups</Text>
|
||||
<Text>
|
||||
- The first meaningful action (create something, send something, buy
|
||||
something)
|
||||
</Text>
|
||||
<Text>- Return visits</Text>
|
||||
<Text>
|
||||
You don't need 50 events. Five good ones will tell you more than fifty
|
||||
random ones.
|
||||
</Text>
|
||||
<Text>
|
||||
If you're not sure whether something's worth tracking, just ask. I'm
|
||||
happy to look at your setup.
|
||||
</Text>
|
||||
<Text>Carl</Text>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -115,6 +115,10 @@ export type CronQueuePayloadInsightsDaily = {
|
||||
type: 'insightsDaily';
|
||||
payload: undefined;
|
||||
};
|
||||
export type CronQueuePayloadOnboarding = {
|
||||
type: 'onboarding';
|
||||
payload: undefined;
|
||||
};
|
||||
export type CronQueuePayload =
|
||||
| CronQueuePayloadSalt
|
||||
| CronQueuePayloadFlushEvents
|
||||
@@ -122,7 +126,8 @@ export type CronQueuePayload =
|
||||
| CronQueuePayloadFlushProfiles
|
||||
| CronQueuePayloadPing
|
||||
| CronQueuePayloadProject
|
||||
| CronQueuePayloadInsightsDaily;
|
||||
| CronQueuePayloadInsightsDaily
|
||||
| CronQueuePayloadOnboarding;
|
||||
|
||||
export type MiscQueuePayloadTrialEndingSoon = {
|
||||
type: 'trialEndingSoon';
|
||||
|
||||
@@ -22,6 +22,13 @@ async function createOrGetOrganization(
|
||||
const TRIAL_DURATION_IN_DAYS = 30;
|
||||
|
||||
if (input.organization) {
|
||||
// Check if this is the user's first organization
|
||||
const existingOrgCount = await db.organization.count({
|
||||
where: {
|
||||
createdByUserId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const organization = await db.organization.create({
|
||||
data: {
|
||||
id: await getId('organization', input.organization),
|
||||
@@ -33,6 +40,14 @@ async function createOrGetOrganization(
|
||||
},
|
||||
});
|
||||
|
||||
// Set onboarding = 1 for first organization creation
|
||||
if (existingOrgCount === 0 && user.onboarding === null) {
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { onboarding: 1 },
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.SELF_HOSTED) {
|
||||
await addTrialEndingSoonJob(
|
||||
organization.id,
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -37,7 +37,7 @@ overrides:
|
||||
|
||||
patchedDependencies:
|
||||
nuqs:
|
||||
hash: 4f93812bf7a04685f9477b2935e3acbf2aa24dfbb1b00b9ae0ed8f7a2877a98e
|
||||
hash: w6thjv3pgywfrbh4sblczc6qpy
|
||||
path: patches/nuqs.patch
|
||||
|
||||
importers:
|
||||
@@ -656,7 +656,7 @@ importers:
|
||||
version: 3.0.1
|
||||
nuqs:
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2(patch_hash=4f93812bf7a04685f9477b2935e3acbf2aa24dfbb1b00b9ae0ed8f7a2877a98e)(@tanstack/react-router@1.132.47(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
|
||||
version: 2.5.2(patch_hash=w6thjv3pgywfrbh4sblczc6qpy)(@tanstack/react-router@1.132.47(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
|
||||
prisma-error-enum:
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3
|
||||
@@ -889,6 +889,9 @@ importers:
|
||||
bullmq:
|
||||
specifier: ^5.63.0
|
||||
version: 5.63.0
|
||||
date-fns:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
express:
|
||||
specifier: ^4.18.2
|
||||
version: 4.18.2
|
||||
@@ -16527,6 +16530,7 @@ packages:
|
||||
tar@6.2.0:
|
||||
resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
|
||||
|
||||
tar@7.4.3:
|
||||
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
||||
@@ -33174,7 +33178,7 @@ snapshots:
|
||||
dependencies:
|
||||
esm-env: esm-env-runtime@0.1.1
|
||||
|
||||
nuqs@2.5.2(patch_hash=4f93812bf7a04685f9477b2935e3acbf2aa24dfbb1b00b9ae0ed8f7a2877a98e)(@tanstack/react-router@1.132.47(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3):
|
||||
nuqs@2.5.2(patch_hash=w6thjv3pgywfrbh4sblczc6qpy)(@tanstack/react-router@1.132.47(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
react: 19.2.3
|
||||
|
||||
Reference in New Issue
Block a user