From 9ca17e4e0b9bfa18c0cba6efdb01d78d0cd88084 Mon Sep 17 00:00:00 2001
From: Baptiste Arnaud
Date: Tue, 14 Mar 2023 14:18:05 +0100
Subject: [PATCH] :chart_with_upwards_trend: Add telemetry webhook
Closes #357
---
.../workflows/send-total-results-digest.yml | 21 +++++
apps/builder/package.json | 1 +
.../api/procedures/createCheckoutSession.ts | 8 +-
.../api/procedures/updateSubscription.ts | 14 +++
.../telemetry/api/processTelemetryEvent.ts | 93 +++++++++++++++++++
.../procedures/createWorkspaceProcedure.ts | 13 +++
apps/builder/src/pages/api/auth/adapter.ts | 42 ++++++---
apps/builder/src/pages/api/publicTypebots.ts | 20 +++-
.../src/pages/api/publicTypebots/[id].ts | 19 +++-
.../pages/api/stripe/custom-plan-checkout.ts | 1 +
apps/builder/src/pages/api/stripe/webhook.ts | 43 ++++++++-
apps/builder/src/pages/api/typebots.ts | 12 +++
.../src/utils/server/routers/v1/trpcRouter.ts | 2 +
.../self-hosting/configuration/builder.mdx | 19 +++-
.../chat/api/utils/continueBotFlow.ts | 11 ++-
packages/models/features/telemetry.ts | 89 ++++++++++++++++++
packages/scripts/package.json | 3 +-
packages/scripts/sendTotalResultsDigest.ts | 85 +++++++++++++++++
packages/utils/package.json | 5 +-
.../utils/telemetry/sendTelemetryEvent.ts | 29 ++++++
pnpm-lock.yaml | 23 +++++
turbo.json | 4 +
22 files changed, 523 insertions(+), 34 deletions(-)
create mode 100644 .github/workflows/send-total-results-digest.yml
create mode 100644 apps/builder/src/features/telemetry/api/processTelemetryEvent.ts
create mode 100644 packages/models/features/telemetry.ts
create mode 100644 packages/scripts/sendTotalResultsDigest.ts
create mode 100644 packages/utils/telemetry/sendTelemetryEvent.ts
diff --git a/.github/workflows/send-total-results-digest.yml b/.github/workflows/send-total-results-digest.yml
new file mode 100644
index 00000000000..d0152537634
--- /dev/null
+++ b/.github/workflows/send-total-results-digest.yml
@@ -0,0 +1,21 @@
+name: Send total results daily digest
+
+on:
+ schedule:
+ - cron: '0 5 * * *'
+
+jobs:
+ send:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./packages/scripts
+ env:
+ DATABASE_URL: '${{ secrets.DATABASE_URL }}'
+ TELEMETRY_WEBHOOK_URL: '${{ secrets.TELEMETRY_WEBHOOK_URL }}'
+ TELEMETRY_WEBHOOK_BEARER_TOKEN: '${{ secrets.TELEMETRY_WEBHOOK_BEARER_TOKEN }}'
+ steps:
+ - uses: actions/checkout@v2
+ - uses: pnpm/action-setup@v2.2.2
+ - run: pnpm i --frozen-lockfile
+ - run: pnpm turbo run telemetry:sendTotalResultsDigest
diff --git a/apps/builder/package.json b/apps/builder/package.json
index fa06ae2c9da..04dfe54fc00 100644
--- a/apps/builder/package.json
+++ b/apps/builder/package.json
@@ -78,6 +78,7 @@
"nodemailer": "6.9.1",
"nprogress": "0.2.0",
"papaparse": "5.3.2",
+ "posthog-node": "^2.5.4",
"prettier": "2.8.4",
"qs": "6.11.0",
"react": "18.2.0",
diff --git a/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts b/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts
index 0aa3357e3fb..7da0db1aec1 100644
--- a/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts
+++ b/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts
@@ -107,7 +107,13 @@ export const createCheckoutSession = authenticatedProcedure
name: 'never',
},
mode: 'subscription',
- metadata: { workspaceId, plan, additionalChats, additionalStorage },
+ metadata: {
+ workspaceId,
+ plan,
+ additionalChats,
+ additionalStorage,
+ userId: user.id,
+ },
currency,
billing_address_collection: 'required',
automatic_tax: { enabled: true },
diff --git a/apps/builder/src/features/billing/api/procedures/updateSubscription.ts b/apps/builder/src/features/billing/api/procedures/updateSubscription.ts
index ddaec42d51e..35a573e2357 100644
--- a/apps/builder/src/features/billing/api/procedures/updateSubscription.ts
+++ b/apps/builder/src/features/billing/api/procedures/updateSubscription.ts
@@ -1,3 +1,4 @@
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/utils/server/trpc'
import { TRPCError } from '@trpc/server'
@@ -141,6 +142,19 @@ export const updateSubscription = authenticatedProcedure
},
})
+ await sendTelemetryEvents([
+ {
+ name: 'Subscription updated',
+ workspaceId,
+ userId: user.id,
+ data: {
+ plan,
+ additionalChatsIndex: additionalChats,
+ additionalStorageIndex: additionalStorage,
+ },
+ },
+ ])
+
return { workspace: updatedWorkspace }
}
)
diff --git a/apps/builder/src/features/telemetry/api/processTelemetryEvent.ts b/apps/builder/src/features/telemetry/api/processTelemetryEvent.ts
new file mode 100644
index 00000000000..23e70273d36
--- /dev/null
+++ b/apps/builder/src/features/telemetry/api/processTelemetryEvent.ts
@@ -0,0 +1,93 @@
+import { eventSchema } from 'models/features/telemetry'
+import { z } from 'zod'
+import { PostHog } from 'posthog-node'
+import { TRPCError } from '@trpc/server'
+import got from 'got'
+import { authenticatedProcedure } from '@/utils/server/trpc'
+
+// Only used for the cloud version of Typebot. It's the way it processes telemetry events and inject it to thrid-party services.
+export const processTelemetryEvent = authenticatedProcedure
+ .meta({
+ openapi: {
+ method: 'POST',
+ path: '/t/process',
+ description:
+ "Only used for the cloud version of Typebot. It's the way it processes telemetry events and inject it to thrid-party services.",
+ },
+ })
+ .input(
+ z.object({
+ events: z.array(eventSchema),
+ })
+ )
+ .output(
+ z.object({
+ message: z.literal('Events injected'),
+ })
+ )
+ .query(async ({ input: { events }, ctx: { user } }) => {
+ if (user.email !== process.env.ADMIN_EMAIL)
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Only app admin can process telemetry events',
+ })
+ if (!process.env.POSTHOG_API_KEY)
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Server does not have POSTHOG_API_KEY configured',
+ })
+ const client = new PostHog(process.env.POSTHOG_API_KEY, {
+ host: 'https://eu.posthog.com',
+ })
+
+ events.forEach(async (event) => {
+ if (event.name === 'User created') {
+ client.identify({
+ distinctId: event.userId,
+ properties: event.data,
+ })
+ }
+ if (
+ event.name === 'Workspace created' ||
+ event.name === 'Subscription updated'
+ )
+ client.groupIdentify({
+ groupType: 'workspace',
+ groupKey: event.workspaceId,
+ properties: event.data,
+ })
+ if (
+ event.name === 'Typebot created' ||
+ event.name === 'Typebot published'
+ )
+ client.groupIdentify({
+ groupType: 'typebot',
+ groupKey: event.typebotId,
+ properties: { name: event.data.name },
+ })
+ if (
+ event.name === 'User created' &&
+ process.env.USER_CREATED_WEBHOOK_URL
+ ) {
+ await got.post(process.env.USER_CREATED_WEBHOOK_URL, {
+ json: {
+ email: event.data.email,
+ name: event.data.name ? event.data.name.split(' ')[0] : undefined,
+ },
+ })
+ }
+ const groups: { workspace?: string; typebot?: string } = {}
+ if ('workspaceId' in event) groups['workspace'] = event.workspaceId
+ if ('typebotId' in event) groups['typebot'] = event.typebotId
+ client.capture({
+ distinctId: event.userId,
+ event: event.name,
+ properties: event.data,
+ groups,
+ })
+ })
+
+ await client.shutdownAsync()
+
+ return { message: 'Events injected' }
+ })
diff --git a/apps/builder/src/features/workspace/api/procedures/createWorkspaceProcedure.ts b/apps/builder/src/features/workspace/api/procedures/createWorkspaceProcedure.ts
index 6174ac8775a..e996a5134b8 100644
--- a/apps/builder/src/features/workspace/api/procedures/createWorkspaceProcedure.ts
+++ b/apps/builder/src/features/workspace/api/procedures/createWorkspaceProcedure.ts
@@ -1,3 +1,4 @@
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/utils/server/trpc'
import { TRPCError } from '@trpc/server'
@@ -49,6 +50,18 @@ export const createWorkspaceProcedure = authenticatedProcedure
},
})) as Workspace
+ await sendTelemetryEvents([
+ {
+ name: 'Workspace created',
+ workspaceId: newWorkspace.id,
+ userId: user.id,
+ data: {
+ name,
+ plan,
+ },
+ },
+ ])
+
return {
workspace: newWorkspace,
}
diff --git a/apps/builder/src/pages/api/auth/adapter.ts b/apps/builder/src/pages/api/auth/adapter.ts
index 87019b5c973..bef0c6e72c8 100644
--- a/apps/builder/src/pages/api/auth/adapter.ts
+++ b/apps/builder/src/pages/api/auth/adapter.ts
@@ -2,7 +2,6 @@
import { PrismaClient, Prisma, WorkspaceRole, Session } from 'db'
import type { Adapter, AdapterUser } from 'next-auth/adapters'
import { createId } from '@paralleldrive/cuid2'
-import { got } from 'got'
import { generateId } from 'utils'
import { parseWorkspaceDefaultPlan } from '@/features/workspace'
import {
@@ -10,6 +9,8 @@ import {
convertInvitationsToCollaborations,
joinWorkspaces,
} from '@/features/auth/api'
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
+import { TelemetryEvent } from 'models/features/telemetry'
export function CustomAdapter(p: PrismaClient): Adapter {
return {
@@ -28,6 +29,11 @@ export function CustomAdapter(p: PrismaClient): Adapter {
workspaceInvitations.length === 0
)
throw Error('New users are forbidden')
+
+ const newWorkspaceData = {
+ name: data.name ? `${data.name}'s workspace` : `My workspace`,
+ plan: parseWorkspaceDefaultPlan(data.email),
+ }
const createdUser = await p.user.create({
data: {
...data,
@@ -42,25 +48,35 @@ export function CustomAdapter(p: PrismaClient): Adapter {
create: {
role: WorkspaceRole.ADMIN,
workspace: {
- create: {
- name: data.name
- ? `${data.name}'s workspace`
- : `My workspace`,
- plan: parseWorkspaceDefaultPlan(data.email),
- },
+ create: newWorkspaceData,
},
},
},
onboardingCategories: [],
},
+ include: {
+ workspaces: { select: { workspaceId: true } },
+ },
})
- if (process.env.USER_CREATED_WEBHOOK_URL)
- await got.post(process.env.USER_CREATED_WEBHOOK_URL, {
- json: {
- email: data.email,
- name: data.name ? (data.name as string).split(' ')[0] : undefined,
- },
+ const newWorkspaceId = createdUser.workspaces.pop()?.workspaceId
+ const events: TelemetryEvent[] = []
+ if (newWorkspaceId) {
+ events.push({
+ name: 'Workspace created',
+ workspaceId: newWorkspaceId,
+ userId: createdUser.id,
+ data: newWorkspaceData,
})
+ }
+ events.push({
+ name: 'User created',
+ userId: createdUser.id,
+ data: {
+ email: data.email,
+ name: data.name ? (data.name as string).split(' ')[0] : undefined,
+ },
+ })
+ await sendTelemetryEvents(events)
if (invitations.length > 0)
await convertInvitationsToCollaborations(p, user, invitations)
if (workspaceInvitations.length > 0)
diff --git a/apps/builder/src/pages/api/publicTypebots.ts b/apps/builder/src/pages/api/publicTypebots.ts
index 7b5c1870e83..ebccc22d43b 100644
--- a/apps/builder/src/pages/api/publicTypebots.ts
+++ b/apps/builder/src/pages/api/publicTypebots.ts
@@ -4,6 +4,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { canPublishFileInput } from '@/utils/api/dbRules'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth/api'
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
@@ -23,10 +24,25 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
!(await canPublishFileInput({ userId: user.id, workspaceId, res }))
)
return
- const typebot = await prisma.publicTypebot.create({
+ const publicTypebot = await prisma.publicTypebot.create({
data: { ...data },
+ include: {
+ typebot: { select: { name: true } },
+ },
})
- return res.send(typebot)
+ await sendTelemetryEvents([
+ {
+ name: 'Typebot published',
+ userId: user.id,
+ workspaceId,
+ typebotId: publicTypebot.typebotId,
+ data: {
+ isFirstPublish: true,
+ name: publicTypebot.typebot.name,
+ },
+ },
+ ])
+ return res.send(publicTypebot)
}
return methodNotAllowed(res)
} catch (err) {
diff --git a/apps/builder/src/pages/api/publicTypebots/[id].ts b/apps/builder/src/pages/api/publicTypebots/[id].ts
index 3eba2414a35..9276f5b5a05 100644
--- a/apps/builder/src/pages/api/publicTypebots/[id].ts
+++ b/apps/builder/src/pages/api/publicTypebots/[id].ts
@@ -4,6 +4,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { canPublishFileInput, canWriteTypebots } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth/api'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
@@ -25,11 +26,25 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
!(await canPublishFileInput({ userId: user.id, workspaceId, res }))
)
return
- const typebots = await prisma.publicTypebot.update({
+ const publicTypebot = await prisma.publicTypebot.update({
where: { id },
data,
+ include: {
+ typebot: { select: { name: true } },
+ },
})
- return res.send({ typebots })
+ await sendTelemetryEvents([
+ {
+ name: 'Typebot published',
+ userId: user.id,
+ workspaceId,
+ typebotId: publicTypebot.typebotId,
+ data: {
+ name: publicTypebot.typebot.name,
+ },
+ },
+ ])
+ return res.send({ typebot: publicTypebot })
}
if (req.method === 'DELETE') {
const publishedTypebotId = req.query.id as string
diff --git a/apps/builder/src/pages/api/stripe/custom-plan-checkout.ts b/apps/builder/src/pages/api/stripe/custom-plan-checkout.ts
index b8f91b0d26c..f31bae07995 100644
--- a/apps/builder/src/pages/api/stripe/custom-plan-checkout.ts
+++ b/apps/builder/src/pages/api/stripe/custom-plan-checkout.ts
@@ -36,6 +36,7 @@ const createCheckoutSession = async (userId: string) => {
mode: 'subscription',
metadata: {
claimableCustomPlanId: claimableCustomPlan.id,
+ userId,
},
currency: claimableCustomPlan.currency,
automatic_tax: { enabled: true },
diff --git a/apps/builder/src/pages/api/stripe/webhook.ts b/apps/builder/src/pages/api/stripe/webhook.ts
index c0806cfe6df..a08f6d4db07 100644
--- a/apps/builder/src/pages/api/stripe/webhook.ts
+++ b/apps/builder/src/pages/api/stripe/webhook.ts
@@ -6,6 +6,7 @@ import { buffer } from 'micro'
import prisma from '@/lib/prisma'
import { Plan } from 'db'
import { RequestHandler } from 'next/dist/server/next'
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET)
throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing')
@@ -46,11 +47,17 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
additionalChats: string
additionalStorage: string
workspaceId: string
+ userId: string
}
- | { claimableCustomPlanId: string }
+ | { claimableCustomPlanId: string; userId: string }
if ('plan' in metadata) {
- const { workspaceId, plan, additionalChats, additionalStorage } =
- metadata
+ const {
+ workspaceId,
+ plan,
+ additionalChats,
+ additionalStorage,
+ userId,
+ } = metadata
if (!workspaceId || !plan || !additionalChats || !additionalStorage)
return res
.status(500)
@@ -58,7 +65,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
await prisma.workspace.update({
where: { id: workspaceId },
data: {
- plan: plan,
+ plan,
stripeId: session.customer as string,
additionalChatsIndex: parseInt(additionalChats),
additionalStorageIndex: parseInt(additionalStorage),
@@ -68,8 +75,21 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
storageLimitSecondEmailSentAt: null,
},
})
+
+ await sendTelemetryEvents([
+ {
+ name: 'Subscription updated',
+ workspaceId,
+ userId,
+ data: {
+ plan,
+ additionalChatsIndex: parseInt(additionalChats),
+ additionalStorageIndex: parseInt(additionalStorage),
+ },
+ },
+ ])
} else {
- const { claimableCustomPlanId } = metadata
+ const { claimableCustomPlanId, userId } = metadata
if (!claimableCustomPlanId)
return res
.status(500)
@@ -90,6 +110,19 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
customSeatsLimit: seatsLimit,
},
})
+
+ await sendTelemetryEvents([
+ {
+ name: 'Subscription updated',
+ workspaceId,
+ userId,
+ data: {
+ plan: Plan.CUSTOM,
+ additionalChatsIndex: 0,
+ additionalStorageIndex: 0,
+ },
+ },
+ ])
}
return res.status(200).send({ message: 'workspace upgraded in DB' })
diff --git a/apps/builder/src/pages/api/typebots.ts b/apps/builder/src/pages/api/typebots.ts
index 109d06abd82..efff54977fe 100644
--- a/apps/builder/src/pages/api/typebots.ts
+++ b/apps/builder/src/pages/api/typebots.ts
@@ -11,6 +11,7 @@ import { getAuthenticatedUser } from '@/features/auth/api'
import { parseNewTypebot } from '@/features/dashboard'
import { NewTypebotProps } from '@/features/dashboard/api/parseNewTypebot'
import { omit } from 'utils'
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
@@ -65,6 +66,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
...data,
}),
})
+ await sendTelemetryEvents([
+ {
+ name: 'Typebot created',
+ userId: user.id,
+ workspaceId: typebot.workspaceId,
+ typebotId: typebot.id,
+ data: {
+ name: typebot.name,
+ },
+ },
+ ])
return res.send(typebot)
}
return methodNotAllowed(res)
diff --git a/apps/builder/src/utils/server/routers/v1/trpcRouter.ts b/apps/builder/src/utils/server/routers/v1/trpcRouter.ts
index e2cf8cc3cf6..d3e29ef50ff 100644
--- a/apps/builder/src/utils/server/routers/v1/trpcRouter.ts
+++ b/apps/builder/src/utils/server/routers/v1/trpcRouter.ts
@@ -3,12 +3,14 @@ import { webhookRouter } from '@/features/blocks/integrations/webhook/api'
import { credentialsRouter } from '@/features/credentials/api/router'
import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure'
import { resultsRouter } from '@/features/results/api'
+import { processTelemetryEvent } from '@/features/telemetry/api/processTelemetryEvent'
import { typebotRouter } from '@/features/typebot/api'
import { workspaceRouter } from '@/features/workspace/api'
import { router } from '../../trpc'
export const trpcRouter = router({
getAppVersionProcedure,
+ processTelemetryEvent,
workspace: workspaceRouter,
typebot: typebotRouter,
webhook: webhookRouter,
diff --git a/apps/docs/docs/self-hosting/configuration/builder.mdx b/apps/docs/docs/self-hosting/configuration/builder.mdx
index 607efae0ffd..f513906ea78 100644
--- a/apps/docs/docs/self-hosting/configuration/builder.mdx
+++ b/apps/docs/docs/self-hosting/configuration/builder.mdx
@@ -235,12 +235,23 @@ These can also be added to the `viewer` environment
-Internal Webhooks
+Telemetry
-| Parameter | Default | Description |
-| ------------------------ | ------- | --------------------------------------------------------------------------------------------- |
-| USER_CREATED_WEBHOOK_URL | | Webhook URL called whenever a new user is created (used for importing a new SendGrid contact) |
+| Parameter | Default | Description |
+| ------------------------------ | ------- | --------------------------------------------------------------------------------------------------------------- |
+| TELEMETRY_WEBHOOK_URL | | Webhook URL called whenever a new telemetry event is captured. See this file that lists all the possible events |
+| TELEMETRY_WEBHOOK_BEARER_TOKEN | | Bearer token to add if the request needs to be authenticated |
+
+
+
+PostHog
+
+
+| Parameter | Default | Description |
+| ---------------- | ------- | ---------------- |
+| POSTHOG_API_KEY | | PostHog API Key |
+| POSTHOG_API_HOST | | PostHog API Host |
diff --git a/apps/viewer/src/features/chat/api/utils/continueBotFlow.ts b/apps/viewer/src/features/chat/api/utils/continueBotFlow.ts
index 50a21f74417..bb1adf5e883 100644
--- a/apps/viewer/src/features/chat/api/utils/continueBotFlow.ts
+++ b/apps/viewer/src/features/chat/api/utils/continueBotFlow.ts
@@ -164,11 +164,10 @@ const saveAnswer =
content: reply,
})
- if (reply.includes('http') && block.type === InputBlockType.FILE) {
- answer.storageUsed = await computeStorageUsed(reply)
- }
-
- if (resultId)
+ if (resultId) {
+ if (reply.includes('http') && block.type === InputBlockType.FILE) {
+ answer.storageUsed = await computeStorageUsed(reply)
+ }
await prisma.answer.upsert({
where: {
resultId_blockId_groupId: {
@@ -180,6 +179,8 @@ const saveAnswer =
create: answer as Prisma.AnswerUncheckedCreateInput,
update: answer,
})
+ }
+
return newSessionState
}
diff --git a/packages/models/features/telemetry.ts b/packages/models/features/telemetry.ts
new file mode 100644
index 00000000000..9e6bcbb3321
--- /dev/null
+++ b/packages/models/features/telemetry.ts
@@ -0,0 +1,89 @@
+import { Plan } from 'db'
+import { z } from 'zod'
+
+const userEvent = z.object({
+ userId: z.string(),
+})
+
+const workspaceEvent = userEvent.merge(
+ z.object({
+ workspaceId: z.string(),
+ })
+)
+
+const typebotEvent = workspaceEvent.merge(
+ z.object({
+ typebotId: z.string(),
+ })
+)
+
+const workspaceCreatedEventSchema = workspaceEvent.merge(
+ z.object({
+ name: z.literal('Workspace created'),
+ data: z.object({
+ name: z.string().optional(),
+ plan: z.nativeEnum(Plan),
+ }),
+ })
+)
+
+const userCreatedEventSchema = userEvent.merge(
+ z.object({
+ name: z.literal('User created'),
+ data: z.object({
+ email: z.string(),
+ name: z.string().optional(),
+ }),
+ })
+)
+
+const typebotCreatedEventSchema = typebotEvent.merge(
+ z.object({
+ name: z.literal('Typebot created'),
+ data: z.object({
+ name: z.string(),
+ template: z.string().optional(),
+ }),
+ })
+)
+
+const publishedTypebotEventSchema = typebotEvent.merge(
+ z.object({
+ name: z.literal('Typebot published'),
+ data: z.object({
+ name: z.string(),
+ isFirstPublish: z.literal(true).optional(),
+ }),
+ })
+)
+
+const subscriptionUpdatedEventSchema = workspaceEvent.merge(
+ z.object({
+ name: z.literal('Subscription updated'),
+ data: z.object({
+ plan: z.nativeEnum(Plan),
+ additionalChatsIndex: z.number(),
+ additionalStorageIndex: z.number(),
+ }),
+ })
+)
+
+const newResultsCollectedEventSchema = typebotEvent.merge(
+ z.object({
+ name: z.literal('New results collected'),
+ data: z.object({
+ total: z.number(),
+ }),
+ })
+)
+
+export const eventSchema = z.discriminatedUnion('name', [
+ workspaceCreatedEventSchema,
+ userCreatedEventSchema,
+ typebotCreatedEventSchema,
+ publishedTypebotEventSchema,
+ subscriptionUpdatedEventSchema,
+ newResultsCollectedEventSchema,
+])
+
+export type TelemetryEvent = z.infer
diff --git a/packages/scripts/package.json b/packages/scripts/package.json
index 0725c88c165..9f7680e4240 100644
--- a/packages/scripts/package.json
+++ b/packages/scripts/package.json
@@ -11,7 +11,8 @@
"db:restore": "tsx restoreDatabase.ts",
"db:setCustomPlan": "tsx setCustomPlan.ts",
"db:bulkUpdate": "tsx bulkUpdate.ts",
- "db:fixTypebots": "tsx fixTypebots.ts"
+ "db:fixTypebots": "tsx fixTypebots.ts",
+ "telemetry:sendTotalResultsDigest": "tsx sendTotalResultsDigest.ts"
},
"devDependencies": {
"@types/node": "18.14.0",
diff --git a/packages/scripts/sendTotalResultsDigest.ts b/packages/scripts/sendTotalResultsDigest.ts
new file mode 100644
index 00000000000..e83d84fe6c4
--- /dev/null
+++ b/packages/scripts/sendTotalResultsDigest.ts
@@ -0,0 +1,85 @@
+import { PrismaClient, WorkspaceRole } from 'db'
+import { isDefined } from 'utils'
+import { promptAndSetEnvironment } from './utils'
+import { TelemetryEvent } from 'models/features/telemetry'
+import { sendTelemetryEvents } from 'utils/telemetry/sendTelemetryEvent'
+
+const prisma = new PrismaClient()
+
+export const sendTotalResultsDigest = async () => {
+ await promptAndSetEnvironment('production')
+
+ console.log("Generating total results yesterday's digest...")
+ const todayMidnight = new Date()
+ todayMidnight.setHours(0, 0, 0, 0)
+ const yesterday = new Date(todayMidnight)
+ yesterday.setDate(yesterday.getDate() - 1)
+
+ const results = await prisma.result.groupBy({
+ by: ['typebotId'],
+ _count: {
+ _all: true,
+ },
+ where: {
+ hasStarted: true,
+ createdAt: {
+ gte: yesterday,
+ lt: todayMidnight,
+ },
+ },
+ })
+
+ console.log(
+ `Found ${results.reduce(
+ (total, result) => total + result._count._all,
+ 0
+ )} results collected yesterday.`
+ )
+
+ const workspaces = await prisma.workspace.findMany({
+ where: {
+ typebots: {
+ some: {
+ id: { in: results.map((result) => result.typebotId) },
+ },
+ },
+ },
+ select: {
+ id: true,
+ typebots: { select: { id: true } },
+ members: { select: { userId: true, role: true } },
+ },
+ })
+
+ const resultsWithWorkspaces = results
+ .flatMap((result) => {
+ const workspace = workspaces.find((workspace) =>
+ workspace.typebots.some((typebot) => typebot.id === result.typebotId)
+ )
+ if (!workspace) return
+ return workspace.members
+ .filter((member) => member.role !== WorkspaceRole.GUEST)
+ .map((member) => ({
+ userId: member.userId,
+ workspaceId: workspace.id,
+ typebotId: result.typebotId,
+ totalResultsYesterday: result._count._all,
+ }))
+ })
+ .filter(isDefined)
+
+ const events = resultsWithWorkspaces.map((result) => ({
+ name: 'New results collected',
+ userId: result.userId,
+ workspaceId: result.workspaceId,
+ typebotId: result.typebotId,
+ data: {
+ total: result.totalResultsYesterday,
+ },
+ })) satisfies TelemetryEvent[]
+
+ await sendTelemetryEvents(events)
+ console.log(`Sent ${events.length} events.`)
+}
+
+sendTotalResultsDigest().then()
diff --git a/packages/utils/package.json b/packages/utils/package.json
index db5b0b2b270..02307f2dbec 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -6,10 +6,10 @@
"main": "./index.ts",
"types": "./index.ts",
"devDependencies": {
+ "@paralleldrive/cuid2": "2.2.0",
"@playwright/test": "1.31.1",
"@types/nodemailer": "6.4.7",
"aws-sdk": "2.1321.0",
- "@paralleldrive/cuid2": "2.2.0",
"db": "workspace:*",
"dotenv": "16.0.3",
"models": "workspace:*",
@@ -22,5 +22,8 @@
"aws-sdk": "2.1152.0",
"next": "13.0.0",
"nodemailer": "6.7.8"
+ },
+ "dependencies": {
+ "got": "12.5.3"
}
}
diff --git a/packages/utils/telemetry/sendTelemetryEvent.ts b/packages/utils/telemetry/sendTelemetryEvent.ts
new file mode 100644
index 00000000000..37597602975
--- /dev/null
+++ b/packages/utils/telemetry/sendTelemetryEvent.ts
@@ -0,0 +1,29 @@
+import got from 'got'
+import { TelemetryEvent } from 'models/features/telemetry'
+import { isEmpty, isNotEmpty } from '../utils'
+
+export const sendTelemetryEvents = async (events: TelemetryEvent[]) => {
+ if (isEmpty(process.env.TELEMETRY_WEBHOOK_URL))
+ return { message: 'Telemetry not enabled' }
+
+ try {
+ await got.post(process.env.TELEMETRY_WEBHOOK_URL, {
+ json: { events },
+ headers: {
+ authorization: isNotEmpty(process.env.TELEMETRY_WEBHOOK_BEARER_TOKEN)
+ ? `Bearer ${process.env.TELEMETRY_WEBHOOK_BEARER_TOKEN}`
+ : undefined,
+ },
+ })
+ } catch (err) {
+ console.error('Failed to send event', err)
+ return {
+ message: 'Failed to send event',
+ error: err instanceof Error ? err.message : 'Unknown error',
+ }
+ }
+
+ return {
+ message: 'Event sent',
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 10300f22a98..2eb1d653f37 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -103,6 +103,7 @@ importers:
nodemailer: 6.9.1
nprogress: 0.2.0
papaparse: 5.3.2
+ posthog-node: ^2.5.4
prettier: 2.8.4
qs: 6.11.0
react: 18.2.0
@@ -189,6 +190,7 @@ importers:
nodemailer: 6.9.1
nprogress: 0.2.0
papaparse: 5.3.2
+ posthog-node: 2.5.4
prettier: 2.8.4
qs: 6.11.0
react: 18.2.0
@@ -753,11 +755,14 @@ importers:
aws-sdk: 2.1321.0
db: workspace:*
dotenv: 16.0.3
+ got: 12.5.3
models: workspace:*
next: 13.1.6
nodemailer: 6.9.1
tsconfig: workspace:*
typescript: 4.9.5
+ dependencies:
+ got: 12.5.3
devDependencies:
'@paralleldrive/cuid2': 2.2.0
'@playwright/test': 1.31.1
@@ -8469,6 +8474,15 @@ packages:
- debug
dev: false
+ /axios/0.27.2:
+ resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
+ dependencies:
+ follow-redirects: 1.15.2
+ form-data: 4.0.0
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/axobject-query/3.1.1:
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
dependencies:
@@ -16535,6 +16549,15 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
+ /posthog-node/2.5.4:
+ resolution: {integrity: sha512-CdywlVh0CZU05/3MrBc0qY/zsLdU2X9XSz/yL1qMRhbyZhD8lrnuGlI69G2cpzZtli6S/nu64wcmULz/mFFA5w==}
+ engines: {node: '>=15.0.0'}
+ dependencies:
+ axios: 0.27.2
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/postman-code-generators/1.3.0:
resolution: {integrity: sha512-ikjYTukybZ97SMyyBYNPtcYNpc8/nf5kpRUgThddadC4RkgQwfGBarormcdUQkPKTgQpDd889KVTwTLVGC0RUg==}
engines: {node: '>=6'}
diff --git a/turbo.json b/turbo.json
index 72e216c89d3..3b5905a83f0 100644
--- a/turbo.json
+++ b/turbo.json
@@ -39,6 +39,10 @@
"db:cleanDatabase": {
"dependsOn": ["db#db:generate"],
"cache": false
+ },
+ "telemetry:sendTotalResultsDigest": {
+ "dependsOn": ["db#db:generate"],
+ "cache": false
}
}
}