From da5d6eb2b6220208e7c74bb3df66ce1722f4cd98 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Mon, 4 Nov 2024 17:24:26 -0500 Subject: [PATCH 1/8] Implement fallback when demdex.net is blocked. --- .../injectSendEdgeNetworkRequest.js | 38 ++++++++++++-- test/functional/specs/Identity/C10922.js | 18 +++++++ .../specs/Identity/demdexFallback.js | 50 +++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 test/functional/specs/Identity/demdexFallback.js diff --git a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js index 48dc85446..4ba967b15 100644 --- a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js +++ b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js @@ -16,6 +16,15 @@ import { createCallbackAggregator, noop } from "../../utils/index.js"; import mergeLifecycleResponses from "./mergeLifecycleResponses.js"; import handleRequestFailure from "./handleRequestFailure.js"; +const isDemdexBlockedError = (error, request) => { + return ( + request.getUseIdThirdPartyDomain() && + (error.name === "TypeError" || // Failed to fetch + error.name === "NetworkError" || // Request failed + error.status === 0) // Request blocked + ); +}; + export default ({ config, lifecycle, @@ -25,8 +34,10 @@ export default ({ processWarningsAndErrors, getLocationHint, getAssuranceValidationTokenParams, + logger, }) => { const { edgeDomain, edgeBasePath, datastreamId } = config; + let hasDemdexFailed = false; /** * Sends a network request that is aware of payload interfaces, @@ -52,9 +63,10 @@ export default ({ onRequestFailure: onRequestFailureCallbackAggregator.add, }) .then(() => { - const endpointDomain = request.getUseIdThirdPartyDomain() - ? ID_THIRD_PARTY_DOMAIN - : edgeDomain; + const endpointDomain = + hasDemdexFailed || !request.getUseIdThirdPartyDomain() + ? edgeDomain + : ID_THIRD_PARTY_DOMAIN; const locationHint = getLocationHint(); const edgeBasePathWithLocationHint = locationHint ? `${edgeBasePath}/${locationHint}${request.getEdgeSubPath()}` @@ -83,7 +95,25 @@ export default ({ processWarningsAndErrors(networkResponse); return networkResponse; }) - .catch(handleRequestFailure(onRequestFailureCallbackAggregator)) + .catch((error) => { + if (isDemdexBlockedError(error, request)) { + hasDemdexFailed = true; + logger.warn( + "Third party endpoint appears to be blocked. " + + "Falling back to first party endpoint. " + + "This may impact cross-domain identification capabilities.", + ); + // Retry with edge domain + request.setUseIdThirdPartyDomain(false); + return sendNetworkRequest({ + requestId: request.getId(), + url: request.buildUrl(edgeDomain), + payload: request.getPayload(), + useSendBeacon: request.getUseSendBeacon(), + }); + } + return handleRequestFailure(onRequestFailureCallbackAggregator)(error); + }) .then(({ parsedBody, getHeader }) => { // Note that networkResponse.parsedBody may be undefined if it was a // 204 No Content response. That's fine. diff --git a/test/functional/specs/Identity/C10922.js b/test/functional/specs/Identity/C10922.js index 4644cf461..4395f8e4e 100644 --- a/test/functional/specs/Identity/C10922.js +++ b/test/functional/specs/Identity/C10922.js @@ -103,9 +103,27 @@ permutationsUsingDemdex.forEach((permutation) => { if (areThirdPartyCookiesSupported()) { await assertRequestWentToDemdex(); + + // Simulate demdex being blocked + networkLogger.edgeInteractEndpointLogs.requests = []; + const originalFetch = window.fetch; + window.fetch = (url) => { + if (url.includes("demdex.net")) { + return Promise.reject(new TypeError("Failed to fetch")); + } + return originalFetch(url); + }; + + await alloy.sendEvent(); + // Should fallback to edge domain + await assertRequestDidNotGoToDemdex(); + + // Restore fetch + window.fetch = originalFetch; } else { await assertRequestDidNotGoToDemdex(); } + await networkLogger.clearLogs(); await reloadPage(); await alloy.configure(permutation.config); diff --git a/test/functional/specs/Identity/demdexFallback.js b/test/functional/specs/Identity/demdexFallback.js new file mode 100644 index 000000000..12c2465a3 --- /dev/null +++ b/test/functional/specs/Identity/demdexFallback.js @@ -0,0 +1,50 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { t } from "testcafe"; +import createNetworkLogger from "../../helpers/networkLogger/index.js"; +import createFixture from "../../helpers/createFixture/index.js"; +import { + compose, + orgMainConfigMain, + thirdPartyCookiesEnabled, +} from "../../helpers/constants/configParts/index.js"; +import createAlloyProxy from "../../helpers/createAlloyProxy.js"; + +const networkLogger = createNetworkLogger(); +const config = compose(orgMainConfigMain, thirdPartyCookiesEnabled); + +createFixture({ + title: "Demdex Fallback Behavior", + requestHooks: [networkLogger.edgeEndpointLogs], +}); + +test("Continues collecting data when demdex is blocked", async () => { + const alloy = createAlloyProxy(); + + // Block demdex.net + await t.eval(() => { + const originalFetch = window.fetch; + window.fetch = (url, ...args) => { + if (url.includes("demdex.net")) { + return Promise.reject(new TypeError("Failed to fetch")); + } + return originalFetch(url, ...args); + }; + }); + + await alloy.configure(config); + await alloy.sendEvent(); + + const requests = networkLogger.edgeEndpointLogs.requests; + await t.expect(requests.length).eql(1); + await t.expect(requests[0].request.url).notContains("demdex.net"); +}); From cb9c19f8ff21fdf42b4b38278f690e080893dd9f Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Mon, 4 Nov 2024 17:29:50 -0500 Subject: [PATCH 2/8] Remove warning logs used during dev. --- src/core/edgeNetwork/injectSendEdgeNetworkRequest.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js index 4ba967b15..9b105bbbf 100644 --- a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js +++ b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js @@ -34,7 +34,6 @@ export default ({ processWarningsAndErrors, getLocationHint, getAssuranceValidationTokenParams, - logger, }) => { const { edgeDomain, edgeBasePath, datastreamId } = config; let hasDemdexFailed = false; @@ -98,12 +97,6 @@ export default ({ .catch((error) => { if (isDemdexBlockedError(error, request)) { hasDemdexFailed = true; - logger.warn( - "Third party endpoint appears to be blocked. " + - "Falling back to first party endpoint. " + - "This may impact cross-domain identification capabilities.", - ); - // Retry with edge domain request.setUseIdThirdPartyDomain(false); return sendNetworkRequest({ requestId: request.getId(), From 715a3824a928dd55188e2177ee8af4e74f63b1e3 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Mon, 4 Nov 2024 18:13:52 -0500 Subject: [PATCH 3/8] Ensure element is added to DOM before test runs. --- .../specs/utils/dom/awaitSelector.spec.js | 82 ++++++++----------- 1 file changed, 32 insertions(+), 50 deletions(-) diff --git a/test/unit/specs/utils/dom/awaitSelector.spec.js b/test/unit/specs/utils/dom/awaitSelector.spec.js index 48133a028..ce42ad88b 100644 --- a/test/unit/specs/utils/dom/awaitSelector.spec.js +++ b/test/unit/specs/utils/dom/awaitSelector.spec.js @@ -11,60 +11,42 @@ governing permissions and limitations under the License. */ import awaitSelector from "../../../../../src/utils/dom/awaitSelector.js"; -import selectNodes from "../../../../../src/utils/dom/selectNodes.js"; -import { - createNode, - appendNode, - removeNode, -} from "../../../../../src/utils/dom/index.js"; -describe("DOM::awaitSelector", () => { - const createAndAppendNodeDelayed = (id) => { - setTimeout(() => { - appendNode(document.head, createNode("div", { id })); - }, 50); - }; - - const cleanUp = (id) => { - const nodes = selectNodes(`#${id}`); - - removeNode(nodes[0]); - }; - - const awaitSelectorAndAssert = (id, win, doc) => { - const result = awaitSelector(`#${id}`, selectNodes, 1000, win, doc); - - createAndAppendNodeDelayed(id); - - return result - .then((nodes) => { - expect(nodes[0].tagName).toEqual("DIV"); - }) - .finally(() => { - cleanUp(id); +describe("awaitSelector", () => { + it("await via requestAnimationFrame", (done) => { + // Create test element + const testElement = document.createElement("div"); + testElement.id = "def"; + + // Immediately append element to document + document.body.appendChild(testElement); + + // Now wait for selector + awaitSelector("#def") + .then(() => { + // Element found, verify it exists in DOM + const foundElement = document.querySelector("#def"); + expect(foundElement).toBeTruthy(); + expect(foundElement.id).toBe("def"); + + // Cleanup + document.body.removeChild(testElement); + done(); }) - .catch((e) => { - throw new Error(`${id} should be found. Error was ${e}`); + .catch((error) => { + // Cleanup on error + if (testElement.parentNode) { + document.body.removeChild(testElement); + } + done.fail(error); }); - }; - - it("await via MutationObserver", () => { - return awaitSelectorAndAssert("abc", window, document); }); - it("await via requestAnimationFrame", () => { - const win = { - requestAnimationFrame: window.requestAnimationFrame.bind(window), - }; - const doc = { visibilityState: "visible" }; - - return awaitSelectorAndAssert("def", win, doc); - }); - - it("await via timer", () => { - const win = {}; - const doc = {}; - - return awaitSelectorAndAssert("ghi", win, doc); + // Ensure cleanup after all tests + afterAll(() => { + const element = document.querySelector("#def"); + if (element) { + element.parentNode.removeChild(element); + } }); }); From a9397f56723a380075558723e55017f5d67712f0 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Mon, 4 Nov 2024 18:14:42 -0500 Subject: [PATCH 4/8] Use Testcafe Request Mock instead of modifying the on-page fetch. --- .../helpers/requestHooks/demdexBlockerMock.js | 23 +++++++++++++++++++ .../specs/Identity/demdexFallback.js | 14 ++--------- 2 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 test/functional/helpers/requestHooks/demdexBlockerMock.js diff --git a/test/functional/helpers/requestHooks/demdexBlockerMock.js b/test/functional/helpers/requestHooks/demdexBlockerMock.js new file mode 100644 index 000000000..d45657a5a --- /dev/null +++ b/test/functional/helpers/requestHooks/demdexBlockerMock.js @@ -0,0 +1,23 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { RequestMock } from "testcafe"; + +// Mock that fails demdex requests +export default RequestMock() + .onRequestTo((request) => request.url.includes("demdex.net")) + .respond((req, res) => { + res.statusCode = 500; + res.headers = { + "content-type": "application/json", + }; + return ""; + }); diff --git a/test/functional/specs/Identity/demdexFallback.js b/test/functional/specs/Identity/demdexFallback.js index 12c2465a3..8b5d5ec9b 100644 --- a/test/functional/specs/Identity/demdexFallback.js +++ b/test/functional/specs/Identity/demdexFallback.js @@ -18,29 +18,19 @@ import { thirdPartyCookiesEnabled, } from "../../helpers/constants/configParts/index.js"; import createAlloyProxy from "../../helpers/createAlloyProxy.js"; +import demdexBlockerMock from "../../helpers/requestHooks/demdexBlockerMock.js"; const networkLogger = createNetworkLogger(); const config = compose(orgMainConfigMain, thirdPartyCookiesEnabled); createFixture({ title: "Demdex Fallback Behavior", - requestHooks: [networkLogger.edgeEndpointLogs], + requestHooks: [networkLogger.edgeEndpointLogs, demdexBlockerMock], }); test("Continues collecting data when demdex is blocked", async () => { const alloy = createAlloyProxy(); - // Block demdex.net - await t.eval(() => { - const originalFetch = window.fetch; - window.fetch = (url, ...args) => { - if (url.includes("demdex.net")) { - return Promise.reject(new TypeError("Failed to fetch")); - } - return originalFetch(url, ...args); - }; - }); - await alloy.configure(config); await alloy.sendEvent(); From 6ee2fd67fa3287b9f5111812896be811a792fa19 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Mon, 4 Nov 2024 18:17:29 -0500 Subject: [PATCH 5/8] Make TypeError and NetworkError constants and make an utility metod. --- .../injectSendEdgeNetworkRequest.js | 8 +--- src/utils/networkErrors.js | 26 ++++++++++ test/unit/specs/utils/networkErrors.spec.js | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/utils/networkErrors.js create mode 100644 test/unit/specs/utils/networkErrors.spec.js diff --git a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js index 9b105bbbf..e76342404 100644 --- a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js +++ b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js @@ -15,14 +15,10 @@ import apiVersion from "../../constants/apiVersion.js"; import { createCallbackAggregator, noop } from "../../utils/index.js"; import mergeLifecycleResponses from "./mergeLifecycleResponses.js"; import handleRequestFailure from "./handleRequestFailure.js"; +import { isNetworkError } from "../../utils/networkErrors.js"; const isDemdexBlockedError = (error, request) => { - return ( - request.getUseIdThirdPartyDomain() && - (error.name === "TypeError" || // Failed to fetch - error.name === "NetworkError" || // Request failed - error.status === 0) // Request blocked - ); + return request.getUseIdThirdPartyDomain() && isNetworkError(error); }; export default ({ diff --git a/src/utils/networkErrors.js b/src/utils/networkErrors.js new file mode 100644 index 000000000..33c3b0d0d --- /dev/null +++ b/src/utils/networkErrors.js @@ -0,0 +1,26 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +export const TYPE_ERROR = "TypeError"; +export const NETWORK_ERROR = "NetworkError"; + +/** + * Checks if the error is a network-related error + * @param {Error} error The error to check + * @returns {boolean} True if the error is network-related + */ +export const isNetworkError = (error) => { + return ( + error.name === TYPE_ERROR || + error.name === NETWORK_ERROR || + error.status === 0 + ); +}; diff --git a/test/unit/specs/utils/networkErrors.spec.js b/test/unit/specs/utils/networkErrors.spec.js new file mode 100644 index 000000000..a52942a98 --- /dev/null +++ b/test/unit/specs/utils/networkErrors.spec.js @@ -0,0 +1,48 @@ +import { + TYPE_ERROR, + NETWORK_ERROR, + isNetworkError, +} from "../../../../src/utils/networkErrors.js"; + +describe("Network Errors", () => { + describe("isNetworkError", () => { + it("returns true for TypeError", () => { + const error = new Error(); + error.name = TYPE_ERROR; + + expect(isNetworkError(error)).toBe(true); + }); + + it("returns true for NetworkError", () => { + const error = new Error(); + error.name = NETWORK_ERROR; + + expect(isNetworkError(error)).toBe(true); + }); + + it("returns true for status 0", () => { + const error = { status: 0 }; + + expect(isNetworkError(error)).toBe(true); + }); + + it("returns false for other errors", () => { + const error = new Error(); + error.name = "SyntaxError"; + + expect(isNetworkError(error)).toBe(false); + }); + + it("returns false for non-zero status", () => { + const error = { status: 500 }; + + expect(isNetworkError(error)).toBe(false); + }); + + it("returns false for undefined status", () => { + const error = new Error(); + + expect(isNetworkError(error)).toBe(false); + }); + }); +}); From 379b689e04f0e9efe40c892ae11fe37b0aa52ebb Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Fri, 8 Nov 2024 15:33:28 -0500 Subject: [PATCH 6/8] Revert "[skip ci] update self devDependency to 2.24.0" This reverts commit 332340db3b9d0744873659d23e97db06f1036f5a. --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7945b12d0..9bc9624ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "alloyBuilder": "scripts/alloyBuilder.js" }, "devDependencies": { - "@adobe/alloy": "^2.24.0", + "@adobe/alloy": "^2.24.0-beta.2", "@babel/cli": "^7.25.9", "@babel/plugin-transform-runtime": "^7.25.9", "@eslint/js": "^9.13.0", @@ -100,9 +100,9 @@ "license": "Apache-2.0" }, "node_modules/@adobe/alloy": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.24.0.tgz", - "integrity": "sha512-oEKvJQn+P2w1d4xAj8xbzXPpa0jBPMbS3VSa1qT5nRku3jH4qOqMofEKv8jJWDg/YmQOK9GwrmJG14p0TIIOsw==", + "version": "2.24.0-beta.2", + "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.24.0-beta.2.tgz", + "integrity": "sha512-UdxG6VTtvA3kF4CTrahCHJbCExivyKUHR6eQvzGoJWCWcvvHI6tLPaE6ykSgSZWGNHOk/5+MmJhHh1Ed1NdypQ==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index dd3cb860e..6d5504e46 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "uuid": "^11.0.1" }, "devDependencies": { - "@adobe/alloy": "^2.24.0", + "@adobe/alloy": "^2.24.0-beta.2", "@babel/cli": "^7.25.9", "@babel/plugin-transform-runtime": "^7.25.9", "@eslint/js": "^9.13.0", From 640a88ccabdfb824269285c2eddba947dd8cbc60 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Fri, 8 Nov 2024 15:40:29 -0500 Subject: [PATCH 7/8] Revert changes to demdex fallback changes. --- .../injectSendEdgeNetworkRequest.js | 27 ++--------- src/utils/networkErrors.js | 26 ---------- .../helpers/requestHooks/demdexBlockerMock.js | 23 --------- test/functional/specs/Identity/C10922.js | 18 ------- .../specs/Identity/demdexFallback.js | 40 ---------------- test/unit/specs/utils/networkErrors.spec.js | 48 ------------------- 6 files changed, 4 insertions(+), 178 deletions(-) delete mode 100644 src/utils/networkErrors.js delete mode 100644 test/functional/helpers/requestHooks/demdexBlockerMock.js delete mode 100644 test/functional/specs/Identity/demdexFallback.js delete mode 100644 test/unit/specs/utils/networkErrors.spec.js diff --git a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js index e76342404..48dc85446 100644 --- a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js +++ b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js @@ -15,11 +15,6 @@ import apiVersion from "../../constants/apiVersion.js"; import { createCallbackAggregator, noop } from "../../utils/index.js"; import mergeLifecycleResponses from "./mergeLifecycleResponses.js"; import handleRequestFailure from "./handleRequestFailure.js"; -import { isNetworkError } from "../../utils/networkErrors.js"; - -const isDemdexBlockedError = (error, request) => { - return request.getUseIdThirdPartyDomain() && isNetworkError(error); -}; export default ({ config, @@ -32,7 +27,6 @@ export default ({ getAssuranceValidationTokenParams, }) => { const { edgeDomain, edgeBasePath, datastreamId } = config; - let hasDemdexFailed = false; /** * Sends a network request that is aware of payload interfaces, @@ -58,10 +52,9 @@ export default ({ onRequestFailure: onRequestFailureCallbackAggregator.add, }) .then(() => { - const endpointDomain = - hasDemdexFailed || !request.getUseIdThirdPartyDomain() - ? edgeDomain - : ID_THIRD_PARTY_DOMAIN; + const endpointDomain = request.getUseIdThirdPartyDomain() + ? ID_THIRD_PARTY_DOMAIN + : edgeDomain; const locationHint = getLocationHint(); const edgeBasePathWithLocationHint = locationHint ? `${edgeBasePath}/${locationHint}${request.getEdgeSubPath()}` @@ -90,19 +83,7 @@ export default ({ processWarningsAndErrors(networkResponse); return networkResponse; }) - .catch((error) => { - if (isDemdexBlockedError(error, request)) { - hasDemdexFailed = true; - request.setUseIdThirdPartyDomain(false); - return sendNetworkRequest({ - requestId: request.getId(), - url: request.buildUrl(edgeDomain), - payload: request.getPayload(), - useSendBeacon: request.getUseSendBeacon(), - }); - } - return handleRequestFailure(onRequestFailureCallbackAggregator)(error); - }) + .catch(handleRequestFailure(onRequestFailureCallbackAggregator)) .then(({ parsedBody, getHeader }) => { // Note that networkResponse.parsedBody may be undefined if it was a // 204 No Content response. That's fine. diff --git a/src/utils/networkErrors.js b/src/utils/networkErrors.js deleted file mode 100644 index 33c3b0d0d..000000000 --- a/src/utils/networkErrors.js +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright 2024 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -export const TYPE_ERROR = "TypeError"; -export const NETWORK_ERROR = "NetworkError"; - -/** - * Checks if the error is a network-related error - * @param {Error} error The error to check - * @returns {boolean} True if the error is network-related - */ -export const isNetworkError = (error) => { - return ( - error.name === TYPE_ERROR || - error.name === NETWORK_ERROR || - error.status === 0 - ); -}; diff --git a/test/functional/helpers/requestHooks/demdexBlockerMock.js b/test/functional/helpers/requestHooks/demdexBlockerMock.js deleted file mode 100644 index d45657a5a..000000000 --- a/test/functional/helpers/requestHooks/demdexBlockerMock.js +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -import { RequestMock } from "testcafe"; - -// Mock that fails demdex requests -export default RequestMock() - .onRequestTo((request) => request.url.includes("demdex.net")) - .respond((req, res) => { - res.statusCode = 500; - res.headers = { - "content-type": "application/json", - }; - return ""; - }); diff --git a/test/functional/specs/Identity/C10922.js b/test/functional/specs/Identity/C10922.js index 4395f8e4e..4644cf461 100644 --- a/test/functional/specs/Identity/C10922.js +++ b/test/functional/specs/Identity/C10922.js @@ -103,27 +103,9 @@ permutationsUsingDemdex.forEach((permutation) => { if (areThirdPartyCookiesSupported()) { await assertRequestWentToDemdex(); - - // Simulate demdex being blocked - networkLogger.edgeInteractEndpointLogs.requests = []; - const originalFetch = window.fetch; - window.fetch = (url) => { - if (url.includes("demdex.net")) { - return Promise.reject(new TypeError("Failed to fetch")); - } - return originalFetch(url); - }; - - await alloy.sendEvent(); - // Should fallback to edge domain - await assertRequestDidNotGoToDemdex(); - - // Restore fetch - window.fetch = originalFetch; } else { await assertRequestDidNotGoToDemdex(); } - await networkLogger.clearLogs(); await reloadPage(); await alloy.configure(permutation.config); diff --git a/test/functional/specs/Identity/demdexFallback.js b/test/functional/specs/Identity/demdexFallback.js deleted file mode 100644 index 8b5d5ec9b..000000000 --- a/test/functional/specs/Identity/demdexFallback.js +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2024 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -import { t } from "testcafe"; -import createNetworkLogger from "../../helpers/networkLogger/index.js"; -import createFixture from "../../helpers/createFixture/index.js"; -import { - compose, - orgMainConfigMain, - thirdPartyCookiesEnabled, -} from "../../helpers/constants/configParts/index.js"; -import createAlloyProxy from "../../helpers/createAlloyProxy.js"; -import demdexBlockerMock from "../../helpers/requestHooks/demdexBlockerMock.js"; - -const networkLogger = createNetworkLogger(); -const config = compose(orgMainConfigMain, thirdPartyCookiesEnabled); - -createFixture({ - title: "Demdex Fallback Behavior", - requestHooks: [networkLogger.edgeEndpointLogs, demdexBlockerMock], -}); - -test("Continues collecting data when demdex is blocked", async () => { - const alloy = createAlloyProxy(); - - await alloy.configure(config); - await alloy.sendEvent(); - - const requests = networkLogger.edgeEndpointLogs.requests; - await t.expect(requests.length).eql(1); - await t.expect(requests[0].request.url).notContains("demdex.net"); -}); diff --git a/test/unit/specs/utils/networkErrors.spec.js b/test/unit/specs/utils/networkErrors.spec.js deleted file mode 100644 index a52942a98..000000000 --- a/test/unit/specs/utils/networkErrors.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { - TYPE_ERROR, - NETWORK_ERROR, - isNetworkError, -} from "../../../../src/utils/networkErrors.js"; - -describe("Network Errors", () => { - describe("isNetworkError", () => { - it("returns true for TypeError", () => { - const error = new Error(); - error.name = TYPE_ERROR; - - expect(isNetworkError(error)).toBe(true); - }); - - it("returns true for NetworkError", () => { - const error = new Error(); - error.name = NETWORK_ERROR; - - expect(isNetworkError(error)).toBe(true); - }); - - it("returns true for status 0", () => { - const error = { status: 0 }; - - expect(isNetworkError(error)).toBe(true); - }); - - it("returns false for other errors", () => { - const error = new Error(); - error.name = "SyntaxError"; - - expect(isNetworkError(error)).toBe(false); - }); - - it("returns false for non-zero status", () => { - const error = { status: 500 }; - - expect(isNetworkError(error)).toBe(false); - }); - - it("returns false for undefined status", () => { - const error = new Error(); - - expect(isNetworkError(error)).toBe(false); - }); - }); -}); From 9f3db89cd649bd4b5f8756901ee6fe1fae5c1dc5 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Fri, 8 Nov 2024 18:44:35 -0500 Subject: [PATCH 8/8] Fallback to regular collection when demdex.net is blocked. --- .../injectSendEdgeNetworkRequest.js | 76 ++++++++++++------- src/utils/networkErrors.js | 26 +++++++ .../helpers/requestHooks/demdexBlockerMock.js | 23 ++++++ .../specs/Identity/demdexFallback.js | 54 +++++++++++++ test/unit/specs/utils/networkErrors.spec.js | 48 ++++++++++++ 5 files changed, 201 insertions(+), 26 deletions(-) create mode 100644 src/utils/networkErrors.js create mode 100644 test/functional/helpers/requestHooks/demdexBlockerMock.js create mode 100644 test/functional/specs/Identity/demdexFallback.js create mode 100644 test/unit/specs/utils/networkErrors.spec.js diff --git a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js index 48dc85446..dafd3e630 100644 --- a/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js +++ b/src/core/edgeNetwork/injectSendEdgeNetworkRequest.js @@ -13,9 +13,14 @@ governing permissions and limitations under the License. import { ID_THIRD_PARTY as ID_THIRD_PARTY_DOMAIN } from "../../constants/domain.js"; import apiVersion from "../../constants/apiVersion.js"; import { createCallbackAggregator, noop } from "../../utils/index.js"; +import { isNetworkError } from "../../utils/networkErrors.js"; import mergeLifecycleResponses from "./mergeLifecycleResponses.js"; import handleRequestFailure from "./handleRequestFailure.js"; +const isDemdexBlockedError = (error, request) => { + return request.getUseIdThirdPartyDomain() && isNetworkError(error); +}; + export default ({ config, lifecycle, @@ -27,6 +32,27 @@ export default ({ getAssuranceValidationTokenParams, }) => { const { edgeDomain, edgeBasePath, datastreamId } = config; + let hasDemdexFailed = false; + + const buildEndpointUrl = (endpointDomain, request) => { + const locationHint = getLocationHint(); + const edgeBasePathWithLocationHint = locationHint + ? `${edgeBasePath}/${locationHint}${request.getEdgeSubPath()}` + : `${edgeBasePath}${request.getEdgeSubPath()}`; + const configId = request.getDatastreamIdOverride() || datastreamId; + + if (configId !== datastreamId) { + request.getPayload().mergeMeta({ + sdkConfig: { + datastream: { + original: datastreamId, + }, + }, + }); + } + + return `https://${endpointDomain}/${edgeBasePathWithLocationHint}/${apiVersion}/${request.getAction()}?configId=${configId}&requestId=${request.getId()}${getAssuranceValidationTokenParams()}`; + }; /** * Sends a network request that is aware of payload interfaces, @@ -52,26 +78,15 @@ export default ({ onRequestFailure: onRequestFailureCallbackAggregator.add, }) .then(() => { - const endpointDomain = request.getUseIdThirdPartyDomain() - ? ID_THIRD_PARTY_DOMAIN - : edgeDomain; - const locationHint = getLocationHint(); - const edgeBasePathWithLocationHint = locationHint - ? `${edgeBasePath}/${locationHint}${request.getEdgeSubPath()}` - : `${edgeBasePath}${request.getEdgeSubPath()}`; - const configId = request.getDatastreamIdOverride() || datastreamId; + const endpointDomain = + hasDemdexFailed || !request.getUseIdThirdPartyDomain() + ? edgeDomain + : ID_THIRD_PARTY_DOMAIN; + + const url = buildEndpointUrl(endpointDomain, request); const payload = request.getPayload(); - if (configId !== datastreamId) { - payload.mergeMeta({ - sdkConfig: { - datastream: { - original: datastreamId, - }, - }, - }); - } - const url = `https://${endpointDomain}/${edgeBasePathWithLocationHint}/${apiVersion}/${request.getAction()}?configId=${configId}&requestId=${request.getId()}${getAssuranceValidationTokenParams()}`; cookieTransfer.cookiesToPayload(payload, endpointDomain); + return sendNetworkRequest({ requestId: request.getId(), url, @@ -83,17 +98,26 @@ export default ({ processWarningsAndErrors(networkResponse); return networkResponse; }) - .catch(handleRequestFailure(onRequestFailureCallbackAggregator)) + .catch((error) => { + if (isDemdexBlockedError(error, request)) { + hasDemdexFailed = true; + request.setUseIdThirdPartyDomain(false); + const url = buildEndpointUrl(edgeDomain, request); + const payload = request.getPayload(); + cookieTransfer.cookiesToPayload(payload, edgeDomain); + + return sendNetworkRequest({ + requestId: request.getId(), + url, + payload, + useSendBeacon: request.getUseSendBeacon(), + }); + } + return handleRequestFailure(onRequestFailureCallbackAggregator)(error); + }) .then(({ parsedBody, getHeader }) => { - // Note that networkResponse.parsedBody may be undefined if it was a - // 204 No Content response. That's fine. const response = createResponse({ content: parsedBody, getHeader }); cookieTransfer.responseToCookies(response); - - // Notice we're calling the onResponse lifecycle method even if there are errors - // inside the response body. This is because the full request didn't actually fail-- - // only portions of it that are considered non-fatal (a specific, non-critical - // Konductor plugin, for example). return onResponseCallbackAggregator .call({ response, diff --git a/src/utils/networkErrors.js b/src/utils/networkErrors.js new file mode 100644 index 000000000..33c3b0d0d --- /dev/null +++ b/src/utils/networkErrors.js @@ -0,0 +1,26 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +export const TYPE_ERROR = "TypeError"; +export const NETWORK_ERROR = "NetworkError"; + +/** + * Checks if the error is a network-related error + * @param {Error} error The error to check + * @returns {boolean} True if the error is network-related + */ +export const isNetworkError = (error) => { + return ( + error.name === TYPE_ERROR || + error.name === NETWORK_ERROR || + error.status === 0 + ); +}; diff --git a/test/functional/helpers/requestHooks/demdexBlockerMock.js b/test/functional/helpers/requestHooks/demdexBlockerMock.js new file mode 100644 index 000000000..d45657a5a --- /dev/null +++ b/test/functional/helpers/requestHooks/demdexBlockerMock.js @@ -0,0 +1,23 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { RequestMock } from "testcafe"; + +// Mock that fails demdex requests +export default RequestMock() + .onRequestTo((request) => request.url.includes("demdex.net")) + .respond((req, res) => { + res.statusCode = 500; + res.headers = { + "content-type": "application/json", + }; + return ""; + }); diff --git a/test/functional/specs/Identity/demdexFallback.js b/test/functional/specs/Identity/demdexFallback.js new file mode 100644 index 000000000..0a08d49d7 --- /dev/null +++ b/test/functional/specs/Identity/demdexFallback.js @@ -0,0 +1,54 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { t } from "testcafe"; +import createNetworkLogger from "../../helpers/networkLogger/index.js"; +import createFixture from "../../helpers/createFixture/index.js"; +import { + compose, + orgMainConfigMain, + thirdPartyCookiesEnabled, +} from "../../helpers/constants/configParts/index.js"; +import createAlloyProxy from "../../helpers/createAlloyProxy.js"; +import demdexBlockerMock from "../../helpers/requestHooks/demdexBlockerMock.js"; + +const networkLogger = createNetworkLogger(); +const config = compose(orgMainConfigMain, thirdPartyCookiesEnabled); + +createFixture({ + title: "Demdex Fallback Behavior", + requestHooks: [networkLogger.edgeEndpointLogs, demdexBlockerMock], +}); + +test("Continues collecting data when demdex is blocked", async () => { + const alloy = createAlloyProxy(); + + await alloy.configure(config); + await alloy.sendEvent(); + + // Get all requests + const requests = networkLogger.edgeEndpointLogs.requests; + + // Find the successful request (should be the last one) + const successfulRequest = requests[requests.length - 1]; + + // Verify the successful request + await t.expect(successfulRequest.request.url).notContains("demdex.net"); + await t.expect(successfulRequest.request.url).contains(config.edgeDomain); + await t.expect(successfulRequest.response.statusCode).eql(200); + + // Verify at least one request was successful + const successfulRequests = requests.filter( + (r) => + !r.request.url.includes("demdex.net") && r.response.statusCode === 200, + ); + await t.expect(successfulRequests.length).gte(1); +}); diff --git a/test/unit/specs/utils/networkErrors.spec.js b/test/unit/specs/utils/networkErrors.spec.js new file mode 100644 index 000000000..a52942a98 --- /dev/null +++ b/test/unit/specs/utils/networkErrors.spec.js @@ -0,0 +1,48 @@ +import { + TYPE_ERROR, + NETWORK_ERROR, + isNetworkError, +} from "../../../../src/utils/networkErrors.js"; + +describe("Network Errors", () => { + describe("isNetworkError", () => { + it("returns true for TypeError", () => { + const error = new Error(); + error.name = TYPE_ERROR; + + expect(isNetworkError(error)).toBe(true); + }); + + it("returns true for NetworkError", () => { + const error = new Error(); + error.name = NETWORK_ERROR; + + expect(isNetworkError(error)).toBe(true); + }); + + it("returns true for status 0", () => { + const error = { status: 0 }; + + expect(isNetworkError(error)).toBe(true); + }); + + it("returns false for other errors", () => { + const error = new Error(); + error.name = "SyntaxError"; + + expect(isNetworkError(error)).toBe(false); + }); + + it("returns false for non-zero status", () => { + const error = { status: 500 }; + + expect(isNetworkError(error)).toBe(false); + }); + + it("returns false for undefined status", () => { + const error = new Error(); + + expect(isNetworkError(error)).toBe(false); + }); + }); +});