Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: change to per-product usageV2 topic #6113

Merged
merged 4 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 5 additions & 2 deletions packages/service-utils/src/cf-worker/usageV2.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UsageV2Event } from "../core/usageV2.js";
import type { ServiceName } from "src/core/services.js";
import { type UsageV2Event, getTopicName } from "../core/usageV2.js";

/**
* Send events to Kafka.
Expand All @@ -19,6 +20,7 @@ export async function sendUsageV2Events(
events: UsageV2Event[],
options: {
environment: "development" | "production";
productName: ServiceName;
serviceKey: string;
},
): Promise<void> {
Expand All @@ -27,7 +29,8 @@ export async function sendUsageV2Events(
? "https://u.thirdweb.com"
: "https://u.thirdweb-dev.com";

const resp = await fetch(`${baseUrl}/usage-v2/raw-events`, {
const topic = getTopicName(options.productName);
const resp = await fetch(`${baseUrl}/usage-v2/${topic}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
16 changes: 9 additions & 7 deletions packages/service-utils/src/core/usageV2.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ServiceName } from "src/node/index.js";

export interface UsageV2Event {
/**
* A unique identifier for the event. Defaults to a random UUID.
Expand All @@ -8,10 +10,6 @@ export interface UsageV2Event {
* The event timestamp. Defaults to now().
*/
created_at?: Date;
/**
* The source of the event. Example: "storage"
*/
source: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is no longer needed because events are sent to product-specific topics (and stored to product-specific tables).

/**
* The action of the event. Example: "upload"
*/
Expand Down Expand Up @@ -49,8 +47,12 @@ export interface UsageV2Event {
*/
product_version?: string;
/**
* An object of service-specific data. Example: "file_size_bytes"
* It is safe to pass any new JSON-serializable data here before updating the usageV2 schema.
* An object of arbitrary key-value pairs.
* Values can be boolean, number, string, Date, or null.
*/
data: Record<string, unknown>;
[key: string]: boolean | number | string | Date | null | undefined;
Copy link

Choose a reason for hiding this comment

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

Using an index signature here could cause TypeScript to miss type errors on the explicitly defined properties in UsageV2Event. A safer approach would be to add a dedicated data property with type Record<string, boolean | number | string | Date | null> to handle the arbitrary key-value pairs, while keeping the explicit properties type-safe.

Spotted by Graphite Reviewer

Is this helpful? React 👍 or 👎 to let us know.

}

export function getTopicName(productName: ServiceName) {
return `usage_v2.raw_${productName}`;
}
25 changes: 11 additions & 14 deletions packages/service-utils/src/node/usageV2.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { randomUUID } from "node:crypto";
import { checkServerIdentity } from "node:tls";
import { Kafka, type Producer } from "kafkajs";
import type { UsageV2Event } from "../core/usageV2.js";

const TOPIC_USAGE_V2 = "usage_v2.raw_events";
import type { ServiceName } from "src/core/services.js";
import { type UsageV2Event, getTopicName } from "../core/usageV2.js";

/**
* Creates a UsageV2Producer which opens a persistent TCP connection.
Expand All @@ -21,6 +20,7 @@ const TOPIC_USAGE_V2 = "usage_v2.raw_events";
export class UsageV2Producer {
private kafka: Kafka;
private producer: Producer | null = null;
private productName: ServiceName;

constructor(config: {
/**
Expand All @@ -31,6 +31,10 @@ export class UsageV2Producer {
* The environment the service is running in.
*/
environment: "development" | "production";
/**
* The product "source" where usage is coming from.
*/
productName: ServiceName;

username: string;
password: string;
Expand All @@ -52,6 +56,8 @@ export class UsageV2Producer {
password: config.password,
},
});

this.productName = config.productName;
}

/**
Expand Down Expand Up @@ -82,27 +88,18 @@ export class UsageV2Producer {

const parsedEvents = events.map((event) => {
return {
...event,
id: event.id ?? randomUUID(),
created_at: event.created_at ?? new Date(),
source: event.source,
action: event.action,
// Remove the "team_" prefix, if any.
team_id: event.team_id.startsWith("team_")
? event.team_id.slice(5)
: event.team_id,
project_id: event.project_id,
sdk_name: event.sdk_name,
sdk_platform: event.sdk_platform,
sdk_version: event.sdk_version,
sdk_os: event.sdk_os,
product_name: event.product_name,
product_version: event.product_version,
data: JSON.stringify(event.data),
};
Comment on lines 90 to 98
Copy link

Choose a reason for hiding this comment

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

Consider moving ...event after the team_id assignment. Currently, the explicit property assignments will be overridden by the spread operator, preventing the defaults from taking effect. The intended order should be:

return {
  id: event.id ?? randomUUID(),
  created_at: event.created_at ?? new Date(),
  team_id: event.team_id.startsWith("team_")
    ? event.team_id.slice(5)
    : event.team_id,
  ...event
};

Spotted by Graphite Reviewer

Is this helpful? React 👍 or 👎 to let us know.

});

await this.producer.send({
topic: TOPIC_USAGE_V2,
topic: getTopicName(this.productName),
messages: parsedEvents.map((event) => ({
value: JSON.stringify(event),
})),
Expand Down
Loading