From e3600d70efa3f2397e87048216774be4f31cedcc Mon Sep 17 00:00:00 2001 From: Rishabh Garg Date: Thu, 29 Sep 2022 22:31:48 +0530 Subject: [PATCH] Added referrer attribution from request context (#15499) closes TryGhost/Team#2007 - uses request context to add referrer source and medium for a new member - uses integration name as referrer medium if exists --- .../services/member-attribution/index.js | 3 +- .../admin/__snapshots__/members.test.js.snap | 278 +++++++++++++++--- .../__snapshots__/webhooks.test.js.snap | 70 +---- .../test/e2e-api/members/webhooks.test.js | 21 +- ghost/member-attribution/lib/service.js | 82 +++++- ghost/member-attribution/test/service.test.js | 78 ++++- ghost/members-api/lib/repositories/member.js | 4 +- .../members-api/lib/services/member-bread.js | 6 +- 8 files changed, 421 insertions(+), 121 deletions(-) diff --git a/ghost/core/core/server/services/member-attribution/index.js b/ghost/core/core/server/services/member-attribution/index.js index e0d00de3e1..98e2014827 100644 --- a/ghost/core/core/server/services/member-attribution/index.js +++ b/ghost/core/core/server/services/member-attribution/index.js @@ -35,7 +35,8 @@ class MemberAttributionServiceWrapper { this.service = new MemberAttributionService({ models: { MemberCreatedEvent: models.MemberCreatedEvent, - SubscriptionCreatedEvent: models.SubscriptionCreatedEvent + SubscriptionCreatedEvent: models.SubscriptionCreatedEvent, + Integration: models.Integration }, attributionBuilder: this.attributionBuilder }); diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap index 077c2d549c..911073f6cf 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -539,7 +539,15 @@ exports[`Members API Can add 1: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -569,7 +577,7 @@ exports[`Members API Can add 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "641", + "content-length": "774", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -582,7 +590,15 @@ exports[`Members API Can add a member that is not subscribed (old) 1: [body] 1`] Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -612,7 +628,7 @@ exports[`Members API Can add a member that is not subscribed (old) 2: [headers] Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "517", + "content-length": "650", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": Any, @@ -652,7 +668,15 @@ Object { "subscribed": true, "subscriptions": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": null, + "referrer_source": null, + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "cancel_at_period_end": false, "cancellation_reason": null, "current_period_end": Any, @@ -703,7 +727,7 @@ exports[`Members API Can add a subscription 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2375", + "content-length": "2485", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -742,7 +766,15 @@ Object { "subscribed": true, "subscriptions": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": null, + "referrer_source": null, + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "cancel_at_period_end": false, "cancellation_reason": null, "current_period_end": Any, @@ -793,7 +825,7 @@ exports[`Members API Can add a subscription 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2375", + "content-length": "2485", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -805,7 +837,15 @@ exports[`Members API Can add and edit with custom newsletters 1: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -862,7 +902,7 @@ exports[`Members API Can add and edit with custom newsletters 2: [headers] 1`] = Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1361", + "content-length": "1494", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -875,7 +915,15 @@ exports[`Members API Can add and edit with custom newsletters 3: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -932,7 +980,7 @@ exports[`Members API Can add and edit with custom newsletters 4: [headers] 1`] = Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1360", + "content-length": "1493", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -944,7 +992,15 @@ exports[`Members API Can add and send a signup confirmation email (old) 1: [body Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1027,7 +1083,7 @@ exports[`Members API Can add and send a signup confirmation email (old) 2: [head Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1856", + "content-length": "1989", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": Any, @@ -1050,7 +1106,15 @@ exports[`Members API Can add and send a signup confirmation email 1: [body] 1`] Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1133,7 +1197,7 @@ exports[`Members API Can add and send a signup confirmation email 2: [headers] 1 Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1851", + "content-length": "1984", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": Any, @@ -1156,7 +1220,15 @@ exports[`Members API Can add complimentary subscription (out of date) 1: [body] Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1186,7 +1258,7 @@ exports[`Members API Can add complimentary subscription (out of date) 2: [header Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1119", + "content-length": "1252", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1199,7 +1271,15 @@ exports[`Members API Can add complimentary subscription (out of date) 3: [body] Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": true, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1246,7 +1326,7 @@ exports[`Members API Can add complimentary subscription (out of date) 4: [header Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2655", + "content-length": "2898", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -1555,7 +1635,15 @@ exports[`Members API Can create a member with an existing complimentary subscrip Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": true, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1629,7 +1717,7 @@ exports[`Members API Can create a member with an existing complimentary subscrip Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2706", + "content-length": "2949", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1642,7 +1730,15 @@ exports[`Members API Can create a member with an existing paid subscription 1: [ Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1699,7 +1795,7 @@ exports[`Members API Can create a member with an existing paid subscription 2: [ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2692", + "content-length": "2935", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1712,7 +1808,15 @@ exports[`Members API Can create a new member with a product (complimentary) 1: [ Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": true, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1786,7 +1890,7 @@ exports[`Members API Can create a new member with a product (complimentary) 2: [ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2463", + "content-length": "2596", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1809,7 +1913,15 @@ exports[`Members API Can destroy 1: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1839,7 +1951,7 @@ exports[`Members API Can destroy 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1826", + "content-length": "1959", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1892,7 +2004,15 @@ exports[`Members API Can edit by id 1: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1922,7 +2042,7 @@ exports[`Members API Can edit by id 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1137", + "content-length": "1270", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1935,7 +2055,15 @@ exports[`Members API Can edit by id 3: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1965,7 +2093,7 @@ exports[`Members API Can edit by id 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "492", + "content-length": "625", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -3403,7 +3531,15 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -3433,7 +3569,7 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "499", + "content-length": "632", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -3446,7 +3582,15 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -3529,7 +3673,7 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1840", + "content-length": "1973", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -3541,7 +3685,15 @@ exports[`Members API Can subscribe to a newsletter 1: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -3571,7 +3723,7 @@ exports[`Members API Can subscribe to a newsletter 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1127", + "content-length": "1260", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -3584,7 +3736,15 @@ exports[`Members API Can subscribe to a newsletter 3: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -3614,7 +3774,7 @@ exports[`Members API Can subscribe to a newsletter 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1183", + "content-length": "1316", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -3626,7 +3786,7 @@ exports[`Members API Can subscribe to a newsletter 5: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "4822", + "content-length": "4978", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -3638,7 +3798,15 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -3695,7 +3863,7 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1145", + "content-length": "1278", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -3708,7 +3876,15 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -3738,7 +3914,7 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "504", + "content-length": "637", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -4075,7 +4251,15 @@ exports[`Members API Subscribes to default newsletters 1: [body] 1`] = ` Object { "members": Array [ Object { - "attribution": null, + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": "url", + "url": null, + }, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -4105,7 +4289,7 @@ exports[`Members API Subscribes to default newsletters 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1827", + "content-length": "1960", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, diff --git a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap index 7e71810ace..f489440365 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap @@ -4,15 +4,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "members": Array [ Object { - "attribution": Object { - "id": "1", - "referrer_medium": null, - "referrer_source": null, - "referrer_url": null, - "title": "Joe Bloggs", - "type": "author", - "url": "http://127.0.0.1:2369/author/joe-bloggs/", - }, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -54,15 +46,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "members": Array [ Object { - "attribution": Object { - "id": null, - "referrer_medium": null, - "referrer_source": null, - "referrer_url": null, - "title": "/removed-blog-post/", - "type": "url", - "url": "http://127.0.0.1:2369/removed-blog-post/", - }, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -104,7 +88,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "members": Array [ Object { - "attribution": null, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -134,7 +118,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2611", + "content-length": "2831", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -146,15 +130,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "members": Array [ Object { - "attribution": Object { - "id": "618ba1ffbe2896088840a6e9", - "referrer_medium": null, - "referrer_source": null, - "referrer_url": null, - "title": "This is a static page", - "type": "page", - "url": "http://127.0.0.1:2369/static-page-test/", - }, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -196,15 +172,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "members": Array [ Object { - "attribution": Object { - "id": "618ba1ffbe2896088840a6df", - "referrer_medium": null, - "referrer_source": null, - "referrer_url": null, - "title": "HTML Ipsum", - "type": "post", - "url": "http://127.0.0.1:2369/html-ipsum/", - }, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -246,15 +214,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "members": Array [ Object { - "attribution": Object { - "id": "618ba1febe2896088840a6db", - "referrer_medium": null, - "referrer_source": null, - "referrer_url": null, - "title": "kitchen sink", - "type": "tag", - "url": "http://127.0.0.1:2369/tag/kitchen-sink/", - }, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -296,15 +256,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "members": Array [ Object { - "attribution": Object { - "id": null, - "referrer_medium": null, - "referrer_source": null, - "referrer_url": null, - "title": "homepage", - "type": "url", - "url": "http://127.0.0.1:2369/", - }, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -346,7 +298,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent witho Object { "members": Array [ Object { - "attribution": null, + "attribution": Any, "avatar_image": null, "comped": false, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -376,7 +328,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent witho Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2611", + "content-length": "2831", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -439,7 +391,7 @@ exports[`Members API Member attribution Returns subscription created attribution Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "14502", + "content-length": "14722", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", diff --git a/ghost/core/test/e2e-api/members/webhooks.test.js b/ghost/core/test/e2e-api/members/webhooks.test.js index 41bdcb7d6a..c7af12145b 100644 --- a/ghost/core/test/e2e-api/members/webhooks.test.js +++ b/ghost/core/test/e2e-api/members/webhooks.test.js @@ -1663,6 +1663,7 @@ describe('Members API', function () { subscriptions: anyArray, labels: anyArray, tiers: anyArray, + attribution: anyObject, newsletters: anyArray }; @@ -1957,13 +1958,29 @@ describe('Members API', function () { it('Creates a SubscriptionCreatedEvent without attribution', async function () { const attribution = undefined; - await testWithAttribution(attribution, null); + await testWithAttribution(attribution, { + id: null, + url: null, + type: 'url', + title: null, + referrer_source: null, + referrer_medium: null, + referrer_url: null + }); }); it('Creates a SubscriptionCreatedEvent with empty attribution object', async function () { // Shouldn't happen, but to make sure we handle it const attribution = {}; - await testWithAttribution(attribution, null); + await testWithAttribution(attribution, { + id: null, + url: null, + type: 'url', + title: null, + referrer_source: null, + referrer_medium: null, + referrer_url: null + }); }); // Activity feed diff --git a/ghost/member-attribution/lib/service.js b/ghost/member-attribution/lib/service.js index 68a9cd3221..1c5795f81b 100644 --- a/ghost/member-attribution/lib/service.js +++ b/ghost/member-attribution/lib/service.js @@ -14,6 +14,54 @@ class MemberAttributionService { this.attributionBuilder = attributionBuilder; } + /** + * + * @param {Object} context instance of ghost framework context object + * @returns {Promise} + */ + async getAttributionFromContext(context) { + if (!context) { + return null; + } + + const source = this._resolveContextSource(context); + + // We consider only select internal context sources + if (['import', 'api', 'admin'].includes(source)) { + let attribution = { + id: null, + type: null, + url: null, + title: null, + referrerUrl: null, + referrerSource: null, + referrerMedium: null + }; + if (source === 'import') { + attribution.referrerSource = 'Imported'; + attribution.referrerMedium = 'Member Importer'; + } else if (source === 'admin') { + attribution.referrerSource = 'Created manually'; + attribution.referrerMedium = 'Ghost Admin'; + } else if (source === 'api') { + attribution.referrerSource = 'Created via API'; + attribution.referrerMedium = 'Admin API'; + } + + // If context has integration, set referrer medium as integration anme + if (context?.integration?.id) { + try { + const integration = await this.models.Integration.findOne({id: context.integration.id}); + attribution.referrerSource = `Integration: ${integration?.get('name')}`; + } catch (error) { + // ignore error for integration not found + } + } + return attribution; + } + return null; + } + /** * * @param {import('./history').UrlHistoryArray} historyArray @@ -63,10 +111,6 @@ class MemberAttributionService { * @returns {import('./attribution').AttributionResource|null} */ getEventAttribution(eventModel) { - if (eventModel.get('attribution_type') === null) { - return null; - } - const _attribution = this.attributionBuilder.build({ id: eventModel.get('attribution_id'), url: eventModel.get('attribution_url'), @@ -76,7 +120,7 @@ class MemberAttributionService { referrerUrl: eventModel.get('referrer_url') }); - if (_attribution.type !== 'url') { + if (_attribution.type && _attribution.type !== 'url') { // Find the right relation to use to fetch the resource const tryRelations = [ eventModel.related('postAttribution'), @@ -100,7 +144,7 @@ class MemberAttributionService { */ async getMemberCreatedAttribution(memberId) { const memberCreatedEvent = await this.models.MemberCreatedEvent.findOne({member_id: memberId}, {require: false, withRelated: []}); - if (!memberCreatedEvent || !memberCreatedEvent.get('attribution_type')) { + if (!memberCreatedEvent) { return null; } const attribution = this.attributionBuilder.build({ @@ -121,7 +165,7 @@ class MemberAttributionService { */ async getSubscriptionCreatedAttribution(subscriptionId) { const subscriptionCreatedEvent = await this.models.SubscriptionCreatedEvent.findOne({subscription_id: subscriptionId}, {require: false, withRelated: []}); - if (!subscriptionCreatedEvent || !subscriptionCreatedEvent.get('attribution_type')) { + if (!subscriptionCreatedEvent) { return null; } const attribution = this.attributionBuilder.build({ @@ -134,6 +178,30 @@ class MemberAttributionService { }); return await attribution.fetchResource(); } + + /** + * Maps the framework context to source string + * @param {Object} context instance of ghost framework context object + * @returns {'import' | 'system' | 'api' | 'admin' | 'member'} + * @private + */ + _resolveContextSource(context) { + let source; + + if (context.import || context.importer) { + source = 'import'; + } else if (context.internal) { + source = 'system'; + } else if (context.api_key) { + source = 'api'; + } else if (context.user) { + source = 'admin'; + } else { + source = 'member'; + } + + return source; + } } module.exports = MemberAttributionService; diff --git a/ghost/member-attribution/test/service.test.js b/ghost/member-attribution/test/service.test.js index 856f9bfccd..25df2b9992 100644 --- a/ghost/member-attribution/test/service.test.js +++ b/ghost/member-attribution/test/service.test.js @@ -10,16 +10,90 @@ describe('MemberAttributionService', function () { }); }); + describe('getAttributionFromContext', function () { + it('returns null if no context is provided', async function () { + const service = new MemberAttributionService({}); + const attribution = await service.getAttributionFromContext(); + + should(attribution).be.null(); + }); + + it('returns attribution for importer context', async function () { + const service = new MemberAttributionService({}); + const attribution = await service.getAttributionFromContext({importer: true}); + + should(attribution).containEql({referrerSource: 'Imported', referrerMedium: 'Member Importer'}); + }); + + it('returns attribution for admin context', async function () { + const service = new MemberAttributionService({}); + const attribution = await service.getAttributionFromContext({user: 'abc'}); + + should(attribution).containEql({referrerSource: 'Created manually', referrerMedium: 'Ghost Admin'}); + }); + + it('returns attribution for api without integration context', async function () { + const service = new MemberAttributionService({}); + const attribution = await service.getAttributionFromContext({ + api_key: 'abc' + }); + + should(attribution).containEql({referrerSource: 'Created via API', referrerMedium: 'Admin API'}); + }); + + it('returns attribution for api with integration context', async function () { + const service = new MemberAttributionService({ + models: { + Integration: { + findOne: () => { + return { + get: () => 'Test Integration' + }; + } + } + } + }); + const attribution = await service.getAttributionFromContext({ + api_key: 'abc', + integration: {id: 'integration_1'} + }); + + should(attribution).containEql({referrerSource: 'Integration: Test Integration', referrerMedium: 'Admin API'}); + }); + }); + describe('getEventAttribution', function () { it('returns null if attribution_type is null', function () { - const service = new MemberAttributionService({}); + const service = new MemberAttributionService({ + attributionBuilder: { + build(attribution) { + return { + ...attribution, + getResource() { + return { + ...attribution, + title: 'added' + }; + } + }; + } + } + }); const model = { id: 'event_id', get() { return null; } }; - should(service.getEventAttribution(model)).eql(null); + should(service.getEventAttribution(model)).eql({ + id: null, + url: null, + title: 'added', + type: null, + referrerSource: null, + referrerMedium: null, + referrerUrl: null + }); }); it('returns url attribution types', function () { diff --git a/ghost/members-api/lib/repositories/member.js b/ghost/members-api/lib/repositories/member.js index b6f8dba66f..2aa3bc3b20 100644 --- a/ghost/members-api/lib/repositories/member.js +++ b/ghost/members-api/lib/repositories/member.js @@ -205,7 +205,7 @@ module.exports = class MemberRepository { * @param {Object[]} [data.newsletters] * @param {Object} [data.stripeCustomer] * @param {string} [data.offerId] - * @param {import('@tryghost/member-attribution/lib/history').Attribution} [data.attribution] + * @param {import('@tryghost/member-attribution/lib/attribution').AttributionResource} [data.attribution] * @param {*} options * @returns */ @@ -778,7 +778,7 @@ module.exports = class MemberRepository { * @param {String} data.id - member ID * @param {Object} data.subscription * @param {String} data.offerId - * @param {import('@tryghost/member-attribution/lib/history').Attribution} data.attribution + * @param {import('@tryghost/member-attribution/lib/attribution').AttributionResource} [data.attribution] * @param {*} options * @returns */ diff --git a/ghost/members-api/lib/services/member-bread.js b/ghost/members-api/lib/services/member-bread.js index 824c3c5723..d4e5fb4442 100644 --- a/ghost/members-api/lib/services/member-bread.js +++ b/ghost/members-api/lib/services/member-bread.js @@ -174,7 +174,7 @@ module.exports = class MemberBREADService { async attachAttributionsToMember(member, subscriptionIdMap) { // Created attribution member.attribution = await this.memberAttributionService.getMemberCreatedAttribution(member.id); - + // Subscriptions attributions for (const subscription of member.subscriptions) { if (!subscription.id) { @@ -254,6 +254,10 @@ module.exports = class MemberBREADService { let model; try { + const attribution = await this.memberAttributionService.getAttributionFromContext(options?.context); + if (attribution) { + data.attribution = attribution; + } model = await this.memberRepository.create(data, options); } catch (error) { if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {