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
5 changes: 5 additions & 0 deletions .changeset/eager-bottles-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scrawn/analytics": patch
---
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The changeset bump type is patch, but removing sdkEvent from the publicly exported EventQueries interface and replacing it with basicUsage is a breaking change for any consumer currently calling analytics.query.sdkEvent. Per semver, dropping a named property from a published interface requires at minimum a minor bump, and strictly a major if the package is >= 1.0. Existing user code that destructures sdkEvent will fail at runtime with no compile-time warning until they update.

Suggested change
---
"@scrawn/analytics": patch
---
---
"@scrawn/analytics": major
---


fix: broken analytics layer
14 changes: 14 additions & 0 deletions examples/ai-sdk-wrapper-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as ai from "ai";
import { google } from "@ai-sdk/google";
import { biller } from "./scrawn/biller.js";
import { config } from "dotenv";
import { mul, outputTokens } from "@scrawn/core";
config({ path: ".env.local" });

async function main() {
Expand All @@ -16,6 +17,19 @@ async function main() {
prompt: "Write a 2 sentence story about a robot.",
});

const result1 = await ai.streamText({
model: google("gemini-2.5-flash"),
prompt: "Write a 2 sentence story about a robot.",
onFinish: (event) => {
biller.trackAI({
userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f",
event,
inputDebit: biller.tag("PREMIUM_CALL"),
outputDebit: mul(outputTokens(), 0.0001),
});
},
});

console.log(`Generated: "${await result.text}"\n`);
Comment on lines +20 to 33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 result1 is assigned but never used — the console.log at the end only reads result.text. Since ai.streamText returns a StreamTextResult, the stream is opened but its output is silently discarded. If the intent is to show the response, result1.text should be logged; if the intent is only to demonstrate the onFinish billing hook, the assignment can be dropped entirely.

Suggested change
const result1 = await ai.streamText({
model: google("gemini-2.5-flash"),
prompt: "Write a 2 sentence story about a robot.",
onFinish: (event) => {
biller.trackAI({
userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f",
event,
inputDebit: biller.tag("PREMIUM_CALL"),
outputDebit: mul(outputTokens(), 0.0001),
});
},
});
console.log(`Generated: "${await result.text}"\n`);
const result1 = await ai.streamText({
model: google("gemini-2.5-flash"),
prompt: "Write a 2 sentence story about a robot.",
onFinish: (event) => {
biller.trackAI({
userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f",
event,
inputDebit: biller.tag("PREMIUM_CALL"),
outputDebit: mul(outputTokens(), 0.0001),
});
},
});
console.log(`Generated (wrapped): "${await result.text}"\n`);
console.log(`Generated (raw): "${await result1.text}"\n`);

}

Expand Down
27 changes: 13 additions & 14 deletions examples/analytics-usage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
Analytics,
eq,
neq,
gt,
and,
asc,
Expand All @@ -16,28 +15,28 @@ config({ path: ".env.local" });
async function main() {
const analytics = new Analytics(biller);

const { sdkEvent, aiToken, payment } = analytics.query;
const { basicUsage, aiToken, payment } = analytics.query;
const { users, tags, sessions, expressions, metadata } = analytics.data;

// ── Event Queries ──

// List recent SDK call events
const recentSdkCalls = await sdkEvent
.where(eq(sdkEvent.fields.basicUsageType, "RAW"))
.orderBy(desc(sdkEvent.fields.reportedTimestamp))
const recentSdkCalls = await basicUsage
.where(eq(basicUsage.fields.basicUsageType, "RAW"))
.orderBy(desc(basicUsage.fields.reportedTimestamp))
.limit(10)
.execute();
console.log("Recent SDK calls:", JSON.stringify(recentSdkCalls, null, 2));

// Middleware events with high debit
const expensiveMiddleware = await sdkEvent
const expensiveMiddleware = await basicUsage
.where(
and(
eq(sdkEvent.fields.basicUsageType, "MIDDLEWARE_CALL"),
gt(sdkEvent.fields.debitAmount, 100)
eq(basicUsage.fields.basicUsageType, "MIDDLEWARE_CALL"),
gt(basicUsage.fields.debitAmount, 100)
)
)
.orderBy(desc(sdkEvent.fields.debitAmount))
.orderBy(desc(basicUsage.fields.debitAmount))
.limit(5)
.execute();
console.log(
Expand All @@ -49,15 +48,15 @@ async function main() {
const gpt4Usage = await aiToken
.where(eq(aiToken.fields.model, "gpt-4"))
.orderBy(desc(aiToken.fields.reportedTimestamp))
.limit(20)
.limit(10)
.execute();
console.log("GPT-4 token usage:", JSON.stringify(gpt4Usage, null, 2));

// Total debit per user (aggregation)
const totalByUser = await sdkEvent
.where(gt(sdkEvent.fields.debitAmount, 0))
.aggregate(sum(sdkEvent.fields.debitAmount))
.groupBy(sdkEvent.fields.userId)
const totalByUser = await basicUsage
.where(gt(basicUsage.fields.debitAmount, 0))
.aggregate(sum(basicUsage.fields.debitAmount))
.groupBy(basicUsage.fields.userId)
.limit(10)
.execute();
console.log("Total debit by user:", JSON.stringify(totalByUser, null, 2));
Expand Down
8 changes: 4 additions & 4 deletions packages/analytics/scripts/gen-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type FieldEntry = { name: string; type: string; protoName: string };
// ── Parse EventRow interface from query.ts ──

function parseQueryFields(): {
sdkCall: FieldEntry[];
basicUsage: FieldEntry[];
aiToken: FieldEntry[];
payment: FieldEntry[];
} {
Expand Down Expand Up @@ -63,7 +63,7 @@ function parseQueryFields(): {
{ name: "apiKeyId", type: "string", protoName: "api_key_id" },
];

const sdkCall: FieldEntry[] = [
const basicUsage: FieldEntry[] = [
...common,
{ name: "basicUsageType", type: "string", protoName: "basic_usage_type" },
{ name: "debitAmount", type: "number", protoName: "debit_amount" },
Expand Down Expand Up @@ -100,7 +100,7 @@ function parseQueryFields(): {
{ name: "creditAmount", type: "number", protoName: "credit_amount" },
];

return { sdkCall, aiToken, payment };
return { basicUsage, aiToken, payment };
}

// ── Parse per-table enums from data_pb.d.ts ──
Expand Down Expand Up @@ -183,7 +183,7 @@ function generateFieldsFile(
const queryFields = parseQueryFields();
generateFieldsFile(
{
sdkEvent: queryFields.sdkCall,
basicUsage: queryFields.basicUsage,
aiToken: queryFields.aiToken,
payment: queryFields.payment,
},
Expand Down
16 changes: 8 additions & 8 deletions packages/analytics/src/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Scrawn } from "@scrawn/core";
import type { GrpcClient } from "@scrawn/core";
import { SdkEventBuilder } from "./query/sdkEvent.js";
import { BasicUsageBuilder } from "./query/basicUsage.js";
import { AiTokenBuilder } from "./query/aiToken.js";
import { PaymentBuilder } from "./query/payment.js";
import {
Expand All @@ -16,7 +16,7 @@ import {
* Each builder is pre-scoped to the correct event type.
*/
export interface EventQueries {
sdkEvent: SdkEventBuilder;
basicUsage: BasicUsageBuilder;
aiToken: AiTokenBuilder;
payment: PaymentBuilder;
}
Expand Down Expand Up @@ -46,13 +46,13 @@ export interface DataQueries {
*
* const analytics = new Analytics(biller);
*
* // Query SDK events
* const events = await analytics.query.sdkEvent
* // Query basic usage events
* const events = await analytics.query.basicUsage
* .where(and(
* eq(analytics.query.sdkEvent.fields.sdkCallType, "RAW"),
* gt(analytics.query.sdkEvent.fields.debitAmount, 100),
* eq(analytics.query.basicUsage.fields.basicUsageType, "RAW"),
* gt(analytics.query.basicUsage.fields.debitAmount, 100),
* ))
* .orderBy(desc(analytics.query.sdkEvent.fields.reportedTimestamp))
* .orderBy(desc(analytics.query.basicUsage.fields.reportedTimestamp))
* .limit(10)
* .execute();
*
Expand All @@ -77,7 +77,7 @@ export class Analytics {
const apiKey = biller.apikey;

this.query = {
sdkEvent: new SdkEventBuilder(this.grpc, apiKey),
basicUsage: new BasicUsageBuilder(this.grpc, apiKey),
aiToken: new AiTokenBuilder(this.grpc, apiKey),
payment: new PaymentBuilder(this.grpc, apiKey),
};
Expand Down
11 changes: 11 additions & 0 deletions packages/analytics/src/query/basicUsage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseEventBuilder } from "./base.js";
import type { GrpcClient } from "@scrawn/core";
import { basicUsageFields } from "./fields.js";

export class BasicUsageBuilder extends BaseEventBuilder<
typeof basicUsageFields
> {
constructor(grpc: GrpcClient, apiKey: string) {
super(basicUsageFields, "BASIC_USAGE", grpc, apiKey);
}
}
2 changes: 1 addition & 1 deletion packages/analytics/src/query/fields.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// AUTO-GENERATED by scripts/gen-fields.ts — do not edit
import { FieldRef } from "../fieldRef.js";

export const sdkEventFields = {
export const basicUsageFields = {
eventId: new FieldRef<string>("eventId"),
eventType: new FieldRef<string>("eventType"),
userId: new FieldRef<string>("userId"),
Expand Down
9 changes: 0 additions & 9 deletions packages/analytics/src/query/sdkEvent.ts

This file was deleted.

Loading