Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface StatsConfig {
endpointBrowser?: string;
id?: string;
token?: string;
version?: string;
local?: {
enabled?: boolean;
endpoint?: string;
Expand Down
6 changes: 5 additions & 1 deletion apps/admin-x-framework/src/utils/stats-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export const getStatEndpointUrl = (config?: StatsConfig | null, endpointName?: s
} else {
baseUrl = config.endpoint || '';
}
return `${baseUrl}/v0/pipes/${endpointName}.json?${params}`;

// Append version suffix if provided (e.g., "v2" -> "api_kpis_v2")
const finalEndpointName = config.version ? `${endpointName}_${config.version}` : endpointName;

return `${baseUrl}/v0/pipes/${finalEndpointName}.json?${params}`;
};

export const getToken = () => {
Expand Down
1 change: 0 additions & 1 deletion ghost/core/core/server/api/endpoints/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ const controller = {
'date_to',
'timezone',
'member_status',
'tb_version',
'post_type',
'post_uuid',
'pathname',
Expand Down
2 changes: 2 additions & 0 deletions ghost/core/core/server/data/tinybird/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Sample config:
// -- optional override for site uuid
// "id": "106a623d-9792-4b63-acde-4a0c28ead3dc",
"endpoint": "https://api.tinybird.co",
// -- optional endpoint version suffix (e.g., "v2" calls api_kpis_v2 instead of api_kpis)
// "version": "v2",
// -- tinybird local configuration (optional)
"local": {
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
SCHEMA >
`site_uuid` LowCardinality(String),
`session_id` String,
`pageviews` AggregateFunction(count, UInt64),
`first_pageview` AggregateFunction(min, DateTime),
`last_pageview` AggregateFunction(max, DateTime),
`source` AggregateFunction(argMin, String, DateTime),
`device` AggregateFunction(argMin, String, DateTime),
`utm_source` AggregateFunction(argMin, String, DateTime),
`utm_medium` AggregateFunction(argMin, String, DateTime),
`utm_campaign` AggregateFunction(argMin, String, DateTime),
`utm_term` AggregateFunction(argMin, String, DateTime),
`utm_content` AggregateFunction(argMin, String, DateTime)

ENGINE "AggregatingMergeTree"
ENGINE_SORTING_KEY "site_uuid, session_id"
4 changes: 2 additions & 2 deletions ghost/core/core/server/data/tinybird/endpoints/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Ghost analytics distinguishes between two types of attributes:

#### Session-Level Attributes
These are captured from the **first hit** (earliest timestamp) in a session using `argMin(field, timestamp)` in the `mv_session_data` materialized view:
These are captured from the **first hit** (earliest timestamp) in a session using `argMinState(field, timestamp)` in the `_mv_session_data` materialized view (an `AggregatingMergeTree` table):

- `source` - Referring domain
- `utm_source` - UTM source parameter
Expand Down Expand Up @@ -39,7 +39,7 @@ Finds sessions where **at least one hit** matches the hit-level filter criteria
```sql
NODE sessions_filtered_by_session_attributes
```
Further filters by session-level attributes (source, utm_*) by joining with `mv_session_data`. These filters check attributes from the **first hit only**.
Further filters by session-level attributes (source, utm_*) by reading from `_mv_session_data` using `-Merge` combinators (e.g., `argMinMerge(source)`). These filters check attributes from the **first hit only**.

**Stage 3: Final Output**
```sql
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
TOKEN "stats_page" READ
TOKEN "axis" READ

NODE _active_visitors_0
SQL >
%
select
uniqExact(session_id) as active_visitors
from _mv_hits
where
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Site UUID", required=True)}}
and timestamp >= (now() - interval 5 minute)
{% if defined(post_uuid) %} and post_uuid = {{ String(post_uuid, description="Post UUID to filter on", required=False) }} {% end %}

TYPE ENDPOINT
162 changes: 162 additions & 0 deletions ghost/core/core/server/data/tinybird/endpoints/api_kpis_v2.pipe
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
TOKEN "stats_page" READ
TOKEN "axis" READ

NODE timeseries
SQL >

%
{% set _single_day = defined(date_from) and day_diff(date_from, date_to) == 0 %}
with
{% if defined(date_from) %}
toStartOfDay(
toDate(
{{
Date(
date_from,
description="Starting day for filtering a date range",
required=False,
)
}}
)
) as start,
{% else %} toStartOfDay(timestampAdd(today(), interval -7 day)) as start,
{% end %}
{% if defined(date_to) %}
toStartOfDay(
toDate(
{{
Date(
date_to,
description="Finishing day for filtering a date range",
required=False,
)
}}
)
) as end
{% else %} toStartOfDay(today()) as end
{% end %}
{% if _single_day %}
select
arrayJoin(
arrayMap(
x -> toDateTime(toString(toDateTime(x)), {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}}),
range(
toUInt32(toDateTime(start)), toUInt32(timestampAdd(end, interval 1 day)), 3600
)
)
) as date
{% else %}
select
arrayJoin(
arrayMap(
x -> toDate(x),
range(toUInt32(start), toUInt32(timestampAdd(end, interval 1 day)), 24 * 3600)
)
) as date
{% end %}


NODE session_data
DESCRIPTION >
Read session data from AggregatingMergeTree MV using -Merge combinators

SQL >
%
SELECT
site_uuid,
session_id,
countMerge(pageviews) as pageviews,
minMerge(first_pageview) as first_pageview,
maxMerge(last_pageview) as last_pageview
FROM _mv_session_data_v2
WHERE site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
GROUP BY site_uuid, session_id

NODE session_metrics
DESCRIPTION >
Calculate session-level metrics (visits, pageviews, bounce rate, avg session duration)

SQL >

%
select
site_uuid,
{% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
toStartOfHour(toTimezone(first_pageview, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) as date,
{% else %}
toDate(toTimezone(first_pageview, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) as date,
{% end %}
sd.session_id,
pageviews,
pageviews = 1 as is_bounce,
last_pageview - first_pageview as session_sec
from session_data sd
inner join filtered_sessions_v2 fs
on fs.session_id = sd.session_id


NODE data
DESCRIPTION >
Calculate KPIs per time period

SQL >

select
a.date,
uniq(distinct s.session_id) as visits,
sum(s.pageviews) as pageviews,
truncate(avg(s.is_bounce), 2) as bounce_rate,
truncate(avg(s.session_sec), 2) as avg_session_sec
from timeseries a
inner join session_metrics s on a.date = s.date
group by a.date
order by a.date


NODE pathname_pageviews
DESCRIPTION >
Calculate pageviews for specific pathname with time granularity handling

SQL >

%
select
{% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
toStartOfHour(toTimezone(timestamp, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) as date,
{% else %}
toDate(toTimezone(timestamp, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) as date,
{% end %}
count() pageviews
from timeseries a
inner join _mv_hits h on
{% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
a.date = toStartOfHour(toTimezone(timestamp, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}}))
{% else %}
a.date = toDate(toTimezone(timestamp, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}}))
{% end %}
inner join filtered_sessions_v2 fs
on fs.session_id = h.session_id
where
site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
{% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
{% if defined(post_uuid) %} and post_uuid = {{String(post_uuid, description="Post UUID to filter on", required=False) }} {% end %}
group by date
order by date


NODE finished_data
SQL >

%
select
a.date as date,
coalesce(b.visits, 0) as visits,
{% if defined(pathname) or defined(post_uuid) %}coalesce(c.pageviews, 0){% else %}coalesce(b.pageviews, 0){% end %} as pageviews,
coalesce(b.bounce_rate, 0) as bounce_rate,
coalesce(b.avg_session_sec, 0) as avg_session_sec
from timeseries a
left join data b on a.date = b.date
{% if defined(pathname) or defined(post_uuid) %}left join pathname_pageviews c on a.date = c.date{% end %}
TYPE ENDPOINT
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
TOKEN "stats_page" READ
TOKEN "axis" READ

NODE _post_visitor_counts_0
SQL >
%
select
post_uuid,
uniqExact(session_id) as visits
from _mv_hits
where
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Site UUID", required=True)}}
and post_uuid IN {{ Array(post_uuids, description="Array of post UUIDs to get visitor counts for", required=True) }}
group by post_uuid
order by visits desc

TYPE ENDPOINT
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
TOKEN "stats_page" READ
TOKEN "axis" READ

NODE session_data
SQL >
%
SELECT
site_uuid,
session_id,
argMinMerge(device) as device
FROM _mv_session_data_v2
WHERE site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
GROUP BY site_uuid, session_id

NODE top_devices
SQL >
%
select
device,
count() as visits
from session_data sd
inner join filtered_sessions_v2 fs
on fs.session_id = sd.session_id
group by device
order by visits desc
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}

TYPE ENDPOINT
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
TOKEN "stats_page" READ
TOKEN "axis" READ

NODE _top_locations_0
SQL >

%
select
location,
uniqExact(session_id) as visits
from _mv_hits h
inner join filtered_sessions_v2 fs
on fs.session_id = h.session_id
where
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
{% if defined(member_status) %}
and member_status IN (
select arrayJoin(
{{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }}
|| if('paid' IN {{ Array(member_status) }}, ['comped'], [])
)
)
{% end %}
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
{% if defined(post_uuid) %} and post_uuid = {{ String(post_uuid, description="Post UUID to filter on", required=False) }} {% end %}
group by location
order by visits desc
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}

TYPE ENDPOINT
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
TOKEN "stats_page" READ
TOKEN "axis" READ

NODE _top_pages_0
SQL >

%
select
case when post_uuid = 'undefined' then '' else post_uuid end as post_uuid,
pathname,
uniqExact(session_id) as visits
from _mv_hits h
inner join filtered_sessions_v2 fs
on fs.session_id = h.session_id
where
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
{% if defined(member_status) %}
and member_status IN (
select arrayJoin(
{{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }}
|| if('paid' IN {{ Array(member_status) }}, ['comped'], [])
)
)
{% end %}
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
{% if defined(post_uuid) %} and post_uuid = {{ String(post_uuid, description="Post UUID to filter on", required=False) }} {% end %}
{% if defined(post_type) %}
{% if post_type == 'post' %}
and post_type = 'post'
{% else %}
and (post_type != 'post' or post_type is null)
{% end %}
{% end %}
group by post_uuid, pathname
order by visits desc
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}

TYPE ENDPOINT
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
TOKEN "stats_page" READ
TOKEN "axis" READ

NODE session_data
SQL >
%
SELECT
site_uuid,
session_id,
argMinMerge(source) as source
FROM _mv_session_data_v2
WHERE site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
GROUP BY site_uuid, session_id

NODE top_sources
SQL >
%
select
source,
count() as visits
from session_data sd
inner join filtered_sessions_v2 fs
on fs.session_id = sd.session_id
group by source
order by visits desc
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}

TYPE ENDPOINT
Loading
Loading