Added signup form attribution (#16899)

fixes https://github.com/TryGhost/Team/issues/3331

This adds attribution tracking to the signup form. It sends a newly
created url history when sending the signup API call, this url history
will get translated to a proper attribution and saved on the backend. We
send a history with only a single item that contains the referrer
source, medium and path of the Embed form.

This also makes some changes to the E2E tests so that the tests run
in an https environment instead of about:blank.
This commit is contained in:
Simon Backx 2023-06-01 10:18:11 +02:00 committed by GitHub
parent e8220b1387
commit 7e27d3f3e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 226 additions and 99 deletions

View File

@ -366,7 +366,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -402,7 +402,7 @@ exports[`Members API Adding newsletters to member with no subscriptions works ev
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": "694",
"content-length": "693",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -422,7 +422,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -493,7 +493,7 @@ exports[`Members API Adding newsletters to member with no subscriptions works ev
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": "1597",
"content-length": "1596",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -764,7 +764,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -800,7 +800,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": "827",
"content-length": "826",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -820,7 +820,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -856,7 +856,7 @@ exports[`Members API Can add a member and trigger host email verification limits
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": "2442",
"content-length": "2441",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -876,7 +876,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -912,7 +912,7 @@ exports[`Members API Can add a member and trigger host email verification limits
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": "2442",
"content-length": "2441",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -932,7 +932,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -968,7 +968,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": "703",
"content-length": "702",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1019,7 +1019,7 @@ Object {
"referrer_source": null,
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"cancel_at_period_end": false,
@ -1111,7 +1111,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": "3545",
"content-length": "3544",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1161,7 +1161,7 @@ Object {
"referrer_source": null,
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"cancel_at_period_end": false,
@ -1253,7 +1253,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": "3545",
"content-length": "3544",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1272,7 +1272,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -1343,7 +1343,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": "1753",
"content-length": "1752",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1363,7 +1363,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -1434,7 +1434,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": "1752",
"content-length": "1751",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1453,7 +1453,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -1558,7 +1558,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": "2454",
"content-length": "2453",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1589,7 +1589,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -1694,7 +1694,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": "2449",
"content-length": "2448",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1929,7 +1929,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -1965,7 +1965,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": "1511",
"content-length": "1510",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1985,7 +1985,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -2041,7 +2041,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": "3271",
"content-length": "3269",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2400,7 +2400,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -2491,7 +2491,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": "3322",
"content-length": "3320",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2511,7 +2511,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -2602,7 +2602,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": "3308",
"content-length": "3306",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2622,7 +2622,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -2713,7 +2713,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": "2969",
"content-length": "2968",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2755,7 +2755,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -2791,7 +2791,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": "2424",
"content-length": "2423",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2884,7 +2884,7 @@ Object {
"referrer_source": null,
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"cancel_at_period_end": false,
@ -2976,7 +2976,7 @@ exports[`Members API Can edit 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": "3388",
"content-length": "3387",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -3026,7 +3026,7 @@ Object {
"referrer_source": null,
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"cancel_at_period_end": false,
@ -3079,7 +3079,7 @@ exports[`Members API Can edit 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": "2488",
"content-length": "2487",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -3098,7 +3098,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -3134,7 +3134,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": "1529",
"content-length": "1528",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -3154,7 +3154,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -3190,7 +3190,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": "678",
"content-length": "677",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -4889,7 +4889,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -4925,7 +4925,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": "685",
"content-length": "684",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -4945,7 +4945,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5050,7 +5050,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": "2438",
"content-length": "2437",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5069,7 +5069,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5105,7 +5105,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": "1519",
"content-length": "1518",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5125,7 +5125,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5161,7 +5161,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": "1575",
"content-length": "1574",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5174,7 +5174,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": "5687",
"content-length": "5686",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5193,7 +5193,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5264,7 +5264,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": "1537",
"content-length": "1536",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5284,7 +5284,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5320,7 +5320,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": "690",
"content-length": "689",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5657,7 +5657,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5762,7 +5762,7 @@ exports[`Members API Setting subscribed when editing a member won't reset to def
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": "2509",
"content-length": "2508",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5782,7 +5782,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5887,7 +5887,7 @@ exports[`Members API Setting subscribed when editing a member won't reset to def
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": "2509",
"content-length": "2508",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -5906,7 +5906,7 @@ Object {
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": "url",
"type": null,
"url": null,
},
"avatar_image": null,
@ -5942,7 +5942,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": "2425",
"content-length": "2424",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -132,7 +132,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": "3204",
"content-length": "3202",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -367,7 +367,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": "3204",
"content-length": "3202",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -381,35 +381,35 @@ Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
Object {
"data": Any<Object>,
"type": Any<String>,
"type": "subscription_event",
},
],
"meta": Object {
@ -429,7 +429,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": "7962",
"content-length": "7960",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -1965,7 +1965,7 @@ describe('Members API', function () {
await testWithAttribution(attribution, {
id: null,
url: null,
type: 'url',
type: null,
title: null,
referrer_source: null,
referrer_medium: null,
@ -1979,7 +1979,7 @@ describe('Members API', function () {
await testWithAttribution(attribution, {
id: null,
url: null,
type: 'url',
type: null,
title: null,
referrer_source: null,
referrer_medium: null,
@ -2000,7 +2000,6 @@ describe('Members API', function () {
})
.matchBodySnapshot({
events: new Array(subscriptionAttributions.length).fill({
type: anyString,
data: anyObject
})
})

View File

@ -2,7 +2,7 @@
* @typedef {object} AttributionResource
* @prop {string|null} id
* @prop {string|null} url (absolute URL)
* @prop {'page'|'post'|'author'|'tag'|'url'} type
* @prop {'page'|'post'|'author'|'tag'|'url'|null} type
* @prop {string|null} title
* @prop {string|null} referrerSource
* @prop {string|null} referrerMedium
@ -17,7 +17,7 @@ class Attribution {
* @param {object} data
* @param {string|null} [data.id]
* @param {string|null} [data.url] Relative to subdirectory
* @param {'page'|'post'|'author'|'tag'|'url'} [data.type]
* @param {'page'|'post'|'author'|'tag'|'url'|null} [data.type]
* @param {string|null} [data.referrerSource]
* @param {string|null} [data.referrerMedium]
* @param {string|null} [data.referrerUrl]
@ -49,6 +49,17 @@ class Attribution {
*/
getResource(model) {
if (!this.id || this.type === 'url' || !this.type || !model) {
if (!this.url) {
return {
id: null,
type: null,
url: null,
title: null,
referrerSource: this.referrerSource,
referrerMedium: this.referrerMedium,
referrerUrl: this.referrerUrl
};
}
return {
id: null,
type: 'url',

View File

@ -45,14 +45,15 @@ class UrlHistory {
/**
* @private
* @param {any[]} history
* @returns {boolean}
* @returns {history is UrlHistoryArray}
*/
static isValidHistory(history) {
for (const item of history) {
const isValidIdEntry = typeof item?.id === 'string' && typeof item?.type === 'string' && ALLOWED_TYPES.includes(item.type);
const isValidPathEntry = typeof item?.path === 'string';
const isValidReferrerSource = typeof item?.referrerSource === 'string';
const isValidEntry = isValidPathEntry || isValidIdEntry;
const isValidEntry = isValidPathEntry || isValidIdEntry || isValidReferrerSource;
if (!isValidEntry || !Number.isSafeInteger(item?.time)) {
return false;

View File

@ -158,6 +158,21 @@ describe('AttributionBuilder', function () {
});
});
it('Returns all null if only invalid ids', async function () {
const history = UrlHistory.create([
{id: 'invalid', type: 'post', time: now + 124},
{id: 'invalid', type: 'post', time: now + 124}
]);
should(await attributionBuilder.getAttribution(history)).match({
type: null,
id: null,
url: null,
referrerSource: 'Ghost Explore',
referrerMedium: 'Ghost Network',
referrerUrl: 'https://ghost.org/explore'
});
});
it('Returns null referrer attribution', async function () {
attributionBuilder = new AttributionBuilder({
urlTranslator,
@ -179,18 +194,6 @@ describe('AttributionBuilder', function () {
});
});
it('Returns all null if only invalid ids', async function () {
const history = UrlHistory.create([
{id: 'invalid', type: 'post', time: now + 124},
{id: 'invalid', type: 'post', time: now + 124}
]);
should(await attributionBuilder.getAttribution(history)).match({
type: null,
id: null,
url: null
});
});
it('Returns all null for invalid histories', async function () {
const history = UrlHistory.create('invalid');
should(await attributionBuilder.getAttribution(history)).match({

View File

@ -85,6 +85,12 @@ describe('UrlHistory', function () {
referrerSource: 'ghost-explore',
referrerMedium: null,
referrerUrl: 'https://ghost.org'
}],
[{
time: Date.now(),
referrerSource: 'ghost-explore',
referrerMedium: null,
referrerUrl: 'https://ghost.org'
}]
];
for (const input of inputs) {

View File

@ -78,6 +78,10 @@ describe('UrlTranslator', function () {
});
});
it('skips items without path and type', async function () {
should(await translator.getResourceDetails({time: 123})).eql(null);
});
it('returns posts for explicit items', async function () {
should(await translator.getResourceDetails({id: 'my-post', type: 'post', time: 123})).eql({
type: 'post',

View File

@ -1,3 +1,5 @@
import {getUrlHistory} from './helpers';
export const setupGhostApi = ({siteUrl}: {siteUrl: string}) => {
const apiPath = 'members/api';
@ -16,7 +18,8 @@ export const setupGhostApi = ({siteUrl}: {siteUrl: string}) => {
const payload = JSON.stringify({
email,
emailType: 'signup',
labels
labels,
urlHistory: getUrlHistory()
});
const response = await fetch(url, {

View File

@ -1,5 +1,31 @@
import {SignupFormOptions} from '../AppContext';
export type URLHistory = {
type?: 'post',
path?: string,
time: number,
referrerSource: string | null,
referrerMedium: string | null,
referrerUrl: string | null,
}[];
export function isMinimal(options: SignupFormOptions): boolean {
return !options.title;
}
export function getUrlHistory(): URLHistory {
const history: URLHistory = [];
// Href without query string
const currentPath = window.location.protocol + '//' + window.location.host + window.location.pathname;
const currentTime = new Date().getTime();
history.push({
time: currentTime,
referrerSource: window.location.host,
referrerMedium: 'Embed',
referrerUrl: currentPath
});
return history;
}

View File

@ -0,0 +1,61 @@
import {expect} from '@playwright/test';
import {initialize} from '../utils/e2e';
import {test} from '@playwright/test';
async function testHistory({page, path, referrer, urlHistory}: {page: any, path: string, urlHistory: any[]}) {
const {frame, lastApiRequest} = await initialize({page, title: 'Sign up', path});
// Fill out the form
const emailInput = frame.getByTestId('input');
await emailInput.fill('jamie@example.com');
// Click the submit button
const submitButton = frame.getByTestId('button');
await submitButton.click();
// Check input and button are gone
await expect(frame.getByTestId('input')).toHaveCount(0);
await expect(frame.getByTestId('button')).toHaveCount(0);
// Showing the success page
await expect(frame.getByTestId('success-page')).toHaveCount(1);
// Check email address text is visible on the page
await expect(frame.getByText('jamie@example.com')).toBeVisible();
// Check the request body
expect(lastApiRequest.body).not.toBeNull();
expect(lastApiRequest.body).toHaveProperty('email', 'jamie@example.com');
expect(lastApiRequest.body).toHaveProperty('urlHistory', urlHistory);
}
test.describe('Attribution', async () => {
test('Sends the current path', async ({page}) => {
await testHistory({page,
path: '/my-custom-path/123',
urlHistory: [
{
referrerMedium: 'Embed',
referrerSource: 'localhost:1234',
referrerUrl: 'https://localhost:1234/my-custom-path/123',
time: expect.any(Number)
}
]}
);
});
test('removes query string', async ({page}) => {
await testHistory({page,
path: '/my-custom-path/123?ref=ghost',
urlHistory: [
{
referrerMedium: 'Embed',
referrerSource: 'localhost:1234',
referrerUrl: 'https://localhost:1234/my-custom-path/123',
time: expect.any(Number)
}
]}
);
});
});

View File

@ -190,7 +190,7 @@ test.describe('Form', async () => {
});
test('Shows error message on network issues', async ({page}) => {
const {frame} = await initialize({page, title: 'Sign up', site: '127.0.0.1:9999'});
const {frame} = await initialize({page, title: 'Sign up', site: 'http://localhost:1234/invalid'});
// Fill out the form
const emailInput = frame.getByTestId('input');

View File

@ -1,16 +1,25 @@
import {E2E_PORT} from '../../playwright.config';
import {Page} from '@playwright/test';
const MOCKED_SITE_URL = 'http://localhost:1234';
const MOCKED_SITE_URL = 'https://localhost:1234';
type LastApiRequest = {
body: null | any
};
export async function initialize({page, ...options}: {page: any; title?: string, description?: string, logo?: string, backgroundColor?: string, buttonColor?: string, site?: string, 'label-1'?: string, 'label-2'?: string}) {
const url = `http://localhost:${E2E_PORT}/signup-form.min.js`;
export async function initialize({page, path, ...options}: {page: Page, path?: string; title?: string, description?: string, logo?: string, backgroundColor?: string, buttonColor?: string, site?: string, 'label-1'?: string, 'label-2'?: string}) {
const sitePath = `${MOCKED_SITE_URL}${path ?? ''}`;
await page.route(sitePath, async (route) => {
await route.fulfill({
status: 200,
body: '<html><body></body></html>'
});
});
await page.goto('about:blank');
const url = `http://localhost:${E2E_PORT}/signup-form.min.js`;
await page.setViewportSize({width: 1000, height: 1000});
await page.goto(sitePath);
const lastApiRequest = await mockApi({page});
if (!options.site) {
@ -50,5 +59,9 @@ export async function mockApi({page}: {page: any}) {
});
});
await page.route(`${MOCKED_SITE_URL}/invalid/members/api/send-magic-link/`, async (route) => {
await route.abort('addressunreachable');
});
return lastApiRequest;
}