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..e8cbdefb9 --- /dev/null +++ b/src/utils/networkErrors.js @@ -0,0 +1,15 @@ +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..d7c9ea74f --- /dev/null +++ b/test/functional/helpers/requestHooks/demdexBlockerMock.js @@ -0,0 +1,12 @@ +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..f8f787422 --- /dev/null +++ b/test/functional/specs/Identity/demdexFallback.js @@ -0,0 +1,43 @@ +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); + }); + }); +});