import { NoopTinybird, Tinybird } from "@chronark/zod-bird"; import { newId } from "@aigxion/id"; import { auditLogSchemaV1, unkeyAuditLogEvents } from "@aigxion/schema/src/auditlog"; import { ratelimitSchemaV1 } from "@aigxion/schema/src/ratelimit-tinybird"; import { z } from "zod"; import { MaybeArray } from "./types/maybe"; // const datetimeToUnixMilli = z.string().transform((t) => new Date(t).getTime()); /** * `t` has the format `2021-01-01 00:00:00` * * If we transform it as is, we get `1609459200000` which is `2021-01-01 01:00:00` due to fun timezone stuff. * So we split the string at the space and take the date part, and then parse that. */ const dateToUnixMilli = z.string().transform((t) => new Date(t.split(" ").at(0) ?? t).getTime()); export class Analytics { public readonly client: Tinybird | NoopTinybird; constructor(token?: string) { this.client = token ? new Tinybird({ token }) : new NoopTinybird(); } public get ingestSdkTelemetry() { return this.client.buildIngestEndpoint({ datasource: "sdk_telemetry__v1", event: z.object({ runtime: z.string(), platform: z.string(), versions: z.array(z.string()), requestId: z.string(), time: z.number(), }), }); } public ingestAuditLogs( logs: MaybeArray<{ workspaceId: string; event: z.infer; description: string; actor: { type: "user" | "key"; name?: string; id: string; }; resources: Array<{ type: | "key" | "api" | "workspace" | "role" | "permission" | "keyAuth" | "vercelBinding" | "vercelIntegration"; id: string; meta?: Record; }>; context: { userAgent?: string; location: string; }; }>, ) { return this.client.buildIngestEndpoint({ datasource: "audit_logs__v2", event: auditLogSchemaV1 .merge( z.object({ event: unkeyAuditLogEvents, auditLogId: z.string().default(newId("auditLog")), bucket: z.string().default("unkey_mutations"), time: z.number().default(Date.now()), }), ) .transform((l) => ({ ...l, actor: { ...l.actor, meta: l.actor.meta ? JSON.stringify(l.actor.meta) : undefined, }, resources: JSON.stringify(l.resources), })), })(logs); } public get ingestRatelimit() { return this.client.buildIngestEndpoint({ datasource: "ratelimits__v1", event: ratelimitSchemaV1.transform((l) => ({ ...l, resources: JSON.stringify(l.resources), })), }); } public get ingestKeyVerification() { return this.client.buildIngestEndpoint({ datasource: "key_verifications__v2", event: z.object({ workspaceId: z.string(), apiId: z.string(), keyId: z.string(), deniedReason: z .enum([ "RATE_LIMITED", "USAGE_EXCEEDED", "FORBIDDEN", "UNAUTHORIZED", "DISABLED", "INSUFFICIENT_PERMISSIONS", ]) .optional(), time: z.number(), ipAddress: z.string().default(""), userAgent: z.string().default(""), requestedResource: z.string().default(""), edgeRegion: z.string().default(""), region: z.string(), // deprecated, use deniedReason ratelimited: z.boolean().default(false), // deprecated, use deniedReason usageExceeded: z.boolean().default(false), }), }); } public get getVerificationsDaily() { return this.client.buildPipe({ pipe: "get_verifications_daily__v1", parameters: z.object({ workspaceId: z.string(), apiId: z.string(), keyId: z.string().optional(), start: z.number().optional(), end: z.number().optional(), }), data: z.object({ time: dateToUnixMilli, success: z.number(), rateLimited: z.number(), usageExceeded: z.number(), }), }); } }