89
packages/models/features/telemetry.ts
Normal file
89
packages/models/features/telemetry.ts
Normal 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>
|
||||
@@ -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",
|
||||
|
||||
85
packages/scripts/sendTotalResultsDigest.ts
Normal file
85
packages/scripts/sendTotalResultsDigest.ts
Normal 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()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
29
packages/utils/telemetry/sendTelemetryEvent.ts
Normal file
29
packages/utils/telemetry/sendTelemetryEvent.ts
Normal 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',
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user