Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e8996e9
Add metadata filtering to analytics & events wip
devkiran Jul 16, 2025
fea11d2
Update analytics.ts
devkiran Jul 16, 2025
fd9f27c
Merge branch 'main' into filter-events-by-metadata
devkiran Jul 17, 2025
2d5934a
Add filter extraction and processing for event metadata in analytics
devkiran Jul 17, 2025
9aefa03
Implement query filter parsing for analytics and events; refactor fil…
devkiran Jul 17, 2025
5a36767
Update analytics-query-parser.ts
devkiran Jul 17, 2025
127ca9c
Update analytics.ts
devkiran Jul 17, 2025
1c26a43
Merge branch 'main' into filter-events-by-metadata
devkiran Jul 17, 2025
a2287c2
Merge branch 'main' into filter-events-by-metadata
devkiran Jul 18, 2025
8ce567b
support Stripe-like query filter
devkiran Jul 18, 2025
8c69b57
Update analytics-query-parser.ts
devkiran Jul 18, 2025
d6c229b
Update analytics-query-parser.test.ts
devkiran Jul 18, 2025
9e7bd00
Refactor analytics query parser to use descriptive operator names and…
devkiran Jul 18, 2025
76d5670
Update analytics-query-parser.ts
devkiran Jul 18, 2025
01dab20
fix tests
devkiran Jul 18, 2025
a244e8a
Merge branch 'main' into filter-events-by-metadata
devkiran Jul 19, 2025
284925f
Merge branch 'main' into filter-events-by-metadata
steven-tey Jul 19, 2025
56de4b2
Merge branch 'main' into filter-events-by-metadata
steven-tey Jul 20, 2025
b99c72d
Merge branch 'main' into filter-events-by-metadata
steven-tey Jul 21, 2025
200e1c3
Merge branch 'main' into filter-events-by-metadata
steven-tey Jul 22, 2025
ad5aede
Merge branch 'main' into filter-events-by-metadata
devkiran Jul 25, 2025
4d76c88
Merge branch 'main' into filter-events-by-metadata
steven-tey Jul 28, 2025
090552a
Merge branch 'main' into filter-events-by-metadata
devkiran Aug 4, 2025
ccaa34f
Merge branch 'main' into filter-events-by-metadata
steven-tey Aug 4, 2025
cfed824
Merge branch 'main' into filter-events-by-metadata
steven-tey Aug 4, 2025
4dd148f
rearrange parseFiltersFromQuery
steven-tey Aug 4, 2025
cfdc5cf
finalize schemas
steven-tey Aug 4, 2025
a240dc5
add metadata column
steven-tey Aug 4, 2025
1bb0850
Merge branch 'main' into filter-events-by-metadata
steven-tey Aug 4, 2025
c9ee2b9
add metadata.productId filter test
steven-tey Aug 4, 2025
d6ece15
parseFiltersFromQuery → queryParser
steven-tey Aug 4, 2025
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
1 change: 1 addition & 0 deletions apps/web/app/(ee)/api/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { eventsQuerySchema } from "@/lib/zod/schemas/analytics";
import { Folder, Link } from "@dub/prisma/client";
import { NextResponse } from "next/server";

// GET /api/events
export const GET = withWorkspace(
async ({ searchParams, workspace, session }) => {
throwIfClicksUsageExceeded(workspace);
Expand Down
5 changes: 5 additions & 0 deletions apps/web/lib/analytics/get-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SINGULAR_ANALYTICS_ENDPOINTS,
} from "./constants";
import { AnalyticsFilters } from "./types";
import { parseFiltersFromQuery } from "./utils/analytics-query-parser";
import { getStartEndDates } from "./utils/get-start-end-dates";

// Fetch data for /api/analytics
Expand All @@ -32,6 +33,7 @@ export const getAnalytics = async (params: AnalyticsFilters) => {
timezone = "UTC",
isDeprecatedClicksEndpoint = false,
dataAvailableFrom,
query,
} = params;

const tagIds = combineTagIds(params);
Expand Down Expand Up @@ -104,6 +106,8 @@ export const getAnalytics = async (params: AnalyticsFilters) => {
: analyticsResponse[groupBy],
});

const filters = parseFiltersFromQuery(query);

const response = await pipe({
...params,
...(UTM_TAGS_PLURAL_LIST.includes(groupBy)
Expand All @@ -119,6 +123,7 @@ export const getAnalytics = async (params: AnalyticsFilters) => {
timezone,
country,
region,
filters: filters ? JSON.stringify(filters) : undefined,
});

if (groupBy === "count") {
Expand Down
5 changes: 5 additions & 0 deletions apps/web/lib/analytics/get-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
saleEventSchemaTBEndpoint,
} from "../zod/schemas/sales";
import { EventsFilters } from "./types";
import { parseFiltersFromQuery } from "./utils/analytics-query-parser";
import { getStartEndDates } from "./utils/get-start-end-dates";

// Fetch data for /api/events
Expand All @@ -39,6 +40,7 @@ export const getEvents = async (params: EventsFilters) => {
order,
sortOrder,
dataAvailableFrom,
query,
} = params;

const { startDate, endDate } = getStartEndDates({
Expand Down Expand Up @@ -78,6 +80,8 @@ export const getEvents = async (params: EventsFilters) => {
}[eventType] ?? clickEventSchemaTBEndpoint,
});

const filters = parseFiltersFromQuery(query);

const response = await pipe({
...params,
eventType,
Expand All @@ -89,6 +93,7 @@ export const getEvents = async (params: EventsFilters) => {
offset: (params.page - 1) * params.limit,
start: startDate.toISOString().replace("T", " ").replace("Z", ""),
end: endDate.toISOString().replace("T", " ").replace("Z", ""),
filters: filters ? JSON.stringify(filters) : undefined,
});

const [linksMap, customersMap] = await Promise.all([
Expand Down
39 changes: 39 additions & 0 deletions apps/web/lib/analytics/utils/analytics-query-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { z } from "zod";
import { EventsFilters } from "../types";

interface Filter {
operand: string;
operator: "equals"; // can be expanded later
value: string;
}

export const parseFiltersFromQuery = (query: EventsFilters["query"]) => {
if (!query) {
return undefined;
}

const result = z
.object({
metadata: z.record(z.string(), z.string()),
})
.safeParse(query);

if (!result.success) {
console.log(`Ignoring the invalid query ${JSON.stringify(query)}`);
return undefined;
}

const filters: Filter[] = Object.entries(result.data.metadata)
.slice(0, 1)
.map(([key, value]) => {
return {
operand: key,
operator: "equals",
value,
};
});

console.log("filters", filters);

return filters;
};
21 changes: 21 additions & 0 deletions apps/web/lib/zod/schemas/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,26 @@ export const analyticsQuerySchema = z
.describe(
"Filter for root domains. If true, filter for domains only. If false, filter for links only. If undefined, return both.",
),
query: z
.string()
.transform((v) => {
try {
return JSON.parse(v);
} catch (e) {
return undefined;
}
})
.optional()
.describe(
"Filter by event metadata key-value pairs. Format: key=value. Only available for lead and sale events.",
)
.openapi({
example: {
metadata: {
eventType: "project_transfer",
},
},
}),
})
.merge(utmTagsSchema);

Expand Down Expand Up @@ -254,6 +274,7 @@ export const analyticsFilterTB = z
.optional()
.describe("The folder IDs to retrieve analytics for."),
isMegaFolder: z.boolean().optional(),
filters: z.any(),
})
.merge(
analyticsQuerySchema.pick({
Expand Down
18 changes: 18 additions & 0 deletions packages/tinybird/pipes/v2_events.pipe
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ SQL >
AND url LIKE concat('%utm_content=', encodeURLFormComponent({{ String(utm_content) }}), '%')
{% end %}
{% if defined(url) %} AND url = {{ url }} {% end %}

{% if defined(filters) %}
{% for item in JSON(filters, '[]') %}
{% if item.get('operator', '') == 'equals' %}
AND JSONExtractString(metadata, {{ item.get('operand', '') }}) = {{ item.get('value', '') }}
{% end %}
{% end %}
{% end %}

ORDER BY timestamp {% if order == 'asc' %} ASC {% else %} DESC {% end %}
LIMIT {{ Int32(limit, 100) }}
{% if defined(offset) %} OFFSET {{ Int32(offset, 0) }} {% end %}
Expand Down Expand Up @@ -174,6 +183,15 @@ SQL >
{% if defined(utm_term) %} AND url LIKE concat('%utm_term=', encodeURLFormComponent({{ String(utm_term) }}), '%') {% end %}
{% if defined(utm_content) %} AND url LIKE concat('%utm_content=', encodeURLFormComponent({{ String(utm_content) }}), '%') {% end %}
{% if defined(url) %} AND url = {{ url }} {% end %}

{% if defined(filters) %}
{% for item in JSON(filters, '[]') %}
{% if item.get('operator', '') == 'equals' %}
AND JSONExtractString(metadata, {{ item.get('operand', '') }}) = {{ item.get('value', '') }}
{% end %}
{% end %}
{% end %}

ORDER BY timestamp {% if order == 'asc' %} ASC {% else %} DESC {% end %}
LIMIT {{ Int32(limit, 100) }}
{% if defined(offset) %} OFFSET {{ Int32(offset, 0) }} {% end %}
Expand Down