diff --git a/server/src/internal/analytics/actions/listRawEvents.ts b/server/src/internal/analytics/actions/listRawEvents.ts
index b13e68545..bdb832ba5 100644
--- a/server/src/internal/analytics/actions/listRawEvents.ts
+++ b/server/src/internal/analytics/actions/listRawEvents.ts
@@ -71,6 +71,7 @@ export type ListRawEventsParams = {
customer?: FullCustomer;
aggregateAll?: boolean;
event_name?: string;
+ event_names?: string[];
limit?: number;
};
@@ -114,6 +115,16 @@ export const listRawEvents = async ({
? billingCycleResult.endDate
: formatJsDateToClickHouseDateTime(new Date());
+ const eventNameFilter = (() => {
+ if (params.event_names && params.event_names.length > 0) {
+ return params.event_names;
+ }
+ if (params.event_name) {
+ return [params.event_name];
+ }
+ return undefined;
+ })();
+
const pipeParams = {
org_id: org.id,
env,
@@ -121,7 +132,7 @@ export const listRawEvents = async ({
end_date: finalEndDate,
customer_id: params.aggregateAll ? undefined : params.customer_id,
entity_id: params.entity_id,
- event_names: params.event_name ? [params.event_name] : undefined,
+ event_names: eventNameFilter,
limit: params.limit ?? DEFAULT_LIMIT,
offset: 0,
};
diff --git a/server/src/internal/analytics/internalHandlers/handleInternalListRawEvents.ts b/server/src/internal/analytics/internalHandlers/handleInternalListRawEvents.ts
index 26cc625ae..e69883218 100644
--- a/server/src/internal/analytics/internalHandlers/handleInternalListRawEvents.ts
+++ b/server/src/internal/analytics/internalHandlers/handleInternalListRawEvents.ts
@@ -13,6 +13,7 @@ const InternalListRawEventsSchema = z.object({
message: "custom_range.start must be before custom_range.end",
})
.optional(),
+ event_names: z.array(z.string()).optional(),
customer_id: z.string().nullish(),
entity_id: z.string().optional(),
});
@@ -26,7 +27,7 @@ export const handleInternalListRawEvents = createRoute({
handler: async (c) => {
const ctx = c.get("ctx");
const { db, org, env } = ctx;
- const { interval, custom_range, customer_id, entity_id } =
+ const { interval, custom_range, event_names, customer_id, entity_id } =
c.req.valid("json");
let aggregateAll = false;
@@ -60,6 +61,7 @@ export const handleInternalListRawEvents = createRoute({
entity_id: entity_id,
interval: interval ?? undefined,
custom_range: custom_range ?? undefined,
+ event_names: event_names?.filter((name) => name !== ""),
customer,
aggregateAll,
},
diff --git a/vite/src/views/customers/customer/analytics/components/ChartSkeleton.tsx b/vite/src/views/customers/customer/analytics/components/ChartSkeleton.tsx
index f5e104033..4f6ac1363 100644
--- a/vite/src/views/customers/customer/analytics/components/ChartSkeleton.tsx
+++ b/vite/src/views/customers/customer/analytics/components/ChartSkeleton.tsx
@@ -36,7 +36,7 @@ export const ChartSkeleton = ({
const bars = useMemo(() => buildSkeletonBars(barCount), [barCount]);
return (
-
+
{[72, 56, 64].map((width, i) => (
@@ -92,6 +92,8 @@ export const ChartSkeleton = ({
))}
+
+
);
};
diff --git a/vite/src/views/customers/customer/analytics/components/EventsTable.tsx b/vite/src/views/customers/customer/analytics/components/EventsTable.tsx
index f92101150..40b31abab 100644
--- a/vite/src/views/customers/customer/analytics/components/EventsTable.tsx
+++ b/vite/src/views/customers/customer/analytics/components/EventsTable.tsx
@@ -2,7 +2,6 @@ import { CaretLeftIcon, CaretRightIcon, DatabaseIcon } from "@phosphor-icons/rea
import { memo, useState } from "react";
import { Table } from "@/components/general/table";
import { IconButton } from "@/components/v2/buttons/IconButton";
-import { Skeleton } from "@/components/ui/skeleton";
import {
Select,
SelectContent,
@@ -11,32 +10,15 @@ import {
SelectValue,
} from "@/components/v2/selects/Select";
import { cn } from "@/lib/utils";
-import type { ColumnDef } from "@tanstack/react-table";
import type { IRow } from "./analytics-types";
import { createEventsColumns } from "./EventsColumns";
import { RowClickDialog } from "./RowClickDialog";
import { useEventsTable } from "../hooks/useEventsTable";
const PAGE_SIZE_OPTIONS = [100, 500, 1000] as const;
+
const columns = createEventsColumns();
-const SKELETON_CELL_WIDTHS = ["w-28", "w-20", "w-8", "w-40"] as const;
-
-const skeletonColumns: ColumnDef
[] = columns.map((col, i) => ({
- ...col,
- cell: () => ,
-}));
-
-const PLACEHOLDER_ROWS: IRow[] = Array.from({ length: 15 }, (_, i) => ({
- timestamp: "",
- event_name: "",
- value: 0,
- properties: "",
- idempotency_key: String(i),
- entity_id: "",
- customer_id: "",
-}));
-
export const EventsTable = memo(function EventsTable({
data,
isLoading = false,
@@ -51,9 +33,7 @@ export const EventsTable = memo(function EventsTable({
const [selectedEvent, setSelectedEvent] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
- const tableData = isLoading ? PLACEHOLDER_ROWS : data;
- const activeColumns = isLoading ? skeletonColumns : columns;
- const table = useEventsTable({ data: tableData, columns: activeColumns });
+ const table = useEventsTable({ data, columns });
const { pageIndex, pageSize } = table.getState().pagination;
const totalPages = table.getPageCount();
@@ -130,8 +110,8 @@ export const EventsTable = memo(function EventsTable({
{
@@ -28,8 +27,6 @@ export const useAnalyticsData = ({
const [searchParams] = useSearchParams();
const customerId = searchParams.get("customer_id");
const entityId = searchParams.get("entity_id");
- const featureIds = searchParams.get("feature_ids")?.split(",");
- const eventNames = searchParams.get("event_names")?.split(",");
const groupBy = searchParams.get("group_by");
const maxGroups = Number(searchParams.get("max_groups")) || 10;
@@ -38,36 +35,17 @@ export const useAnalyticsData = ({
const customRange =
interval === "custom" && start && end ? { start, end } : undefined;
- const { eventNames: cachedEventNames, isLoading: eventNamesLoading } =
- useEventNames();
+ const { selectedEventNames, featuresData, featuresLoading, eventNamesLoading } =
+ useSelectedEventNames();
const timezone = useMemo(() => getUserTimezone(), []);
- const { features: featuresData, isLoading: featuresLoading } =
- useFeaturesQuery();
-
const formattedGroupBy = groupBy
? groupBy === "customer_id" || groupBy === "entity_id" || groupBy === "plan_id"
? groupBy
: `properties.${groupBy}`
: undefined;
- const featureLinkedEventNames = useMemo(() => {
- if (!featuresData?.length) return cachedEventNames;
- return cachedEventNames.filter((e) =>
- featuresData.some(
- (f) =>
- (f.type === FeatureType.Metered || f.type === FeatureType.CreditSystem) &&
- (f.event_names?.includes(e.event_name) || f.id === e.event_name),
- ),
- );
- }, [cachedEventNames, featuresData]);
-
- const selectedEventNames =
- eventNames || featureIds
- ? [...(eventNames || []), ...(featureIds || [])]
- : featureLinkedEventNames.slice(0, 3).map((e) => e.event_name);
-
const postBody = {
customer_id: customerId || undefined,
entity_id: entityId || undefined,
@@ -142,21 +120,21 @@ export const useRawAnalyticsData = () => {
const customRange =
interval === "custom" && start && end ? { start, end } : undefined;
- const { features: featuresData, isLoading: featuresLoading } =
- useFeaturesQuery();
+ const { selectedEventNames, featuresData, featuresLoading, eventNamesLoading } =
+ useSelectedEventNames();
+
+ const isReady = !eventNamesLoading && !featuresLoading;
const postBody = {
customer_id: customerId || undefined,
entity_id: entityId || undefined,
interval: customRange ? undefined : interval,
custom_range: customRange,
+ event_names: selectedEventNames,
};
- const {
- data,
- isLoading: queryLoading,
- error,
- } = useQuery({
+ const { data, isLoading, error } = useQuery({
+ enabled: isReady,
queryKey: buildKey([
"query-raw-events",
customerId,
@@ -164,6 +142,7 @@ export const useRawAnalyticsData = () => {
interval,
String(start ?? ""),
String(end ?? ""),
+ ...selectedEventNames.sort(),
]),
queryFn: async () => {
const { data } = await axiosInstance.post("/query/raw", postBody);
@@ -171,6 +150,8 @@ export const useRawAnalyticsData = () => {
},
});
+ const queryLoading = !isReady || isLoading;
+
return {
customer: data?.customer,
features: featuresData || [],
diff --git a/vite/src/views/customers/customer/analytics/hooks/useSelectedEventNames.tsx b/vite/src/views/customers/customer/analytics/hooks/useSelectedEventNames.tsx
new file mode 100644
index 000000000..bf64e4a09
--- /dev/null
+++ b/vite/src/views/customers/customer/analytics/hooks/useSelectedEventNames.tsx
@@ -0,0 +1,48 @@
+import { FeatureType } from "@autumn/shared";
+import { parseAsArrayOf, parseAsString, useQueryStates } from "nuqs";
+import { useMemo } from "react";
+import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
+import { type EventNameWithCount, useEventNames } from "./useEventNames";
+
+/** Resolves the event names the analytics views filter by: explicit URL
+ * selection (event_names / feature_ids) or the top metered events by default.
+ * Shared by the chart and the events table so they stay in sync. */
+export const useSelectedEventNames = () => {
+ const [{ feature_ids: featureIds, event_names: eventNames }] = useQueryStates({
+ feature_ids: parseAsArrayOf(parseAsString),
+ event_names: parseAsArrayOf(parseAsString),
+ });
+
+ const { eventNames: cachedEventNames, isLoading: eventNamesLoading } =
+ useEventNames();
+ const { features: featuresData, isLoading: featuresLoading } =
+ useFeaturesQuery();
+
+ const featureLinkedEventNames = useMemo(() => {
+ if (!featuresData?.length) {
+ return cachedEventNames;
+ }
+ return cachedEventNames.filter((e: EventNameWithCount) =>
+ featuresData.some(
+ (f) =>
+ (f.type === FeatureType.Metered ||
+ f.type === FeatureType.CreditSystem) &&
+ (f.event_names?.includes(e.event_name) || f.id === e.event_name),
+ ),
+ );
+ }, [cachedEventNames, featuresData]);
+
+ const selectedEventNames =
+ eventNames || featureIds
+ ? [...(eventNames || []), ...(featureIds || [])]
+ : featureLinkedEventNames
+ .slice(0, 3)
+ .map((e: EventNameWithCount) => e.event_name);
+
+ return {
+ selectedEventNames,
+ featuresData,
+ featuresLoading,
+ eventNamesLoading,
+ };
+};