🐛 Fixed non-deterministic member attribution resolution#28491
Conversation
no ref - created-event lookups used findOne on the non-unique member_id / subscription_id columns with no ORDER BY, so when more than one event row existed for a member or subscription the database could return an arbitrary row, making attribution resolution non-deterministic - the lookups now order by created_at, id (descending) — matching the existing deterministic pattern in member-created-event.js — so the most recent created event is always selected
WalkthroughThis PR refactors member and subscription attribution event lookups to use deterministic ordered queries instead of implicit single-record lookup. The service methods 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ghost/core/test/unit/server/services/member-attribution/service.test.js (1)
5-14: ⚡ Quick winAssert ordering in the chainable mock to protect the determinism contract.
Current tests validate output shape but don’t verify that the query callback applies
orderBy('created_at','desc')thenorderBy('id','desc'). A small enhancement to the helper would prevent silent regressions to non-deterministic lookup behavior.Proposed test-helper hardening
function createEventModelMock(event) { + const orderByCalls = []; const chainable = { where: () => chainable, - query: () => chainable, + query: (cb) => { + cb({ + orderBy: (col, dir) => { + orderByCalls.push([col, dir]); + return this; + } + }); + return chainable; + }, fetch: async () => event }; + chainable._getOrderByCalls = () => orderByCalls; return chainable; }Then assert in each deterministic lookup test that:
['created_at', 'desc']['id', 'desc']were both applied in order.
Also applies to: 282-283, 293-305, 336-337, 347-359
🤖 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/core/test/unit/server/services/member-attribution/service.test.js` around lines 5 - 14, The chainable test helper createEventModelMock currently ignores the query callback so tests don't validate ordering; update createEventModelMock so its query(fn) invokes the provided fn with a mock builder that records calls to orderBy (pushing each [column, direction] into an array) and then returns chainable, and expose that recorded array (e.g., as chainable._appliedOrders) so tests can assert the sequence equals [['created_at','desc'], ['id','desc']] in the deterministic lookup tests; ensure where and fetch behavior is unchanged (where returns chainable, fetch resolves to event).
🤖 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.
Nitpick comments:
In `@ghost/core/test/unit/server/services/member-attribution/service.test.js`:
- Around line 5-14: The chainable test helper createEventModelMock currently
ignores the query callback so tests don't validate ordering; update
createEventModelMock so its query(fn) invokes the provided fn with a mock
builder that records calls to orderBy (pushing each [column, direction] into an
array) and then returns chainable, and expose that recorded array (e.g., as
chainable._appliedOrders) so tests can assert the sequence equals
[['created_at','desc'], ['id','desc']] in the deterministic lookup tests; ensure
where and fetch behavior is unchanged (where returns chainable, fetch resolves
to event).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dbbd84e4-e4e9-4dd7-8a7f-bcf7fca98b12
📒 Files selected for processing (2)
ghost/core/core/server/services/member-attribution/member-attribution-service.jsghost/core/test/unit/server/services/member-attribution/service.test.js
Summary
Makes member- and subscription-created attribution lookups deterministic.
getMemberCreatedAttributionandgetSubscriptionCreatedAttributioninmember-attribution-service.jslooked up the created event withfindOne({member_id})/findOne({subscription_id}). Those columns are not unique and the queries had noORDER BY, so when more than one created-event row existed for a member or subscription the database could return an arbitrary row — making attribution resolution non-deterministic (and a source of flakiness).The lookups now order by
created_at, id(descending) and fetch the most recent row, matching the existing deterministic pattern already used inmember-created-event.js.Changes
member-attribution-service.js:findOne(...)→.where(col, id).query(qb => qb.orderBy('created_at','desc').orderBy('id','desc')).fetch(...)in both created-attribution lookupsservice.test.js: updated the model mocks to the new.where().query().fetch()chainTesting
pnpm --dir ghost/core test:single test/unit/server/services/member-attribution/service.test.js— 20 passingpnpm --dir ghost/core test:single test/e2e-server/services/member-attribution.test.js— 16 passingContext
Found while reviewing #23146 — this core attribution fix was bundled with an unrelated serializer field addition, so it's been split out into its own PR.