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
5 changes: 2 additions & 3 deletions apps/admin-x-framework/src/providers/framework-provider.tsx
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 Expand Up @@ -86,9 +87,7 @@ export function FrameworkProvider({children, queryClientOptions, ...props}: Fram
return (
<SentryErrorBoundary>
<QueryClientProvider client={client}>
<FrameworkContext.Provider value={props}>
{children}
</FrameworkContext.Provider>
<FrameworkContext.Provider children={children} value={props} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check the actual code at the specified location
head -n 95 apps/admin-x-framework/src/providers/framework-provider.tsx | tail -n 10

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 333


🏁 Script executed:

# Check for Biome configuration
find . -name "biome.json" -o -name "biome.jsonc" -o -name ".biomeignore"

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 96


🏁 Script executed:

# Search for lint rules in the codebase
rg "noChildrenProp" --type json

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 66


🏁 Script executed:

# Read the Biome configuration
cat biome.json

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 1859


🏁 Script executed:

# Search for any lint configuration related to children or correctness
rg "children" biome.json -A 3 -B 3

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 66


🌐 Web query:

Biome lint rule noChildrenProp jsx children prop

💡 Result:

Biome’s lint rule is lint/correctness/noChildrenProp (rule name: noChildrenProp). It disallows passing children via a JSX prop, e.g. <MyComp children={...} />, because the canonical React pattern is to pass children between tags (or as additional args to React.createElement). It’s recommended and enabled by default, and Biome reports it as an error. [1]

What it flags

// ❌ flagged
<FirstComponent children={'foo'} />

// ❌ flagged
React.createElement('div', { children: 'foo' });

[1]

What to do instead

// ✅ preferred
<FirstComponent>{'foo'}</FirstComponent>

// ✅ preferred
React.createElement('div', {}, 'foo');

[1]

Config (if you want to explicitly set it)

{
  "linter": {
    "rules": {
      "correctness": {
        "noChildrenProp": "error"
      }
    }
  }
}

[1]

If you’re trying to allow function-as-children patterns, note that the upstream ESLint rule (react/no-children-prop) documents an allowFunctions option (Biome may not expose that option the same way). [2]

Sources: Biome rule docs [1]; upstream ESLint rule behavior/options [2].

[1] https://biomejs.dev/linter/rules/no-children-prop/
[2] https://help.klocwork.com/2024/en-us/reference/js.react.no.children.prop.htm


Avoid passing children as a prop—Biome lint error.

The noChildrenProp rule is enabled by default (via "recommended": true in biome.json) and will fail linting. Pass children between tags instead.

🔧 Proposed fix
-                <FrameworkContext.Provider children={children} value={props} />
+                <FrameworkContext.Provider value={props}>
+                    {children}
+                </FrameworkContext.Provider>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<FrameworkContext.Provider children={children} value={props} />
<FrameworkContext.Provider value={props}>
{children}
</FrameworkContext.Provider>
🧰 Tools
🪛 Biome (2.3.13)

[error] 90-90: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🤖 Prompt for AI Agents
In `@apps/admin-x-framework/src/providers/framework-provider.tsx` at line 90,
Replace the usage of FrameworkContext.Provider that passes children as a prop:
remove the children prop and render children between the Provider tags instead;
update the JSX where FrameworkContext.Provider is used (the element currently
written as FrameworkContext.Provider with children={children} and value={props})
to use <FrameworkContext.Provider
value={props}>children</FrameworkContext.Provider> style so children are not
passed as a prop.

</QueryClientProvider>
</SentryErrorBoundary>
);
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 ? `${config.version}_${endpointName}` : endpointName;

return `${baseUrl}/v0/pipes/${finalEndpointName}.json?${params}`;
Comment on lines +17 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix versioned endpoint naming to match v2 pipe suffixes.

Current logic builds v2_<endpointName>, but the v2 pipes in this PR are suffixed (e.g., api_top_utm_sources_v2). With config.version=v2 and endpointName=api_top_utm_sources, this will point at a non-existent pipe. Also the comment says “suffix” but the code prefixes.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Append version suffix if provided (e.g., "v2" -> "api_kpis_v2")
const finalEndpointName = config.version ? `${config.version}_${endpointName}` : endpointName;
return `${baseUrl}/v0/pipes/${finalEndpointName}.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}`;
🤖 Prompt for AI Agents
In `@apps/admin-x-framework/src/utils/stats-config.ts` around lines 17 - 20, The
code builds versioned endpoint names by prefixing the version (const
finalEndpointName = config.version ? `${config.version}_${endpointName}` :
endpointName) but v2 pipes use a suffix (e.g., api_top_utm_sources_v2); update
the logic in stats-config (where finalEndpointName is computed) to append the
version as a suffix when config.version is present and adjust the comment to say
"append version suffix" so finalEndpointName becomes
`${endpointName}_${config.version}` for versioned endpoints.

};

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
Comment on lines +18 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Include site_uuid in the filtered_sessions_v2 join.

Joining only on session_id can mix sessions across tenants if IDs collide. Safer to join on both session_id and site_uuid.

🔧 Proposed fix
-    from session_data sd
-      inner join filtered_sessions_v2 fs
-         on fs.session_id = sd.session_id
+    from session_data sd
+      inner join filtered_sessions_v2 fs
+         on fs.session_id = sd.session_id
+        and fs.site_uuid = sd.site_uuid
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
select
device,
count() as visits
from session_data sd
inner join filtered_sessions_v2 fs
on fs.session_id = sd.session_id
and fs.site_uuid = sd.site_uuid
group by device
🤖 Prompt for AI Agents
In `@ghost/core/core/server/data/tinybird/endpoints/api_top_devices_v2.pipe`
around lines 18 - 24, The join between session_data (sd) and
filtered_sessions_v2 (fs) only matches on session_id which can cross tenants;
update the ON clause in the query (the inner join between filtered_sessions_v2
fs and session_data sd) to include fs.site_uuid = sd.site_uuid in addition to
fs.session_id = sd.session_id so rows are matched per-tenant; no other
structural changes required unless you also want to surface site_uuid in
SELECT/GROUP BY for downstream use.

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
Loading