From 72b56222a879c768e56044e748d8718faecd81f9 Mon Sep 17 00:00:00 2001 From: Stanley Peng Date: Thu, 18 Dec 2025 13:41:31 -0800 Subject: [PATCH 1/4] add url validation for android referres --- spec/src/utils/helpers.js | 74 +++++++++++++++++++++++++++++++++++++++ src/utils/helpers.js | 32 ++++++++++++++--- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/spec/src/utils/helpers.js b/spec/src/utils/helpers.js index a038299b..749dac6c 100644 --- a/spec/src/utils/helpers.js +++ b/spec/src/utils/helpers.js @@ -15,6 +15,7 @@ const { isNil, getWindowLocation, getCanonicalUrl, + getDocumentReferrer, dispatchEvent, createCustomEvent, hasOrderIdRecord, @@ -215,6 +216,18 @@ describe('ConstructorIO - Utils - Helpers', () => { }); describe('getCanonicalUrl', () => { + it('Should return android app referrers in a valid url structure', () => { + const cleanup = jsdom(); + + const canonicalUrl = 'android-app://com.google.android.googlequicksearchbox/'; + const canonicalEle = document.querySelector('[rel=canonical]'); + canonicalEle.setAttribute('href', canonicalUrl); + + expect(getCanonicalUrl()).to.equal('https://com.google.android.googlequicksearchbox/'); + + cleanup(); + }); + it('Should return the canonical URL from the DOM link element', () => { const cleanup = jsdom(); @@ -264,6 +277,67 @@ describe('ConstructorIO - Utils - Helpers', () => { }); }); + describe('getDocumentReferrer', () => { + it('Should return android app referrers in a valid url structure', () => { + const cleanup = jsdom(); + + const referrerUrl = 'android-app://com.google.android.googlequicksearchbox/'; + Object.defineProperty(document, 'referrer', { + value: referrerUrl, + configurable: true, + }); + + expect(getDocumentReferrer()).to.equal('https://com.google.android.googlequicksearchbox/'); + + cleanup(); + }); + + it('Should return the referrer URL from the document', () => { + const cleanup = jsdom(); + + const referrerUrl = 'https://constructor.io/products/item'; + Object.defineProperty(document, 'referrer', { + value: referrerUrl, + configurable: true, + }); + + expect(getDocumentReferrer()).to.equal(referrerUrl); + + cleanup(); + }); + + it('Should return null for a relative url', () => { + const cleanup = jsdom(); + + const relativeUrl = '/products/item'; + Object.defineProperty(document, 'referrer', { + value: relativeUrl, + configurable: true, + }); + + const result = getDocumentReferrer(); + expect(result).to.be.null; + + cleanup(); + }); + + it('Should return null when referrer is empty', () => { + const cleanup = jsdom(); + + Object.defineProperty(document, 'referrer', { + value: '', + configurable: true, + }); + + expect(getDocumentReferrer()).to.be.null; + cleanup(); + }); + + it('Should return null when not in a DOM context', () => { + expect(getDocumentReferrer()).to.be.null; + }); + }); + describe('dispatchEvent', () => { it('Should dispatch an event if in a DOM context', () => { const cleanup = jsdom(); diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 057745d3..4f688a6b 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -44,6 +44,23 @@ const utils = { return cleanedParams; }, + cleanAndValidateUrl: (url, baseUrl = undefined) => { + let validatedUrl = null; + + try { + // Handle android app referrers + if (url?.startsWith('android-app')) { + url = url?.replace('android-app', 'https') + } + + validatedUrl = (new URL(url, baseUrl)).toString(); + } catch (e) { + // do nothing + } + + return validatedUrl; + }, + throwHttpErrorFromResponse: (error, response) => response.json().then((json) => { error.message = json.message; error.status = response.status; @@ -92,11 +109,17 @@ const utils = { }, getDocumentReferrer: () => { - if (utils.canUseDOM()) { - return document?.referrer; + let documentReferrer = null; + + try { + if (utils.canUseDOM()) { + documentReferrer = utils.cleanAndValidateUrl(document.referrer); + } + } catch (e) { + // do nothing } - return null; + return documentReferrer; }, getCanonicalUrl: () => { @@ -108,8 +131,7 @@ const utils = { const href = linkEle?.getAttribute('href'); if (href) { - const url = new URL(href, document.location.href); - canonicalURL = url.toString(); + canonicalURL = utils.cleanAndValidateUrl(href, document.location.href); } } } catch (e) { From 979fe650e475beff6dbdffba6d0b7ec0d1a38805 Mon Sep 17 00:00:00 2001 From: Stanley Peng Date: Thu, 18 Dec 2025 14:20:36 -0800 Subject: [PATCH 2/4] lint spell check --- cspell.json | 3 ++- src/utils/helpers.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index 1a988a7b..b91c2462 100644 --- a/cspell.json +++ b/cspell.json @@ -48,6 +48,7 @@ "atcs", "testdata", "Bytespider", - "Timespans" + "Timespans", + "googlequicksearchbox" ] } diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 4f688a6b..c1a4953d 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -50,7 +50,7 @@ const utils = { try { // Handle android app referrers if (url?.startsWith('android-app')) { - url = url?.replace('android-app', 'https') + url = url?.replace('android-app', 'https'); } validatedUrl = (new URL(url, baseUrl)).toString(); From 443285575c97422b7b375aea78b51003e3fb6390 Mon Sep 17 00:00:00 2001 From: Stanley Peng Date: Mon, 29 Dec 2025 12:18:12 -0800 Subject: [PATCH 3/4] add url trimming --- spec/src/utils/helpers.js | 85 +++++++++++++++++++++++++++++++++++++++ src/utils/helpers.js | 40 +++++++++++++++++- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/spec/src/utils/helpers.js b/spec/src/utils/helpers.js index 749dac6c..2da73912 100644 --- a/spec/src/utils/helpers.js +++ b/spec/src/utils/helpers.js @@ -24,6 +24,8 @@ const { stringify, convertResponseToJson, addHTTPSToString, + trimUrl, + cleanAndValidateUrl, } = require('../../../test/utils/helpers'); // eslint-disable-line import/extensions const jsdom = require('./jsdom-global'); const store = require('../../../test/utils/store'); // eslint-disable-line import/extensions @@ -613,5 +615,88 @@ describe('ConstructorIO - Utils - Helpers', () => { expect(addHTTPSToString(testUrl)).to.equal(null); }); }); + + describe('trimUrl', () => { + it('Should return the URL as-is if it is under the max length', () => { + const testUrl = new URL('https://www.constructor.io/search?q=test'); + const result = trimUrl(testUrl); + + expect(result).to.equal('https://www.constructor.io/search?q=test'); + }); + + it('Should remove the longest parameter when URL exceeds max length', () => { + const longValue = 'a'.repeat(2000); + const testUrl = new URL(`https://www.constructor.io/search?short=b&long=${longValue}`); + const result = trimUrl(testUrl, 100); + + expect(result).to.include('short=b'); + expect(result).to.not.include('long='); + expect(result.length).to.be.at.most(100); + }); + + it('Should remove multiple parameters starting with the longest', () => { + const testUrl = new URL('https://www.constructor.io/search?a=1&b=22&c=333&d=4444'); + const result = trimUrl(testUrl, 50); + + expect(result.length).to.be.at.most(50); + }); + + it('Should truncate URL if removing all parameters is not enough', () => { + const longPath = 'a'.repeat(2000); + const testUrl = new URL(`https://www.constructor.io/${longPath}`); + const result = trimUrl(testUrl, 100); + + expect(result.length).to.equal(100); + }); + + it('Should use custom maxLen parameter', () => { + const testUrl = new URL('https://www.constructor.io/search?param=value'); + const customMaxLen = 30; + const result = trimUrl(testUrl, customMaxLen); + + expect(result.length).to.be.at.most(customMaxLen); + }); + }); + + describe('cleanAndValidateUrl', () => { + it('Should return a valid URL string', () => { + const testUrl = 'https://www.constructor.io/search?q=test'; + const result = cleanAndValidateUrl(testUrl); + + expect(result).to.equal(testUrl); + }); + + it('Should handle android-app referrers by converting to https', () => { + const androidUrl = 'android-app://com.google.android.googlequicksearchbox/path'; + const result = cleanAndValidateUrl(androidUrl); + + expect(result).to.include('https://'); + expect(result).to.include('com.google.android.googlequicksearchbox'); + }); + + it('Should return null for invalid URLs', () => { + const invalidUrl = 'not a valid url'; + const result = cleanAndValidateUrl(invalidUrl); + + expect(result).to.be.null; + }); + + it('Should handle relative URLs with baseUrl', () => { + const relativeUrl = '/search?q=test'; + const baseUrl = 'https://www.constructor.io'; + const result = cleanAndValidateUrl(relativeUrl, baseUrl); + + expect(result).to.include('https://www.constructor.io/search?q=test'); + }); + + it('Should trim URLs that exceed max length', () => { + const longValue = 'a'.repeat(2000); + const testUrl = `https://www.constructor.io/search?param=${longValue}`; + const result = cleanAndValidateUrl(testUrl); + + expect(result).to.not.be.null; + expect(result.length).to.be.at.most(2000); + }); + }); } }); diff --git a/src/utils/helpers.js b/src/utils/helpers.js index c1a4953d..fdd7fb8a 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -19,6 +19,7 @@ const PII_REGEX = [ }, // Add more PII REGEX ]; +const URL_MAX_LEN = 2000; const utils = { trimNonBreakingSpaces: (string) => string.replace(/\s/g, ' ').trim(), @@ -43,6 +44,40 @@ const utils = { return cleanedParams; }, + trimUrl: (urlObj, maxLen = URL_MAX_LEN) => { + let urlString = urlObj.toString(); + + if (urlString.length <= maxLen) { + return urlString; + } + + const { searchParams } = urlObj; + const paramEntries = Array.from(searchParams.entries()); + + if (paramEntries.length === 0) { + return utils.truncateString(urlString, maxLen); + } + + paramEntries.sort((a, b) => { + const aLength = a[0].length + a[1].length; + const bLength = b[0].length + b[1].length; + return bLength - aLength; + }); + + for (let i = 0; i < paramEntries.length; i += 1) { + if (urlString.length <= maxLen) { + break; + } + searchParams.delete(paramEntries[i][0]); + urlString = urlObj.toString(); + } + + if (urlString.length > maxLen) { + return utils.truncateString(urlString, maxLen); + } + + return urlString; + }, cleanAndValidateUrl: (url, baseUrl = undefined) => { let validatedUrl = null; @@ -53,7 +88,10 @@ const utils = { url = url?.replace('android-app', 'https'); } - validatedUrl = (new URL(url, baseUrl)).toString(); + const urlObj = new URL(url, baseUrl); + const trimmedUrl = new URL(utils.trimUrl(urlObj)); + + validatedUrl = trimmedUrl.toString(); } catch (e) { // do nothing } From d75cb16e7f8e9786799f74464e6419b951410a44 Mon Sep 17 00:00:00 2001 From: Stanley Peng Date: Mon, 29 Dec 2025 12:42:31 -0800 Subject: [PATCH 4/4] remove mutation --- src/utils/helpers.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/helpers.js b/src/utils/helpers.js index fdd7fb8a..ed26f398 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -51,7 +51,8 @@ const utils = { return urlString; } - const { searchParams } = urlObj; + const urlCopy = new URL(urlObj.toString()); + const { searchParams } = urlCopy; const paramEntries = Array.from(searchParams.entries()); if (paramEntries.length === 0) { @@ -69,7 +70,7 @@ const utils = { break; } searchParams.delete(paramEntries[i][0]); - urlString = urlObj.toString(); + urlString = urlCopy.toString(); } if (urlString.length > maxLen) {