Creator Daily Trend
Per-day views and clicks for the creator's bio page. Bucketed via Postgres date_trunc('day', enteredAt). Days with no traffic are absent — fill gaps client-side.
GET /api/v1/creators/analytics/trend — 🔑 Bearer · Rate limit: 60 req / minute
Daily time series of pageviews and link clicks for the creator's bio page. The bucketing is done server-side with Postgres date_trunc('day', "enteredAt"), so each row is a single calendar day in the database server's time zone.
Days with zero traffic are MISSING from the array. The SQL GROUP BY date_trunc('day', enteredAt) only emits rows that actually have pageviews. To plot a continuous line/bar chart, fill missing dates client-side with views: 0, clicks: 0. Don't assume daily.length === days.
day is bucketed in the database server's time zone. The grouping key is date_trunc('day', "enteredAt") without a timezone argument, so it uses the Postgres server's TZ. Two visits at 2026-04-30T23:30Z and 2026-05-01T00:30Z may or may not fall in the same day depending on server TZ. Render day as a UTC date in your client and accept that day boundaries follow the DB, not the user.
clicks is filtered, not separately counted. The SQL uses COUNT(*) FILTER (WHERE "linkClicked" IS NOT NULL) — same scan, same window, just a conditional aggregate. So clicks ≤ views always holds for the same row.
Request
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
days | integer | 30 | Lookback window. Capped at 365. |
Headers
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JwtAuthGuard. |
Response
200 OK — ApiResponseOf<CreatorTrendDto>
{
"success": true,
"data": {
"days": 30,
"daily": [
{ "day": "2026-04-01T00:00:00.000Z", "views": 145, "clicks": 12 },
{ "day": "2026-04-02T00:00:00.000Z", "views": 132, "clicks": 8 }
]
}
}Top-level fields
| Field | Type | Notes |
|---|---|---|
days | number | Echo of the effective window after defaulting + capping. |
daily | array | One row per calendar day that had at least one pageview. |
Item fields (daily[])
| Field | Type | Notes |
|---|---|---|
day | string (ISO 8601) | Postgres date_trunc('day', enteredAt) serialized — midnight of the day in DB server TZ. |
views | number | Pageview count for that day. |
clicks | number | Subset of views where linkClicked IS NOT NULL. |
Errors
Same as overview.
Side effects
JwtAuthGuard+ThrottleGuard.getBioPageId(userId).analyticsDb.$queryRaw:SELECT date_trunc('day', "enteredAt") as day, COUNT(*)::int as views, COUNT(*) FILTER (WHERE "linkClicked" IS NOT NULL)::int as clicks FROM "AnalyticsPageView" WHERE "bioPageId" = $bioPageId::uuid AND "enteredAt" >= $since GROUP BY date_trunc('day', "enteredAt") ORDER BY day ASC- Return
{ days, daily }. No client-side gap-filling — empty days are omitted.
Code samples
curl 'https://api.bio.re/api/v1/creators/analytics/trend?days=30' \
-H "Authorization: Bearer $ACCESS_TOKEN"type DailyPoint = { day: string; views: number; clicks: number };
async function getTrend(
accessToken: string,
days = 30,
): Promise<{ days: number; daily: DailyPoint[] }> {
const res = await fetch(
`https://api.bio.re/api/v1/creators/analytics/trend?days=${days}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
const json = await res.json();
if (!res.ok || !json.success) throw new Error(json?.error?.message ?? 'Failed');
return json.data;
}
// Server omits zero days — fill the gaps client-side for chart rendering.
function fillGaps(daily: DailyPoint[], windowDays: number): DailyPoint[] {
const map = new Map(daily.map(d => [d.day.slice(0, 10), d]));
const out: DailyPoint[] = [];
for (let i = windowDays - 1; i >= 0; i--) {
const d = new Date(Date.now() - i * 86_400_000);
const key = d.toISOString().slice(0, 10);
out.push(map.get(key) ?? { day: `${key}T00:00:00.000Z`, views: 0, clicks: 0 });
}
return out;
}Try it
Authorization
bearer In: header
Query Parameters
Response Body
application/json
application/json
application/json
curl -X GET "https://loading/api/v1/creators/analytics/trend"{
"success": true,
"data": {
"days": 30,
"daily": [
{
"day": "2019-08-24T14:15:22Z",
"views": 145,
"clicks": 12
}
]
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/analytics/creator-analytics.controller.ts | 118–139 (trend) |
| DTO (response) | apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts | 149–155 (CreatorTrendDto), 136–145 (item shape) |
| Prisma model | packages/prisma-analytics/prisma/schema.prisma | AnalyticsPageView.enteredAt / linkClicked (110–111), index @@index([bioPageId, enteredAt]) (117) |
Creator Device Breakdown
Visitor device-type breakdown for the authenticated creator's bio page (mobile / tablet / desktop / unknown). All segments returned, sorted by count descending.
Creator Session Metrics
Aggregate session metrics for the authenticated creator's bio page — total sessions, bounce rate, average session duration, average pages per session.