From da5d6eb2b6220208e7c74bb3df66ce1722f4cd98 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Mon, 4 Nov 2024 17:24:26 -0500 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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/5] 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); + }); + }); +});