Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR changes member attribution URL handling for sent posts: attribution-builder now computes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
| case 'post': | ||
| case 'page': { | ||
| const post = await this.models.Post.findOne({id}, {require: false}); | ||
| const post = await this.models.Post.findOne({id}, {require: false, context: {internal: true}}); |
There was a problem hiding this comment.
This fixes the missing attribution for the link on email-only posts. Without the context: {internal: true}, Post.findOne() includes a status: published filter by default, which excludes posts sent as email-only.
When resolving the attribution for a newly created subscription, it therefore does not find a post, and falls back to the Homepage.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ghost/core/core/server/services/member-attribution/attribution-builder.js (1)
1-15: Consider updating theAttributionResourcetypedef.The
postStatusfield is now returned bygetResource()for post types, but it's not documented in theAttributionResourcetypedef. Adding it would improve documentation completeness and IDE support.📝 Proposed typedef update
/** * `@typedef` {object} AttributionResource * `@prop` {string|null} id * `@prop` {string|null} url (absolute URL) * `@prop` {'page'|'post'|'author'|'tag'|'url'|null} type * `@prop` {string|null} title + * `@prop` {string|undefined} postStatus * `@prop` {string|null} referrerSource * `@prop` {string|null} referrerMedium * `@prop` {string|null} referrerUrl🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/core/server/services/member-attribution/attribution-builder.js` around lines 1 - 15, The AttributionResource typedef is missing the postStatus property returned by getResource() for post types; update the typedef for AttributionResource to include a postStatus field (string or a union of expected statuses for posts, e.g., 'draft'|'published'|'scheduled'|null) so IDEs and docs reflect the data returned by getResource() and to reference this new property in any code handling post resources.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@ghost/core/core/server/services/member-attribution/attribution-builder.js`:
- Around line 1-15: The AttributionResource typedef is missing the postStatus
property returned by getResource() for post types; update the typedef for
AttributionResource to include a postStatus field (string or a union of expected
statuses for posts, e.g., 'draft'|'published'|'scheduled'|null) so IDEs and docs
reflect the data returned by getResource() and to reference this new property in
any code handling post resources.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0ad037c5-0858-48b1-8001-dc77846da471
📒 Files selected for processing (4)
ghost/admin/app/components/member/subscription-detail-box.hbsghost/core/core/server/api/endpoints/utils/serializers/output/members.jsghost/core/core/server/services/member-attribution/attribution-builder.jsghost/core/core/server/services/member-attribution/url-translator.js
58f5b3c to
50e5c2e
Compare
When a member clicked an offer link in an email, their subscription attribution resolved to the Homepage instead of the originating post. Two issues were fixed: Post.findOne in the URL translator used default filters (type:post+status:published) which excluded email-only posts (status:sent), now uses internal context. The URL service also returned /404/ for email-only posts since they have no public route — attribution now stores the /email/:uuid/ path instead, which is the correct frontend route for viewing email-only posts.
50e5c2e to
f5b0b8e
Compare
The test expected unpublished posts to not resolve during attribution lookup. Since we now use internal context to find posts regardless of status (to support email-only posts with status:sent), draft posts are also found. This is correct — attribution should persist even when a post is unpublished.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
ghost/core/test/e2e-server/services/member-attribution.test.js (1)
95-103: Guard fixture cleanup withtry/finally.If an assertion fails after Line 95, the restore at Line 103 is skipped and leaves shared fixture state mutated for later tests.
Suggested cleanup hardening
- await models.Post.edit({status: 'draft'}, {id}); - - assertObjectMatches(await attribution.fetchResource(), { - id: post.id, - type: 'post', - title: post.get('title') - }); - - await models.Post.edit({status: 'published'}, {id}); + await models.Post.edit({status: 'draft'}, {id}); + try { + assertObjectMatches(await attribution.fetchResource(), { + id: post.id, + type: 'post', + title: post.get('title') + }); + } finally { + await models.Post.edit({status: 'published'}, {id}); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/test/e2e-server/services/member-attribution.test.js` around lines 95 - 103, Wrap the temporary status change and assertion in a try/finally so the post is always restored: call models.Post.edit({status: 'draft'}, {id}) before the try, perform the assertion using attribution.fetchResource() inside the try, and in the finally call models.Post.edit({status: 'published'}, {id}) to guarantee cleanup even if the assertion fails; reference the existing post/id variables and the attribution.fetchResource() call when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ghost/core/test/e2e-server/services/member-attribution.test.js`:
- Around line 93-100: The "with subdirectory" variant of the test has stale
expectations (expects URL fallback) but the non-subdirectory variant now asserts
that an internal draft post still resolves to a post resource; update the
subdirectory test's assertion to match the main case by using
attribution.fetchResource() expectation with id: post.id, type: 'post' and the
same title value (derived from post.get('title')) after calling
models.Post.edit({status: 'draft'}, {id}); ensure the assertion in the "with
subdirectory" block mirrors the main block's shape and values so both use the
internal post resolution path.
---
Nitpick comments:
In `@ghost/core/test/e2e-server/services/member-attribution.test.js`:
- Around line 95-103: Wrap the temporary status change and assertion in a
try/finally so the post is always restored: call models.Post.edit({status:
'draft'}, {id}) before the try, perform the assertion using
attribution.fetchResource() inside the try, and in the finally call
models.Post.edit({status: 'published'}, {id}) to guarantee cleanup even if the
assertion fails; reference the existing post/id variables and the
attribution.fetchResource() call when making this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 46420484-321c-4fb0-ac06-18a724dae412
📒 Files selected for processing (1)
ghost/core/test/e2e-server/services/member-attribution.test.js
Instead of using context: {internal: true} which would also resolve
draft posts, use a specific filter status:[published,sent] to only
include published and email-only posts in attribution lookups. Reverted
test expectations back to original — draft posts should still not
resolve for attribution.
Using options.filter triggered NQL defaultFilters (type:post) which filtered out pages. Instead, use the default findOne query (which finds published posts and pages via simple WHERE clause) and fall back to a second query with status:sent for email-only posts.
…us filter Use type:[post,page]+status:[published,sent] to find published posts, pages, and email-only posts in a single query, overriding the default NQL filters for both type and status.
The attribution link in the member subscription detail now points to the Post Analytics page for post-type attributions, which works for both published and email-only posts.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #26899 +/- ##
==========================================
+ Coverage 73.12% 73.15% +0.02%
==========================================
Files 1537 1537
Lines 121397 121442 +45
Branches 14666 14681 +15
==========================================
+ Hits 88775 88844 +69
+ Misses 31593 31568 -25
- Partials 1029 1030 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Tests the exact scenario from the offer link bug: email-only posts (status:sent) are resolved via id/type history entries and use the /email/:uuid/ URL. Draft posts are excluded from attribution.
refs https://linear.app/ghost/issue/ONC-1569/post-attribution-for-links-in-emails-seems-to-be-broken-on-pro closes https://linear.app/ghost/issue/ONC-1568/email-only-post-attribution-incorrectly-resolves-to-url-type-instead ## Problem Attribution for links in email-only posts does not correctly resolve to the post itself. For example, if a free member clicks a link in an email-only post they received, then upgrade to a paid membership within the same browser session, the newly created subscription should be attributed to the email only post. Currently though, the subscription would be attributed as a `url` and, depending on the link, the URL might be the `/email/:uuid` route of the post, or the Homepage. ## Root cause In `/ghost/core/core/server/services/member-attribution/url-translator.js`, Ghost had previously been filtering out email only posts, because the Post.findOne() query implicitly filters to only `published` posts, while an email-only post will have a status of `sent`. ## Fix Updated the query in the url-translator to filter for `status:[published,sent]` and `type:[post,page]`. With this change, the attribution correctly resolves to the `post` itself, rather than the generic `url`. There is also a change to the attribution builder to support this: since email only post's don't have a URL registered in the URL service, the `attribution_url` was being set to `null`. This adds a conditional to check if the post is email only, and if so, it sets the `attribution_url` to `/email/:uuid` for the post. Finally, it also updated the link on the members page to the post to point to the Post Analytics, rather than the post on the frontend. This change is made for regular `published` posts in addition to the `sent` posts for consistency. ### Files changed - **`url-translator.js`** — Added fallback `findOne` query with `status:sent` for email-only posts; added `/email/:uuid/` URL for sent posts - **`attribution-builder.js`** — Use `/email/:uuid/` URL when resolving attribution for email-only posts at read time
refs https://linear.app/ghost/issue/ONC-1569/post-attribution-for-links-in-emails-seems-to-be-broken-on-pro
closes https://linear.app/ghost/issue/ONC-1568/email-only-post-attribution-incorrectly-resolves-to-url-type-instead
Problem
Attribution for links in email-only posts does not correctly resolve to the post itself. For example, if a free member clicks a link in an email-only post they received, then upgrade to a paid membership within the same browser session, the newly created subscription should be attributed to the email only post. Currently though, the subscription would be attributed as a
urland, depending on the link, the URL might be the/email/:uuidroute of the post, or the Homepage.Root cause
In
/ghost/core/core/server/services/member-attribution/url-translator.js, Ghost had previously been filtering out email only posts, because the Post.findOne() query implicitly filters to onlypublishedposts, while an email-only post will have a status ofsent.Fix
Updated the query in the url-translator to filter for
status:[published,sent]andtype:[post,page]. With this change, the attribution correctly resolves to thepostitself, rather than the genericurl.There is also a change to the attribution builder to support this: since email only post's don't have a URL registered in the URL service, the
attribution_urlwas being set tonull. This adds a conditional to check if the post is email only, and if so, it sets theattribution_urlto/email/:uuidfor the post.Finally, it also updated the link on the members page to the post to point to the Post Analytics, rather than the post on the frontend. This change is made for regular
publishedposts in addition to thesentposts for consistency.Files changed
url-translator.js— Added fallbackfindOnequery withstatus:sentfor email-only posts; added/email/:uuid/URL for sent postsattribution-builder.js— Use/email/:uuid/URL when resolving attribution for email-only posts at read timeTest plan
/email/:uuid/(not/404/)