2
0

📈 Add telemetry webhook

Closes #357
This commit is contained in:
Baptiste Arnaud
2023-03-14 14:18:05 +01:00
parent e7132116f4
commit 9ca17e4e0b
22 changed files with 523 additions and 34 deletions

View File

@@ -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<typeof eventSchema>

View File

@@ -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",

View File

@@ -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()

View File

@@ -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"
}
}

View File

@@ -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',
}
}