From 7e27d3f3e8be4fcac1510da6aca43f947fd9f3c9 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Thu, 1 Jun 2023 10:18:11 +0200 Subject: [PATCH] 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. --- .../admin/__snapshots__/members.test.js.snap | 126 +++++++++--------- .../__snapshots__/webhooks.test.js.snap | 22 +-- .../test/e2e-api/members/webhooks.test.js | 5 +- .../lib/AttributionBuilder.js | 15 ++- ghost/member-attribution/lib/UrlHistory.js | 5 +- .../test/attribution.test.js | 27 ++-- ghost/member-attribution/test/history.test.js | 6 + .../test/url-translator.test.js | 4 + ghost/signup-form/src/utils/api.tsx | 5 +- ghost/signup-form/src/utils/helpers.tsx | 26 ++++ .../signup-form/test/e2e/attribution.test.ts | 61 +++++++++ ghost/signup-form/test/e2e/form.test.ts | 2 +- ghost/signup-form/test/utils/e2e.ts | 21 ++- 13 files changed, 226 insertions(+), 99 deletions(-) create mode 100644 ghost/signup-form/test/e2e/attribution.test.ts 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 e799417f9d..b0bb883873 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 @@ -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 \\]\\|\\\\\\\\\\.\\)\\*"/, 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 5495846133..41c30d859f 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 @@ -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, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "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 \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/members/webhooks.test.js b/ghost/core/test/e2e-api/members/webhooks.test.js index 1752e8cefc..7087508337 100644 --- a/ghost/core/test/e2e-api/members/webhooks.test.js +++ b/ghost/core/test/e2e-api/members/webhooks.test.js @@ -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 }) }) diff --git a/ghost/member-attribution/lib/AttributionBuilder.js b/ghost/member-attribution/lib/AttributionBuilder.js index dd32cf0e57..77404f3725 100644 --- a/ghost/member-attribution/lib/AttributionBuilder.js +++ b/ghost/member-attribution/lib/AttributionBuilder.js @@ -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', diff --git a/ghost/member-attribution/lib/UrlHistory.js b/ghost/member-attribution/lib/UrlHistory.js index 2ebf99b233..5b01c92b96 100644 --- a/ghost/member-attribution/lib/UrlHistory.js +++ b/ghost/member-attribution/lib/UrlHistory.js @@ -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; diff --git a/ghost/member-attribution/test/attribution.test.js b/ghost/member-attribution/test/attribution.test.js index 0cf21de986..3f0e4ae642 100644 --- a/ghost/member-attribution/test/attribution.test.js +++ b/ghost/member-attribution/test/attribution.test.js @@ -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({ diff --git a/ghost/member-attribution/test/history.test.js b/ghost/member-attribution/test/history.test.js index c80752e189..cc04b77beb 100644 --- a/ghost/member-attribution/test/history.test.js +++ b/ghost/member-attribution/test/history.test.js @@ -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) { diff --git a/ghost/member-attribution/test/url-translator.test.js b/ghost/member-attribution/test/url-translator.test.js index 5430792a33..e182c3ab29 100644 --- a/ghost/member-attribution/test/url-translator.test.js +++ b/ghost/member-attribution/test/url-translator.test.js @@ -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', diff --git a/ghost/signup-form/src/utils/api.tsx b/ghost/signup-form/src/utils/api.tsx index ebf0d05f78..771dce5eef 100644 --- a/ghost/signup-form/src/utils/api.tsx +++ b/ghost/signup-form/src/utils/api.tsx @@ -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, { diff --git a/ghost/signup-form/src/utils/helpers.tsx b/ghost/signup-form/src/utils/helpers.tsx index d8c4decde6..a5863a6b35 100644 --- a/ghost/signup-form/src/utils/helpers.tsx +++ b/ghost/signup-form/src/utils/helpers.tsx @@ -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; +} diff --git a/ghost/signup-form/test/e2e/attribution.test.ts b/ghost/signup-form/test/e2e/attribution.test.ts new file mode 100644 index 0000000000..1443b4c21a --- /dev/null +++ b/ghost/signup-form/test/e2e/attribution.test.ts @@ -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) + } + ]} + ); + }); +}); + diff --git a/ghost/signup-form/test/e2e/form.test.ts b/ghost/signup-form/test/e2e/form.test.ts index ddb4215833..5584f6b29a 100644 --- a/ghost/signup-form/test/e2e/form.test.ts +++ b/ghost/signup-form/test/e2e/form.test.ts @@ -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'); diff --git a/ghost/signup-form/test/utils/e2e.ts b/ghost/signup-form/test/utils/e2e.ts index 2f06866754..ceccdff05e 100644 --- a/ghost/signup-form/test/utils/e2e.ts +++ b/ghost/signup-form/test/utils/e2e.ts @@ -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: '' + }); + }); - 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; }