From 74bcb7ead27dfff4c8931a659bdf59709a2bb0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Tue, 3 Mar 2026 11:37:05 +0100 Subject: [PATCH] fix(api): improve export api, properties to be a comma seperated list --- apps/api/src/controllers/export.controller.ts | 51 +++++++++++-------- apps/public/content/docs/api/export.mdx | 39 +++++++++++--- packages/db/src/services/event.service.ts | 1 - 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/apps/api/src/controllers/export.controller.ts b/apps/api/src/controllers/export.controller.ts index 294f59c8..d568439b 100644 --- a/apps/api/src/controllers/export.controller.ts +++ b/apps/api/src/controllers/export.controller.ts @@ -1,20 +1,18 @@ -import { parseQueryString } from '@/utils/parse-zod-query-string'; -import type { FastifyReply, FastifyRequest } from 'fastify'; -import { z } from 'zod'; - -import { HttpError } from '@/utils/errors'; import { DateTime } from '@openpanel/common'; import type { GetEventListOptions } from '@openpanel/db'; import { + ChartEngine, ClientType, db, getEventList, - getEventsCountCached, + getEventsCount, getSettingsForProject, } from '@openpanel/db'; -import { ChartEngine } from '@openpanel/db'; import { zChartEvent, zReport } from '@openpanel/validation'; -import { omit } from 'ramda'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { z } from 'zod'; +import { HttpError } from '@/utils/errors'; +import { parseQueryString } from '@/utils/parse-zod-query-string'; async function getProjectId( request: FastifyRequest<{ @@ -22,8 +20,7 @@ async function getProjectId( project_id?: string; projectId?: string; }; - }>, - reply: FastifyReply, + }> ) { let projectId = request.query.projectId || request.query.project_id; @@ -75,8 +72,20 @@ const eventsScheme = z.object({ limit: z.coerce.number().optional().default(50), includes: z .preprocess( - (arg) => (typeof arg === 'string' ? [arg] : arg), - z.array(z.string()), + (arg) => { + if (arg == null) { + return undefined; + } + if (Array.isArray(arg)) { + return arg; + } + if (typeof arg === 'string') { + const parts = arg.split(',').map((s) => s.trim()).filter(Boolean); + return parts; + } + return arg; + }, + z.array(z.string()) ) .optional(), }); @@ -85,7 +94,7 @@ export async function events( request: FastifyRequest<{ Querystring: z.infer; }>, - reply: FastifyReply, + reply: FastifyReply ) { const query = eventsScheme.safeParse(request.query); @@ -97,7 +106,7 @@ export async function events( }); } - const projectId = await getProjectId(request, reply); + const projectId = await getProjectId(request); const limit = query.data.limit; const page = Math.max(query.data.page, 1); const take = Math.max(Math.min(limit, 1000), 1); @@ -118,20 +127,20 @@ export async function events( meta: false, ...query.data.includes?.reduce( (acc, key) => ({ ...acc, [key]: true }), - {}, + {} ), }, }; const [data, totalCount] = await Promise.all([ getEventList(options), - getEventsCountCached(omit(['cursor', 'take'], options)), + getEventsCount(options), ]); reply.send({ meta: { count: data.length, - totalCount: totalCount, + totalCount, pages: Math.ceil(totalCount / options.take), current: cursor + 1, }, @@ -158,7 +167,7 @@ const chartSchemeFull = zReport filters: zChartEvent.shape.filters.optional(), segment: zChartEvent.shape.segment.optional(), property: zChartEvent.shape.property.optional(), - }), + }) ) .optional(), // Backward compatibility - events will be migrated to series via preprocessing @@ -169,7 +178,7 @@ const chartSchemeFull = zReport filters: zChartEvent.shape.filters.optional(), segment: zChartEvent.shape.segment.optional(), property: zChartEvent.shape.property.optional(), - }), + }) ) .optional(), }); @@ -178,7 +187,7 @@ export async function charts( request: FastifyRequest<{ Querystring: Record; }>, - reply: FastifyReply, + reply: FastifyReply ) { const query = chartSchemeFull.safeParse(parseQueryString(request.query)); @@ -190,7 +199,7 @@ export async function charts( }); } - const projectId = await getProjectId(request, reply); + const projectId = await getProjectId(request); const { timezone } = await getSettingsForProject(projectId); const { events, series, ...rest } = query.data; diff --git a/apps/public/content/docs/api/export.mdx b/apps/public/content/docs/api/export.mdx index 491133d1..bb725046 100644 --- a/apps/public/content/docs/api/export.mdx +++ b/apps/public/content/docs/api/export.mdx @@ -53,14 +53,32 @@ GET /export/events | `end` | string | End date for the event range (ISO format) | `2024-04-18` | | `page` | number | Page number for pagination (default: 1) | `2` | | `limit` | number | Number of events per page (default: 50, max: 1000) | `100` | -| `includes` | string or string[] | Additional fields to include in the response | `profile` or `["profile","meta"]` | +| `includes` | string or string[] | Additional fields to include in the response. Pass multiple as comma-separated (`profile,meta`) or repeated params (`includes=profile&includes=meta`). | `profile` or `profile,meta` | #### Include Options -The `includes` parameter allows you to fetch additional related data: +The `includes` parameter allows you to fetch additional related data. When using query parameters, you can pass multiple values in either of these ways: -- `profile`: Include user profile information -- `meta`: Include event metadata and configuration +- **Comma-separated**: `?includes=profile,meta` (include both profile and meta in the response) +- **Repeated parameter**: `?includes=profile&includes=meta` (same result; useful when building URLs programmatically) + +Supported values (any of these can be combined; names match the response keys): + +**Related data** (adds nested objects or extra lookups): + +- `profile` — User profile for the event (id, email, firstName, lastName, etc.) +- `meta` — Event metadata from project config (name, description, conversion flag) + +**Event fields** (optional columns; these are in addition to the default fields): + +- `properties` — Custom event properties +- `region`, `longitude`, `latitude` — Extra geo (default already has `city`, `country`) +- `osVersion`, `browserVersion`, `device`, `brand`, `model` — Extra device (default already has `os`, `browser`) +- `origin`, `referrer`, `referrerName`, `referrerType` — Referrer/navigation +- `revenue` — Revenue amount +- `importedAt`, `sdkName`, `sdkVersion` — Import/SDK info + +The response always includes: `id`, `name`, `deviceId`, `profileId`, `sessionId`, `projectId`, `createdAt`, `path`, `duration`, `city`, `country`, `os`, `browser`. Use `includes` to add any of the values above. #### Example Request @@ -129,12 +147,15 @@ Retrieve aggregated chart data for analytics and visualization. This endpoint pr GET /export/charts ``` +**Note:** The endpoint accepts either `series` or `events` for the event configuration; `series` is the preferred parameter name. Both use the same structure. + #### Query Parameters | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `projectId` | string | The ID of the project to fetch chart data from | `abc123` | -| `events` | object[] | Array of event configurations to analyze | `[{"name":"screen_view","filters":[]}]` | +| `series` | object[] | Array of event/series configurations to analyze (preferred over `events`) | `[{"name":"screen_view","filters":[]}]` | +| `events` | object[] | Array of event configurations (deprecated in favor of `series`) | `[{"name":"screen_view","filters":[]}]` | | `breakdowns` | object[] | Array of breakdown dimensions | `[{"name":"country"}]` | | `interval` | string | Time interval for data points | `day` | | `range` | string | Predefined date range | `7d` | @@ -144,7 +165,7 @@ GET /export/charts #### Event Configuration -Each event in the `events` array supports the following properties: +Each item in the `series` or `events` array supports the following properties: | Property | Type | Description | Required | Default | |----------|------|-------------|----------|---------| @@ -228,11 +249,13 @@ Common breakdown dimensions include: #### Example Request ```bash -curl 'https://api.openpanel.dev/export/charts?projectId=abc123&events=[{"name":"screen_view","segment":"user"}]&breakdowns=[{"name":"country"}]&interval=day&range=30d&previous=true' \ +curl 'https://api.openpanel.dev/export/charts?projectId=abc123&series=[{"name":"screen_view","segment":"user"}]&breakdowns=[{"name":"country"}]&interval=day&range=30d&previous=true' \ -H 'openpanel-client-id: YOUR_CLIENT_ID' \ -H 'openpanel-client-secret: YOUR_CLIENT_SECRET' ``` +You can use `events` instead of `series` in the query for backward compatibility; both accept the same structure. + #### Example Advanced Request ```bash @@ -241,7 +264,7 @@ curl 'https://api.openpanel.dev/export/charts' \ -H 'openpanel-client-secret: YOUR_CLIENT_SECRET' \ -G \ --data-urlencode 'projectId=abc123' \ - --data-urlencode 'events=[{"name":"purchase","segment":"property_sum","property":"properties.total","filters":[{"name":"properties.total","operator":"isNotNull","value":[]}]}]' \ + --data-urlencode 'series=[{"name":"purchase","segment":"property_sum","property":"properties.total","filters":[{"name":"properties.total","operator":"isNotNull","value":[]}]}]' \ --data-urlencode 'breakdowns=[{"name":"country"}]' \ --data-urlencode 'interval=day' \ --data-urlencode 'range=30d' diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 5b34bfd0..fc1667b0 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -654,7 +654,6 @@ export async function getEventList(options: GetEventListOptions) { return data; } -export const getEventsCountCached = cacheable(getEventsCount, 60 * 10); export async function getEventsCount({ projectId, profileId,