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
This commit is contained in:
Rishabh Garg 2022-09-29 22:31:48 +05:30 committed by GitHub
parent 8a6f082b14
commit e3600d70ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 421 additions and 121 deletions

View File

@ -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
});

View File

@ -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<String>,
@ -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<String>,
@ -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<String>,
@ -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<String>,
@ -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<String>,
@ -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\\}\\\\//,

View File

@ -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<Object>,
"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<Object>,
"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<Object>,
"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<Object>,
"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<Object>,
"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<Object>,
"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<Object>,
"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<Object>,
"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",

View File

@ -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

View File

@ -14,6 +14,54 @@ class MemberAttributionService {
this.attributionBuilder = attributionBuilder;
}
/**
*
* @param {Object} context instance of ghost framework context object
* @returns {Promise<import('./attribution').AttributionResource|null>}
*/
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;

View File

@ -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 () {

View File

@ -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
*/

View File

@ -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) {