fix: 🐛 speeds got wiped for some reason, add top event names

This commit is contained in:
amianthus
2026-01-30 17:31:52 +00:00
parent d375d8e412
commit c571c5d380
15 changed files with 205 additions and 19 deletions

View File

@@ -3,6 +3,7 @@ import { z } from "zod"; // zod-bird requires zod v3, not zod/v4
import { createAggregateGroupablePipe } from "./pipes/aggregateGroupablePipe.js";
import { createAggregatePipe } from "./pipes/aggregatePipe.js";
import { createAggregateSimplePipe } from "./pipes/aggregateSimplePipe.js";
import { createListEventNamesPipe } from "./pipes/listEventNamesPipe.js";
import { createListEventsPipe } from "./pipes/listEventsPipe.js";
const TINYBIRD_API_URL = process.env.TINYBIRD_API_URL;
@@ -47,6 +48,7 @@ export const tinybirdPipes = tinybirdClient
aggregateSimple: createAggregateSimplePipe(tinybirdClient),
aggregateGroupable: createAggregateGroupablePipe(tinybirdClient),
listEvents: createListEventsPipe(tinybirdClient),
listEventNames: createListEventNamesPipe(tinybirdClient),
}
: null;
@@ -90,6 +92,8 @@ export type {
AggregatePipeRow,
AggregateSimplePipeParams,
AggregateSimplePipeRow,
ListEventNamesPipeParams,
ListEventNamesPipeRow,
ListEventsPipeParams,
ListEventsPipeRow,
} from "./pipes/index.js";

View File

@@ -26,3 +26,10 @@ export {
listEventsPipeParamsSchema,
listEventsPipeResponseSchema,
} from "./listEventsPipe.js";
export {
createListEventNamesPipe,
type ListEventNamesPipeParams,
type ListEventNamesPipeRow,
listEventNamesPipeParamsSchema,
listEventNamesPipeResponseSchema,
} from "./listEventNamesPipe.js";

View File

@@ -0,0 +1,31 @@
import type { Tinybird } from "@chronark/zod-bird";
import { z } from "zod";
/** Response schema for the list_event_names pipe */
export const listEventNamesPipeResponseSchema = z.object({
event_name: z.string(),
event_count: z.number(),
});
export type ListEventNamesPipeRow = z.infer<
typeof listEventNamesPipeResponseSchema
>;
/** Parameters schema for the list_event_names pipe */
export const listEventNamesPipeParamsSchema = z.object({
org_id: z.string(),
env: z.string(),
limit: z.number().optional(),
});
export type ListEventNamesPipeParams = z.infer<
typeof listEventNamesPipeParamsSchema
>;
/** Creates the list_event_names pipe caller */
export const createListEventNamesPipe = (tb: Tinybird) =>
tb.buildPipe({
pipe: "list_event_names",
parameters: listEventNamesPipeParamsSchema,
data: listEventNamesPipeResponseSchema,
});

View File

@@ -2,6 +2,7 @@ import { aggregate } from "./aggregate.js";
import { getCountAndSum } from "./getCountAndSum.js";
import { getEventById } from "./getEventById.js";
import { getTopEventNames } from "./getTopEventNames.js";
import { listEventNames } from "./listEventNames.js";
import { listRawEvents } from "./listRawEvents.js";
export const eventActions = {
@@ -9,5 +10,6 @@ export const eventActions = {
getCountAndSum,
getEventById,
getTopEventNames,
listEventNames,
listRawEvents,
} as const;

View File

@@ -0,0 +1,34 @@
import { getTinybirdPipes } from "@/external/tinybird/initTinybird.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
export type EventNameWithCount = {
event_name: string;
event_count: number;
};
/** Lists distinct event names for the org sorted by popularity */
export const listEventNames = async ({
ctx,
limit,
}: {
ctx: AutumnContext;
limit?: number;
}): Promise<EventNameWithCount[]> => {
const { org, env, logger } = ctx;
const pipes = getTinybirdPipes();
const startTime = performance.now();
const result = await pipes.listEventNames({
org_id: org.id,
env,
limit,
});
logger.debug("Listed event names", {
queryMs: Math.round(performance.now() - startTime),
count: result.data.length,
});
return result.data;
};

View File

@@ -1,11 +1,13 @@
import { Hono } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { handleGetEventNames } from "./internalHandlers/handleGetEventNames.js";
import { handleListEventNames } from "./internalHandlers/handleListEventNames.js";
import { handleQueryEvents } from "./internalHandlers/handleQueryEvents.js";
import { handleQueryRawEvents } from "./internalHandlers/handleQueryRawEvents.js";
export const internalAnalyticsRouter = new Hono<HonoEnv>();
internalAnalyticsRouter.get("/event_names", ...handleGetEventNames);
internalAnalyticsRouter.get("/event_names/list", ...handleListEventNames);
internalAnalyticsRouter.post("/events", ...handleQueryEvents);
internalAnalyticsRouter.post("/raw", ...handleQueryRawEvents);

View File

@@ -1,6 +1,6 @@
import { type Feature, FeatureType } from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { queryWithCache } from "@/utils/cacheUtils/queryWithCache.js";
import { eventActions } from "../actions/index.js";
import { AnalyticsService } from "../AnalyticsService.js";
/**
@@ -13,17 +13,8 @@ export const handleGetEventNames = createRoute({
AnalyticsService.handleEarlyExit();
const result = await queryWithCache({
ttl: 3600,
key: `top_events:${org.id}_${env}`,
fn: async () => {
const res = await AnalyticsService.getTopEventNames({
ctx,
});
return res?.eventNames;
},
});
const res = await eventActions.getTopEventNames({ ctx });
const result = res.eventNames;
const featureIds: string[] = [];
const eventNames: string[] = [];

View File

@@ -0,0 +1,30 @@
import { z } from "zod/v4";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { eventActions } from "../actions/index.js";
import { AnalyticsService } from "../AnalyticsService.js";
const ListEventNamesSchema = z.object({
limit: z.number().optional(),
});
/**
* List all distinct event names for the org sorted by popularity
*/
export const handleListEventNames = createRoute({
query: ListEventNamesSchema,
handler: async (c) => {
const ctx = c.get("ctx");
const { limit } = c.req.valid("query");
AnalyticsService.handleEarlyExit();
const eventNames = await eventActions.listEventNames({
ctx,
limit,
});
return c.json({
eventNames,
});
},
});

View File

@@ -12,7 +12,7 @@ import { z } from "zod/v4";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { CusService } from "@/internal/customers/CusService.js";
import * as eventActions from "../actions/aggregate.js";
import { eventActions } from "../actions/index.js";
import { AnalyticsService } from "../AnalyticsService.js";
const QueryEventsSchema = z.object({
@@ -27,7 +27,7 @@ const QueryEventsSchema = z.object({
const getTopEvents = async ({ ctx }: { ctx: AutumnContext }) => {
const { features } = ctx;
const topEventNamesRes = await AnalyticsService.getTopEventNames({
const topEventNamesRes = await eventActions.getTopEventNames({
ctx,
});

View File

@@ -4,6 +4,7 @@ import { z } from "zod/v4";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { CusService } from "@/internal/customers/CusService.js";
import { AnalyticsService } from "../AnalyticsService.js";
import { eventActions } from "../actions/index.js";
const QueryRawEventsSchema = z.object({
interval: z.string().nullish(),
@@ -47,14 +48,14 @@ export const handleQueryRawEvents = createRoute({
}
}
const events = await AnalyticsService.getRawEvents({
const events = await eventActions.listRawEvents({
ctx,
params: {
customer_id: customer?.id,
interval,
customer_id: customer?.id ?? undefined,
interval: interval ?? undefined,
customer,
aggregateAll,
},
customer,
aggregateAll,
});
return c.json({

View File

@@ -0,0 +1,12 @@
DESCRIPTION >
Aggregates distinct event names with counts per org/env.
Used for listing available event names sorted by popularity.
SCHEMA >
`org_id` String,
`env` String,
`event_name` String,
`event_count` SimpleAggregateFunction(sum, UInt64)
ENGINE "AggregatingMergeTree"
ENGINE_SORTING_KEY "org_id, env, event_name"

View File

@@ -0,0 +1,16 @@
DESCRIPTION >
Materializes event names with counts from events table.
Used for listing available event names sorted by popularity.
NODE materialize
SQL >
SELECT
org_id,
env,
event_name,
count() as event_count
FROM events
GROUP BY org_id, env, event_name
TYPE materialized
DATASOURCE event_names_mv

View File

@@ -0,0 +1,21 @@
DESCRIPTION >
Lists distinct event names for an org/env sorted by popularity (event count).
TOKEN "list_event_names_read" READ
NODE endpoint
TYPE endpoint
SQL >
%
SELECT
event_name,
sum(event_count) as event_count
FROM event_names_mv
WHERE
org_id = {{ String(org_id, '') }}
AND env = {{ String(env, 'test') }}
GROUP BY event_name
ORDER BY event_count DESC
{% if defined(limit) %}
LIMIT {{ Int32(limit, 100) }}
{% end %}

View File

@@ -13,6 +13,7 @@ import { useOrg } from "@/hooks/common/useOrg";
import { useDevQuery } from "@/hooks/queries/useDevQuery";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
import { useEventNames } from "@/views/customers/customer/analytics/hooks/useEventNames";
import { useSession } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
import { useEnv } from "@/utils/envUtils";
@@ -126,6 +127,7 @@ const MainContent = ({
useFeaturesQuery();
useRewardsQuery();
useCusSearchQuery();
useEventNames();
return (
<AppContext.Provider value={{}}>

View File

@@ -0,0 +1,33 @@
import { ErrCode } from "@autumn/shared";
import { usePostSWR } from "@/services/useAxiosSwr.js";
export type EventNameWithCount = {
event_name: string;
event_count: number;
};
export const useEventNames = (limit?: number) => {
const {
data,
isLoading,
error,
} = usePostSWR<{ eventNames: EventNameWithCount[] }>({
method: "get",
url: `/query/event_names/list${limit ? `?limit=${limit}` : ""}`,
queryKey: ["query-event-names-list", limit],
options: {
refreshInterval: 0,
onError: (error) => {
if (error.code === ErrCode.ClickHouseDisabled) {
return error;
}
},
},
});
return {
eventNames: data?.eventNames ?? [],
isLoading,
error,
};
};