hts/apps/api/src/pkg/analytics.ts

148 lines
4.1 KiB
TypeScript

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<typeof unkeyAuditLogEvents>;
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<string, string | number | boolean>;
}>;
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(),
}),
});
}
}