Skip to content

🐛 Fixed slow posts and pages lists when analytics data was unavailable#29048

Merged
9larsons merged 2 commits into
mainfrom
steve/onc-1871-non-blocking-post-analytics
Jul 2, 2026
Merged

🐛 Fixed slow posts and pages lists when analytics data was unavailable#29048
9larsons merged 2 commits into
mainfrom
steve/onc-1871-non-blocking-post-analytics

Conversation

@9larsons

@9larsons 9larsons commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

ref https://linear.app/ghost/issue/ONC-1871/possible-performance-issues-early-on-1st-july

Problem

During the Tinybird slowdown early on July 1, customers reported the admin posts/pages lists taking up to 60s to render (and the Analytics page not loading).

The lists were slow because PostsWithAnalytics.afterInfinityModel awaited the visitor-count fetch before returning posts, and the route's model hook waits on that — so the whole screen sat on the loading spinner until the stats request finished. Server-side, each stats/posts-visitor-counts request proxies to Tinybird with a 10s timeout and got's default 2 retries (timeouts/5xx are retried), so a hanging Tinybird held each request for ~30s before returning an empty 200. Filter changes reset the analytics cache and refetch, blocking again each time.

Fix

Fire the visitor-count and member-count loads without awaiting them. The post-analytics service exposes the counts as tracked state that the list-item components already read reactively (and they already handle null counts), so the list renders immediately and the count columns populate whenever the data arrives. An analytics outage now costs nothing but temporarily missing count badges.

Testing

  • ember test --filter="Posts / Pages" — 39 passing (includes the analytics column visibility tests)
  • ember test --filter="post-analytics" — 19 passing
  • lint clean

Possible follow-up (separate PR): cap retries / shorten the timeout on the interactive stats endpoints so they fail fast server-side too.

ref https://linear.app/ghost/issue/ONC-1871/possible-performance-issues-early-on-1st-july

- the posts/pages list route blocked rendering while it awaited visitor
  counts from the Tinybird-backed stats endpoint, so a slow or down
  analytics service delayed the whole screen (~30s+ per request: 10s
  timeout x 3 attempts server-side) even though the counts are optional
- the analytics service already exposes counts as tracked state that the
  list rows read reactively, so the requests can run in the background
  and the columns populate whenever the data arrives
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e2a6c48-f3e1-4ec3-acd1-bc4389a56de9

📥 Commits

Reviewing files that changed from the base of the PR and between a73f87c and 0e56909.

📒 Files selected for processing (2)
  • ghost/admin/app/services/post-analytics.js
  • ghost/admin/tests/unit/services/post-analytics-test.js

Walkthrough

The afterInfinityModel method in PostsWithAnalytics was changed to call visitor and member analytics loaders without awaiting their results. PostAnalyticsService now tracks a generation counter, increments it on reset, and ignores stale in-flight visitor and member count requests. Unit tests were added for reset races covering both visitor and member analytics loads.

Changes

  • Removed async/await and Promise.all from afterInfinityModel.
  • Added a generation-based staleness guard in PostAnalyticsService.
  • Updated visitor and member count task workers to skip stale writes.
  • Added reset tests for pending visitor and member analytics requests.

Sequence Diagram(s)

sequenceDiagram
  participant PostsRoute
  participant PostAnalyticsService
  participant VisitorTask
  participant MemberTask
  PostsRoute->>PostAnalyticsService: loadVisitorCounts(posts)
  PostsRoute->>PostAnalyticsService: loadMemberCounts(posts)
  PostAnalyticsService->>VisitorTask: _loadVisitorCounts(..., generation)
  PostAnalyticsService->>MemberTask: _loadMemberCounts(..., generation)
  PostAnalyticsService->>PostAnalyticsService: reset() increments _generation
  VisitorTask-->>PostAnalyticsService: ignore stale results
  MemberTask-->>PostAnalyticsService: ignore stale results
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: posts and pages lists no longer stall when analytics data is unavailable.
Description check ✅ Passed The description directly matches the changes, explaining the blocking analytics fetches and the non-blocking fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch steve/onc-1871-non-blocking-post-analytics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 0e56909

Command Status Duration Result
nx run @tryghost/admin:build ✅ Succeeded 2m 6s View ↗
nx run ghost-admin:test ✅ Succeeded 2m 25s View ↗
nx run ghost:build:assets ✅ Succeeded 2s View ↗
nx run-many -t lint -p ghost-admin ✅ Succeeded 18s View ↗
nx run ghost:build:tsc ✅ Succeeded 7s View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded 1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-02 14:08:42 UTC

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
ghost/admin/app/routes/posts.js (1)

16-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Overlapping fetch logic with _fetchAnalyticsForPosts.

_fetchAnalyticsForPosts (Lines 156-191, unchanged) already fetches visitor/member counts for publishedAndSentInfinityModel.content from setupController, duplicating what afterInfinityModel now does for every page (including the first). It's harmless today thanks to the _fetchedUuids/_fetchedMemberIds caches short-circuiting the second call, but it's redundant code that could diverge over time.

Consider whether _fetchAnalyticsForPosts/its call site is still needed now that afterInfinityModel covers all pages non-blockingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/admin/app/routes/posts.js` around lines 16 - 35, The analytics fetch
path is duplicated between afterInfinityModel and
_fetchAnalyticsForPosts/setupController, causing redundant visitor/member count
requests for the first page and risking divergence later. Consolidate the logic
into a single place: either keep afterInfinityModel as the non-blocking source
for all pages and remove the now-redundant _fetchAnalyticsForPosts call path, or
have both delegate to one shared helper so the fetch behavior stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/admin/app/routes/posts.js`:
- Around line 16-35: The analytics reset flow in
afterInfinityModel/postAnalytics can leave in-flight
loadVisitorCounts/loadMemberCounts requests able to write stale counts back
after reset() clears the cache. Update the reset() path in the postAnalytics
logic to cancel or invalidate any pending _loadVisitorCounts/_loadMemberCounts
work, and add a guard so late responses are ignored and cannot repopulate
visitorCounts or memberCounts after filters change.

---

Nitpick comments:
In `@ghost/admin/app/routes/posts.js`:
- Around line 16-35: The analytics fetch path is duplicated between
afterInfinityModel and _fetchAnalyticsForPosts/setupController, causing
redundant visitor/member count requests for the first page and risking
divergence later. Consolidate the logic into a single place: either keep
afterInfinityModel as the non-blocking source for all pages and remove the
now-redundant _fetchAnalyticsForPosts call path, or have both delegate to one
shared helper so the fetch behavior stays consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c4de0b5-306d-41e0-9c49-a6f54ea81cf3

📥 Commits

Reviewing files that changed from the base of the PR and between ae505e8 and a73f87c.

📒 Files selected for processing (1)
  • ghost/admin/app/routes/posts.js

Comment thread ghost/admin/app/routes/posts.js
ref https://linear.app/ghost/issue/ONC-1871/possible-performance-issues-early-on-1st-july

- loadVisitorCounts/loadMemberCounts run as plain (non-restartable)
  ember-concurrency tasks, so an in-flight request from a previous
  filter view isn't cancelled when reset() clears the cache for the
  new one - a late response could still merge its stale data back
  into visitorCounts/memberCounts after the view had moved on
- became easier to hit once the list stopped blocking on analytics,
  since users can now change filters again while a slow request is
  still in flight
- tag each load with the reset "generation" it started on and ignore
  the response if reset() has since moved the cache to a new
  generation
@9larsons 9larsons enabled auto-merge (squash) July 2, 2026 14:14
@9larsons 9larsons merged commit d1fd00a into main Jul 2, 2026
41 checks passed
@9larsons 9larsons deleted the steve/onc-1871-non-blocking-post-analytics branch July 2, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant