fix(api): improve export api, properties to be a comma seperated list
This commit is contained in:
@@ -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 { DateTime } from '@openpanel/common';
|
||||||
import type { GetEventListOptions } from '@openpanel/db';
|
import type { GetEventListOptions } from '@openpanel/db';
|
||||||
import {
|
import {
|
||||||
|
ChartEngine,
|
||||||
ClientType,
|
ClientType,
|
||||||
db,
|
db,
|
||||||
getEventList,
|
getEventList,
|
||||||
getEventsCountCached,
|
getEventsCount,
|
||||||
getSettingsForProject,
|
getSettingsForProject,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import { ChartEngine } from '@openpanel/db';
|
|
||||||
import { zChartEvent, zReport } from '@openpanel/validation';
|
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(
|
async function getProjectId(
|
||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
@@ -22,8 +20,7 @@ async function getProjectId(
|
|||||||
project_id?: string;
|
project_id?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
};
|
};
|
||||||
}>,
|
}>
|
||||||
reply: FastifyReply,
|
|
||||||
) {
|
) {
|
||||||
let projectId = request.query.projectId || request.query.project_id;
|
let projectId = request.query.projectId || request.query.project_id;
|
||||||
|
|
||||||
@@ -75,8 +72,20 @@ const eventsScheme = z.object({
|
|||||||
limit: z.coerce.number().optional().default(50),
|
limit: z.coerce.number().optional().default(50),
|
||||||
includes: z
|
includes: z
|
||||||
.preprocess(
|
.preprocess(
|
||||||
(arg) => (typeof arg === 'string' ? [arg] : arg),
|
(arg) => {
|
||||||
z.array(z.string()),
|
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(),
|
.optional(),
|
||||||
});
|
});
|
||||||
@@ -85,7 +94,7 @@ export async function events(
|
|||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Querystring: z.infer<typeof eventsScheme>;
|
Querystring: z.infer<typeof eventsScheme>;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply
|
||||||
) {
|
) {
|
||||||
const query = eventsScheme.safeParse(request.query);
|
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 limit = query.data.limit;
|
||||||
const page = Math.max(query.data.page, 1);
|
const page = Math.max(query.data.page, 1);
|
||||||
const take = Math.max(Math.min(limit, 1000), 1);
|
const take = Math.max(Math.min(limit, 1000), 1);
|
||||||
@@ -118,20 +127,20 @@ export async function events(
|
|||||||
meta: false,
|
meta: false,
|
||||||
...query.data.includes?.reduce(
|
...query.data.includes?.reduce(
|
||||||
(acc, key) => ({ ...acc, [key]: true }),
|
(acc, key) => ({ ...acc, [key]: true }),
|
||||||
{},
|
{}
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const [data, totalCount] = await Promise.all([
|
const [data, totalCount] = await Promise.all([
|
||||||
getEventList(options),
|
getEventList(options),
|
||||||
getEventsCountCached(omit(['cursor', 'take'], options)),
|
getEventsCount(options),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
reply.send({
|
reply.send({
|
||||||
meta: {
|
meta: {
|
||||||
count: data.length,
|
count: data.length,
|
||||||
totalCount: totalCount,
|
totalCount,
|
||||||
pages: Math.ceil(totalCount / options.take),
|
pages: Math.ceil(totalCount / options.take),
|
||||||
current: cursor + 1,
|
current: cursor + 1,
|
||||||
},
|
},
|
||||||
@@ -158,7 +167,7 @@ const chartSchemeFull = zReport
|
|||||||
filters: zChartEvent.shape.filters.optional(),
|
filters: zChartEvent.shape.filters.optional(),
|
||||||
segment: zChartEvent.shape.segment.optional(),
|
segment: zChartEvent.shape.segment.optional(),
|
||||||
property: zChartEvent.shape.property.optional(),
|
property: zChartEvent.shape.property.optional(),
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
// Backward compatibility - events will be migrated to series via preprocessing
|
// Backward compatibility - events will be migrated to series via preprocessing
|
||||||
@@ -169,7 +178,7 @@ const chartSchemeFull = zReport
|
|||||||
filters: zChartEvent.shape.filters.optional(),
|
filters: zChartEvent.shape.filters.optional(),
|
||||||
segment: zChartEvent.shape.segment.optional(),
|
segment: zChartEvent.shape.segment.optional(),
|
||||||
property: zChartEvent.shape.property.optional(),
|
property: zChartEvent.shape.property.optional(),
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
@@ -178,7 +187,7 @@ export async function charts(
|
|||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Querystring: Record<string, string>;
|
Querystring: Record<string, string>;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply
|
||||||
) {
|
) {
|
||||||
const query = chartSchemeFull.safeParse(parseQueryString(request.query));
|
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 { timezone } = await getSettingsForProject(projectId);
|
||||||
const { events, series, ...rest } = query.data;
|
const { events, series, ...rest } = query.data;
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,32 @@ GET /export/events
|
|||||||
| `end` | string | End date for the event range (ISO format) | `2024-04-18` |
|
| `end` | string | End date for the event range (ISO format) | `2024-04-18` |
|
||||||
| `page` | number | Page number for pagination (default: 1) | `2` |
|
| `page` | number | Page number for pagination (default: 1) | `2` |
|
||||||
| `limit` | number | Number of events per page (default: 50, max: 1000) | `100` |
|
| `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
|
#### 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
|
- **Comma-separated**: `?includes=profile,meta` (include both profile and meta in the response)
|
||||||
- `meta`: Include event metadata and configuration
|
- **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
|
#### Example Request
|
||||||
|
|
||||||
@@ -129,12 +147,15 @@ Retrieve aggregated chart data for analytics and visualization. This endpoint pr
|
|||||||
GET /export/charts
|
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
|
#### Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Description | Example |
|
| Parameter | Type | Description | Example |
|
||||||
|-----------|------|-------------|---------|
|
|-----------|------|-------------|---------|
|
||||||
| `projectId` | string | The ID of the project to fetch chart data from | `abc123` |
|
| `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"}]` |
|
| `breakdowns` | object[] | Array of breakdown dimensions | `[{"name":"country"}]` |
|
||||||
| `interval` | string | Time interval for data points | `day` |
|
| `interval` | string | Time interval for data points | `day` |
|
||||||
| `range` | string | Predefined date range | `7d` |
|
| `range` | string | Predefined date range | `7d` |
|
||||||
@@ -144,7 +165,7 @@ GET /export/charts
|
|||||||
|
|
||||||
#### Event Configuration
|
#### 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 |
|
| Property | Type | Description | Required | Default |
|
||||||
|----------|------|-------------|----------|---------|
|
|----------|------|-------------|----------|---------|
|
||||||
@@ -228,11 +249,13 @@ Common breakdown dimensions include:
|
|||||||
#### Example Request
|
#### Example Request
|
||||||
|
|
||||||
```bash
|
```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-id: YOUR_CLIENT_ID' \
|
||||||
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET'
|
-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
|
#### Example Advanced Request
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -241,7 +264,7 @@ curl 'https://api.openpanel.dev/export/charts' \
|
|||||||
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET' \
|
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET' \
|
||||||
-G \
|
-G \
|
||||||
--data-urlencode 'projectId=abc123' \
|
--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 'breakdowns=[{"name":"country"}]' \
|
||||||
--data-urlencode 'interval=day' \
|
--data-urlencode 'interval=day' \
|
||||||
--data-urlencode 'range=30d'
|
--data-urlencode 'range=30d'
|
||||||
|
|||||||
@@ -654,7 +654,6 @@ export async function getEventList(options: GetEventListOptions) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEventsCountCached = cacheable(getEventsCount, 60 * 10);
|
|
||||||
export async function getEventsCount({
|
export async function getEventsCount({
|
||||||
projectId,
|
projectId,
|
||||||
profileId,
|
profileId,
|
||||||
|
|||||||
Reference in New Issue
Block a user