Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cool-llamas-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@scrawn/analytics": patch
"@scrawn/core": patch
---

feat: migrate proto from jspb/google-protobuf to ts-proto/@bufbuild/protobuf
26 changes: 17 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/ai-token-stream-expr-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function main() {
return;
}
console.log(
`Streamed ${response.getEventsprocessed()} token usage events with expression pricing`
`Streamed ${response.eventsProcessed} token usage events with expression pricing`
);
}

Expand Down
4 changes: 2 additions & 2 deletions examples/ai-token-stream-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function fireAndForgetExample() {
return;
}

console.log(`Streamed ${response.getEventsprocessed()} token usage events`);
console.log(`Streamed ${response.eventsProcessed} token usage events`);
}

// Example 2: Return mode
Expand All @@ -59,7 +59,7 @@ async function returnModeExample() {
console.log("Billing failed before processing events");
return;
}
console.log(`Billing complete: ${result.getEventsprocessed()} events processed`);
console.log(`Billing complete: ${result.eventsProcessed} events processed`);
}

async function main() {
Expand Down
2 changes: 1 addition & 1 deletion examples/scrawn/pricerefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
export const TAGS = ["PREMIUM_CALL", "EXTRA_FEE"] as const;
export type ScrawnTag = (typeof TAGS)[number];

export const EXPRESSIONS = [] as const;
export const EXPRESSIONS = ["COMPLEX_FEE", "PER_TOKEN_INPUT"] as const;
export type ScrawnExpr = (typeof EXPRESSIONS)[number];
29 changes: 15 additions & 14 deletions packages/analytics/scripts/gen-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ const OUT_DIR = path.resolve(__dirname, "..", "src");

type FieldEntry = { name: string; type: string; protoName: string };

// ── Parse EventRow.AsObject from query_pb.d.ts ──
// ── Parse EventRow interface from query.ts ──

function parseQueryFields(): { sdkCall: FieldEntry[]; aiToken: FieldEntry[]; payment: FieldEntry[] } {
const dts = fs.readFileSync(path.join(GEN_DIR, "query", "v1", "query_pb.d.ts"), "utf-8");
// Extract EventRow.AsObject properties
const eventRowMatch = dts.match(/export namespace EventRow \{\s*export type AsObject = \{([^}]+)\}/s);
if (!eventRowMatch) throw new Error("Could not find EventRow.AsObject in query_pb.d.ts");
const src = fs.readFileSync(path.join(GEN_DIR, "query", "v1", "query.ts"), "utf-8");

// Extract EventRow interface properties
const eventRowMatch = src.match(/export interface EventRow \{\s*([^}]+)\s*\}/s);
if (!eventRowMatch) throw new Error("Could not find EventRow in query.ts");

const props = eventRowMatch[1];
const fields: FieldEntry[] = [];
Expand All @@ -34,7 +34,8 @@ function parseQueryFields(): { sdkCall: FieldEntry[]; aiToken: FieldEntry[]; pay
const [, name, type] = m;
if (name === "basicUsageType" || name === "debitAmount") continue; // handled below
if (fields.some(f => f.name === name)) continue;
fields.push({ name, type, protoName: name });
const tsType = type === "number" ? "number" : "string";
fields.push({ name, type: tsType, protoName: name });
}

// Map fields to specific groups based on what we know:
Expand Down Expand Up @@ -77,25 +78,25 @@ function parseQueryFields(): { sdkCall: FieldEntry[]; aiToken: FieldEntry[]; pay
// ── Parse per-table enums from data_pb.d.ts ──

function parseTableName(enumName: string): string {
// UsersFieldMap → users, SessionsFieldMap → sessions, etc.
return enumName.replace("FieldMap", "").toLowerCase();
// UsersField → users, SessionsField → sessions, etc.
return enumName.replace("Field", "").toLowerCase();
}

function parseDataFields(): Record<string, FieldEntry[]> {
const dts = fs.readFileSync(path.join(GEN_DIR, "data", "v1", "data_pb.d.ts"), "utf-8");
const src = fs.readFileSync(path.join(GEN_DIR, "data", "v1", "data.ts"), "utf-8");

const tableFields: Record<string, FieldEntry[]> = {};

// Find all FieldMap interfaces
const enumRe = /export interface (\w+FieldMap) \{([^}]+)\}/gs;
// Find all Field enums
const enumRe = /export enum (\w+Field) \{\s*([^}]+)\s*\}/gs;
let m: RegExpExecArray | null;
while ((m = enumRe.exec(dts)) !== null) {
while ((m = enumRe.exec(src)) !== null) {
const enumName = m[1];
const tableName = parseTableName(enumName);
const body = m[2];

const fields: FieldEntry[] = [];
const memberRe = /(\w+):\s*(\d+)/g;
const memberRe = /(\w+)\s*=\s*(\d+)/g;
let mm: RegExpExecArray | null;
while ((mm = memberRe.exec(body)) !== null) {
const protoMember = mm[1];
Expand Down
8 changes: 4 additions & 4 deletions packages/analytics/src/data/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ export abstract class BaseDataBuilder<TFields> {
};
}

protected unwrap(res: { columnsList?: string[]; rowsList?: Array<{ valuesList?: string[] }>; total?: number }): DataQueryResult<TFields> {
const cols = res.columnsList ?? [];
const rows = (res.rowsList ?? []).map((r) => {
const vals = r.valuesList ?? [];
protected unwrap(res: { columns?: string[]; rows?: Array<{ values?: string[] }>; total?: number }): DataQueryResult<TFields> {
const cols = res.columns ?? [];
const rows = (res.rows ?? []).map((r) => {
const vals = r.values ?? [];
const obj: Record<string, string> = {};
cols.forEach((c, i) => { obj[c] = vals[i] ?? ""; });
return obj as unknown as InferRow<TFields>;
Expand Down
117 changes: 54 additions & 63 deletions packages/analytics/src/grpc/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,34 @@ import type { GrpcClient } from "@scrawn/core";

import {
QueryServiceClient,
} from "@scrawn/core";
import type {
QueryEventsRequest,
QueryEventsResponse,
FilterGroup as QFilterGroup,
FilterCondition as QFilterCondition,
Aggregation,
GroupBy,
QueryFilterGroup as QFilterGroup,
QueryFilterCondition as QFilterCondition,
QueryAggregation as Aggregation,
QueryGroupBy as GroupBy,
} from "@scrawn/core";

import {
DataQueryServiceClient,
} from "@scrawn/core";
import type {
QueryRequest,
QueryResponse,
FilterGroup as DFilterGroup,
FilterCondition as DFilterCondition,
OrderBy as DOrderBy,
DataFilterGroup as DFilterGroup,
DataFilterCondition as DFilterCondition,
DataOrderBy as DOrderBy,
} from "@scrawn/core";

import type {
FilterGroup,
Aggregation as AggType,
OrderBy as OrderByType,
} from "../operators.ts";
} from "../operators.js";

function opQuery(op: string): 0 | 1 | 2 | 3 | 4 | 5 | 6 {
function opQuery(op: string): number {
switch (op) {
case "EQ": return 1; case "GT": return 2; case "GTE": return 3;
case "LT": return 4; case "LTE": return 5; case "NEQ": return 6;
Expand All @@ -34,86 +38,73 @@ function opQuery(op: string): 0 | 1 | 2 | 3 | 4 | 5 | 6 {
}

function buildQueryGroup(group: FilterGroup): QFilterGroup {
const fg = new QFilterGroup();
fg.setLogical(group.logical === "AND" ? 1 : 2);
fg.setConditionsList(group.conditions.map((c) => {
const fc = new QFilterCondition();
fc.setField(c.field);
fc.setOperator(opQuery(c.operator));
fc.setValue(c.value);
return fc;
}));
fg.setGroupsList(group.groups.map(buildQueryGroup));
return fg;
return {
logical: group.logical === "AND" ? 1 : 2,
conditions: group.conditions.map((c) => ({
field: c.field,
operator: opQuery(c.operator),
value: c.value,
})),
groups: group.groups.map(buildQueryGroup),
};
}

function buildDataGroup(group: FilterGroup): DFilterGroup {
const fg = new DFilterGroup();
fg.setLogical(group.logical === "AND" ? 1 : 2);
fg.setConditionsList(group.conditions.map((c) => {
const fc = new DFilterCondition();
fc.setField(c.field);
fc.setOperator(opQuery(c.operator));
fc.setValue(c.value);
return fc;
}));
fg.setGroupsList(group.groups.map(buildDataGroup));
return fg;
return {
logical: group.logical === "AND" ? 1 : 2,
conditions: group.conditions.map((c) => ({
field: c.field,
operator: opQuery(c.operator),
value: c.value,
})),
groups: group.groups.map(buildDataGroup),
};
}

export async function callEventQuery(
grpc: GrpcClient,
apiKey: string,
params: { where?: FilterGroup; aggregation?: AggType; groupBy?: string; limit?: number; offset?: number },
): Promise<QueryEventsResponse.AsObject> {
const req = new QueryEventsRequest();
if (params.where) req.setWhere(buildQueryGroup(params.where));
if (params.aggregation) {
const a = new Aggregation();
a.setType(params.aggregation.type === "SUM" ? 1 : 2);
if (params.aggregation.field) a.setField(params.aggregation.field);
req.setAggregation(a);
}
if (params.groupBy) {
const gb = new GroupBy();
gb.setField(params.groupBy);
req.setGroupBy(gb);
}
req.setLimit(params.limit ?? 100);
req.setOffset(params.offset ?? 0);
): Promise<QueryEventsResponse> {
const req: QueryEventsRequest = {
where: params.where ? buildQueryGroup(params.where) : undefined,
aggregation: params.aggregation
? { type: params.aggregation.type === "SUM" ? 1 : 2, field: params.aggregation.field ?? "" }
: undefined,
groupBy: params.groupBy ? { field: params.groupBy } : undefined,
limit: params.limit ?? 100,
offset: params.offset ?? 0,
};

const res = await grpc
.newCall(QueryServiceClient, "queryEvents")
.addMetadata("authorization", `Bearer ${apiKey}`)
.addPayload(req)
.request<QueryEventsResponse>();
return res.toObject();
return res;
}

export async function callDataQuery(
grpc: GrpcClient,
apiKey: string,
tableName: string,
params: { where?: FilterGroup; limit?: number; offset?: number; orderBy?: OrderByType[] },
): Promise<QueryResponse.AsObject> {
const req = new QueryRequest();
req.setTable(tableName);
if (params.where) req.setWhere(buildDataGroup(params.where));
if (params.orderBy && params.orderBy.length > 0) {
req.setOrderByList(params.orderBy.map((o) => {
const ob = new DOrderBy();
ob.setField(o.field);
ob.setDescending(o.descending);
return ob;
}));
}
req.setLimit(params.limit ?? 100);
req.setOffset(params.offset ?? 0);
): Promise<QueryResponse> {
const req: QueryRequest = {
table: tableName,
where: params.where ? buildDataGroup(params.where) : undefined,
orderBy: params.orderBy?.map((o) => ({
field: o.field,
descending: o.descending,
})) ?? [],
limit: params.limit ?? 100,
offset: params.offset ?? 0,
};

const res = await grpc
.newCall(DataQueryServiceClient, "query")
.addMetadata("authorization", `Bearer ${apiKey}`)
.addPayload(req)
.request<QueryResponse>();
return res.toObject();
return res;
}
Loading
Loading