From 0176fb9ca0e6968844c342991c79c56ba506be14 Mon Sep 17 00:00:00 2001 From: Sherzod Date: Wed, 10 Jun 2026 19:57:32 +0500 Subject: [PATCH 1/5] add result_offset to tracking methods --- spec/src/modules/tracker.js | 739 +++++++++++++++++++++++++++++++++--- src/modules/tracker.js | 59 +++ src/types/tracker.d.ts | 10 +- 3 files changed, 761 insertions(+), 47 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index fc275beb..be76ee2f 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -2815,6 +2815,87 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackSearchResultsLoaded(term, parameters)).to.equal(true); }); + + it('Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackSearchResultsLoaded(term, { + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackSearchResultsLoaded(term, { + ...requiredParameters, + })).to.equal(true); + }); + + it('Should receive error from backend when both resultPage and resultOffset are passed', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('error', (error) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); + + // Response + expect(error).to.have.property('message').to.equal('Invalid parameters'); + + done(); + }); + + expect(tracker.trackSearchResultsLoaded(term, { + ...requiredParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); + }); }); describe('trackSearchResultClick', () => { @@ -5218,6 +5299,87 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackRecommendationView(parameters)).to.equal(true); }); + + it('Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackRecommendationView({ + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackRecommendationView({ + ...requiredParameters, + })).to.equal(true); + }); + + it('Should receive error from backend when both resultPage and resultOffset are passed', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('error', (error) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); + + // Response + expect(error).to.have.property('message').to.equal('Invalid parameters'); + + done(); + }); + + expect(tracker.trackRecommendationView({ + ...requiredParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); + }); }); describe('trackRecommendationClick', () => { @@ -5814,6 +5976,87 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackRecommendationClick(parametersWithSeedItemIds)).to.equal(true); }); + + it('Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackRecommendationClick({ + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackRecommendationClick({ + ...requiredParameters, + })).to.equal(true); + }); + + it('Should receive error from backend when both resultPage and resultOffset are passed', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('error', (error) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); + + // Response + expect(error).to.have.property('message').to.equal('Invalid parameters'); + + done(); + }); + + expect(tracker.trackRecommendationClick({ + ...requiredParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); + }); }); describe('trackBrowseResultsLoaded', () => { @@ -6306,23 +6549,8 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackBrowseResultsLoaded(parameters)).to.equal(true); }); - }); - - describe('trackBrowseRedirect', () => { - const requiredParameters = { - filterName: 'group_id', - filterValue: 'Clothing', - searchTerm: 'books', - }; - const optionalParameters = { - section: 'Products', - selectedFilters: { foo: ['bar'] }, - analyticsTags: testAnalyticsTag, - redirectToUrl: 'https://demo.constructor.io/books', - userInput: 'book', - }; - it('Should respond with a valid response when required parameters are provided', (done) => { + it('Should return valid response when resultOffset is passed as an integer', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey, fetch: fetchSpy, @@ -6334,15 +6562,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { // Request expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('key'); - expect(requestParams).to.have.property('i'); - expect(requestParams).to.have.property('s'); - expect(requestParams).to.have.property('c').to.equal(clientVersion); - expect(requestParams).to.have.property('_dt'); - expect(requestParams).to.have.property('filter_name').to.equal(requiredParameters.filterName); - expect(requestParams).to.have.property('filter_value').to.equal(requiredParameters.filterValue); - expect(requestParams).to.have.property('search_term').to.equal(requiredParameters.searchTerm); - validateOriginReferrer(requestParams); + expect(requestParams).to.have.property('result_offset').to.equal(1); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -6351,11 +6571,13 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackBrowseRedirect(requiredParameters)).to.equal(true); + expect(tracker.trackBrowseResultsLoaded({ + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); }); - it('Should respond with a valid response and section should be defaulted when required parameters are provided', (done) => { - const clonedParameters = cloneDeep(requiredParameters); + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey, fetch: fetchSpy, @@ -6367,7 +6589,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { // Request expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('section').to.equal('Products'); + expect(requestParams).to.not.have.property('result_offset'); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -6376,43 +6598,146 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - delete clonedParameters.section; - - expect(tracker.trackBrowseRedirect(clonedParameters)).to.equal(true); + expect(tracker.trackBrowseResultsLoaded({ + ...requiredParameters, + })).to.equal(true); }); - it('Should send along document_referrer and canonical_url query param', (done) => { + it('Should receive error from backend when both resultPage and resultOffset are passed', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey, fetch: fetchSpy, ...requestQueueOptions, }); - tracker.on('success', (responseParams) => { + tracker.on('error', (error) => { const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); // Request expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); - expect(requestParams).to.have.property('document_referrer').to.equal(referrer); + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); // Response - expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message'); + expect(error).to.have.property('message').to.equal('Invalid parameters'); done(); }); - expect(tracker.trackBrowseRedirect(requiredParameters)).to.equal(true); + expect(tracker.trackBrowseResultsLoaded({ + ...requiredParameters, + ...optionalParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); }); + }); - it('Should respond with a valid response when required parameters and segments are provided', (done) => { - const segments = ['foo', 'bar']; - const { tracker } = new ConstructorIO({ - apiKey: testApiKey, - segments, - fetch: fetchSpy, - ...requestQueueOptions, + describe('trackBrowseRedirect', () => { + const requiredParameters = { + filterName: 'group_id', + filterValue: 'Clothing', + searchTerm: 'books', + }; + const optionalParameters = { + section: 'Products', + selectedFilters: { foo: ['bar'] }, + analyticsTags: testAnalyticsTag, + redirectToUrl: 'https://demo.constructor.io/books', + userInput: 'book', + }; + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('filter_name').to.equal(requiredParameters.filterName); + expect(requestParams).to.have.property('filter_value').to.equal(requiredParameters.filterValue); + expect(requestParams).to.have.property('search_term').to.equal(requiredParameters.searchTerm); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackBrowseRedirect(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response and section should be defaulted when required parameters are provided', (done) => { + const clonedParameters = cloneDeep(requiredParameters); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal('Products'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + delete clonedParameters.section; + + expect(tracker.trackBrowseRedirect(clonedParameters)).to.equal(true); + }); + + it('Should send along document_referrer and canonical_url query param', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('document_referrer').to.equal(referrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackBrowseRedirect(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, }); tracker.on('success', (responseParams) => { @@ -7141,6 +7466,87 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackBrowseResultClick(legacyParameters)).to.equal(true); }); + + it('Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackBrowseResultClick({ + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackBrowseResultClick({ + ...requiredParameters, + })).to.equal(true); + }); + + it('Should receive error from backend when both resultPage and resultOffset are passed', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('error', (error) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); + + // Response + expect(error).to.have.property('message').to.equal('Invalid parameters'); + + done(); + }); + + expect(tracker.trackBrowseResultClick({ + ...requiredParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); + }); }); describe('trackGenericResultClick', () => { @@ -7558,6 +7964,59 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackGenericResultClick(requiredParameters)).to.equal(true); }); + + it('Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackGenericResultClick({ + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackGenericResultClick({ + ...requiredParameters, + })).to.equal(true); + }); }); describe('trackQuizResultsLoaded', () => { @@ -8081,6 +8540,100 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackQuizResultsLoaded(parameters)).to.equal(true); }); + + it('Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackQuizResultsLoaded({ + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackQuizResultsLoaded({ + ...requiredParameters, + })).to.equal(true); + }); + + it('Should return an error when resultOffset is not a number', () => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + expect(tracker.trackQuizResultsLoaded({ + ...requiredParameters, + resultOffset: 'not-a-number', + })).to.be.an('error'); + }); + + it('Should receive error from backend when both resultPage and resultOffset are passed', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('error', (error) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); + + // Response + expect(error).to.have.property('message').to.equal('Invalid parameters'); + + done(); + }); + + expect(tracker.trackQuizResultsLoaded({ + ...requiredParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); + }); }); describe('trackQuizResultClick', () => { @@ -8546,6 +9099,100 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackQuizResultClick(requiredParameters)).to.equal(true); }); + + it('Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackQuizResultClick({ + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackQuizResultClick({ + ...requiredParameters, + })).to.equal(true); + }); + + it('Should return an error when resultOffset is not a number', () => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + expect(tracker.trackQuizResultClick({ + ...requiredParameters, + resultOffset: 'not-a-number', + })).to.be.an('error'); + }); + + it('Should receive error from backend when both resultPage and resultOffset are passed', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('error', (error) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); + + // Response + expect(error).to.have.property('message').to.equal('Invalid parameters'); + + done(); + }); + + expect(tracker.trackQuizResultClick({ + ...requiredParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); + }); }); describe('trackQuizConversion', () => { diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 199477fe..50fa1677 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -676,6 +676,7 @@ class Tracker { * @param {object[]} parameters.items - List of product item unique identifiers in search results listing * @param {number} [parameters.resultCount] - Total number of results * @param {number} [parameters.resultPage] - Current page of search results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {string} [parameters.resultId] - Browse result identifier (returned in response from Constructor) * @param {object} [parameters.selectedFilters] - Selected filters * @param {string} [parameters.sortOrder] - Sort order ('ascending' or 'descending') @@ -716,6 +717,8 @@ class Tracker { items = customer_ids || item_ids, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, result_id, resultId = result_id, sort_order, @@ -751,6 +754,7 @@ class Tracker { result_count: resultCount, items: transformedItems, result_page: resultPage, + result_offset: resultOffset, result_id: resultId, sort_order: sortOrder, sort_by: sortBy, @@ -1242,6 +1246,7 @@ class Tracker { * @param {object[]} [parameters.items] - List of Product Item Objects * @param {number} [parameters.resultCount] - Total number of results * @param {number} [parameters.resultPage] - Page number of results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {string} [parameters.resultId] - Recommendation result identifier (returned in response from Constructor) * @param {string} [parameters.section="Products"] - Results section * @param {object} [parameters.analyticsTags] - Pass additional analytics data @@ -1272,6 +1277,8 @@ class Tracker { result_count, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, result_id, resultId = result_id, section, @@ -1294,6 +1301,10 @@ class Tracker { bodyParams.result_page = resultPage; } + if (!helpers.isNil(resultOffset)) { + bodyParams.result_offset = resultOffset; + } + if (resultId) { bodyParams.result_id = resultId; } @@ -1498,6 +1509,7 @@ class Tracker { * @param {string} [parameters.resultId] - Recommendation result identifier (returned in response from Constructor) * @param {number} [parameters.resultCount] - Total number of results * @param {number} [parameters.resultPage] - Page number of results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {number} [parameters.resultPositionOnPage] - Position of result on page * @param {number} [parameters.numResultsPerPage] - Number of results on page * @param {object} [parameters.analyticsTags] - Pass additional analytics data @@ -1540,6 +1552,8 @@ class Tracker { resultCount = result_count, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, result_position_on_page, resultPositionOnPage = result_position_on_page, num_results_per_page, @@ -1578,6 +1592,10 @@ class Tracker { bodyParams.result_page = resultPage; } + if (!helpers.isNil(resultOffset)) { + bodyParams.result_offset = resultOffset; + } + if (!helpers.isNil(resultPositionOnPage)) { bodyParams.result_position_on_page = resultPositionOnPage; } @@ -1654,6 +1672,7 @@ class Tracker { * @param {string} [parameters.section="Products"] - Index section * @param {number} [parameters.resultCount] - Total number of results * @param {number} [parameters.resultPage] - Page number of results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {string} [parameters.resultId] - Browse result identifier (returned in response from Constructor) * @param {object} [parameters.selectedFilters] - Selected filters * @param {string} [parameters.sortOrder] - Sort order ('ascending' or 'descending') @@ -1689,6 +1708,8 @@ class Tracker { result_count, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, result_id, resultId = result_id, selected_filters, @@ -1719,6 +1740,10 @@ class Tracker { bodyParams.result_page = resultPage; } + if (!helpers.isNil(resultOffset)) { + bodyParams.result_offset = resultOffset; + } + if (resultId) { bodyParams.result_id = resultId; } @@ -1789,6 +1814,7 @@ class Tracker { * @param {string} [parameters.resultId] - Browse result identifier (returned in response from Constructor) * @param {number} [parameters.resultCount] - Total number of results * @param {number} [parameters.resultPage] - Page number of results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {number} [parameters.resultPositionOnPage] - Position of clicked item * @param {number} [parameters.numResultsPerPage] - Number of results shown * @param {object} [parameters.selectedFilters] - Selected filters @@ -1832,6 +1858,8 @@ class Tracker { resultCount = result_count, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, result_position_on_page, resultPositionOnPage = result_position_on_page, num_results_per_page, @@ -1874,6 +1902,10 @@ class Tracker { bodyParams.result_page = resultPage; } + if (!helpers.isNil(resultOffset)) { + bodyParams.result_offset = resultOffset; + } + if (!helpers.isNil(resultPositionOnPage)) { bodyParams.result_position_on_page = resultPositionOnPage; } @@ -2053,6 +2085,7 @@ class Tracker { * @param {string} [parameters.itemName] - Product item name * @param {string} [parameters.variationId] - Product item variation unique identifier * @param {string} [parameters.section="Products"] - Index section + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {object} [parameters.analyticsTags] - Pass additional analytics data * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -2080,6 +2113,8 @@ class Tracker { variation_id, variationId = variation_id, section = 'Products', + result_offset, + resultOffset = result_offset, analyticsTags, } = parameters; if (itemId) { @@ -2094,6 +2129,10 @@ class Tracker { bodyParams.variation_id = variationId; } + if (!helpers.isNil(resultOffset)) { + bodyParams.result_offset = resultOffset; + } + if (analyticsTags) { bodyParams.analytics_tags = analyticsTags; } @@ -2132,6 +2171,7 @@ class Tracker { * @param {string} [parameters.section='Products'] - Index section * @param {number} [parameters.resultCount] - Total number of results * @param {number} [parameters.resultPage] - The page of the results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {string} [parameters.resultId] - Quiz result identifier (returned in response from Constructor) * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -2169,6 +2209,8 @@ class Tracker { resultId = result_id, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, items, } = parameters; const queryParams = {}; @@ -2228,6 +2270,13 @@ class Tracker { bodyParams.result_page = resultPage; } + if (!helpers.isNil(resultOffset)) { + if (typeof resultOffset !== 'number') { + return new Error('"resultOffset" must be a number'); + } + bodyParams.result_offset = resultOffset; + } + const requestURL = `${requestPath}${applyParamsAsString(queryParams, this.options)}`; const requestMethod = 'POST'; const requestBody = applyParams(bodyParams, { ...this.options, requestMethod }); @@ -2262,6 +2311,7 @@ class Tracker { * @param {string} [parameters.section='Products'] - Index section * @param {number} [parameters.resultCount] - Total number of results * @param {number} [parameters.resultPage] - The page of the results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {string} [parameters.resultId] - Quiz result identifier (returned in response from Constructor) * @param {number} [parameters.resultPositionOnPage] - Position of clicked item * @param {number} [parameters.numResultsPerPage] - Number of results shown @@ -2303,6 +2353,8 @@ class Tracker { resultId = result_id, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, num_results_per_page, numResultsPerPage = num_results_per_page, result_position_on_page, @@ -2383,6 +2435,13 @@ class Tracker { bodyParams.result_page = resultPage; } + if (!helpers.isNil(resultOffset)) { + if (typeof resultOffset !== 'number') { + return new Error('"resultOffset" must be a number'); + } + bodyParams.result_offset = resultOffset; + } + if (!helpers.isNil(numResultsPerPage)) { if (typeof numResultsPerPage !== 'number') { return new Error('"numResultsPerPage" must be a number'); diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index e04cfb85..6e71b7b4 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -1,5 +1,5 @@ +import { ConstructorClientOptions, ItemTracked, ItemTrackedPurchase, NetworkParameters, Question, TimeSpan } from '.'; import { EventEmitter } from './events'; -import { ConstructorClientOptions, ItemTracked, ItemTrackedPurchase, Question, TimeSpan, NetworkParameters } from '.'; import RequestQueue from './request-queue'; export default Tracker; @@ -81,6 +81,7 @@ declare class Tracker { items: ItemTracked[]; resultCount?: number; resultPage?: number; + resultOffset?: number; resultId?: string; selectedFilters?: Record; sortOrder?: string; @@ -143,6 +144,7 @@ declare class Tracker { items?: ItemTracked[]; resultCount?: number; resultPage?: number; + resultOffset?: number; resultId?: string; section?: string; analyticsTags?: Record; @@ -161,6 +163,7 @@ declare class Tracker { resultId?: string; resultCount?: number; resultPage?: number; + resultOffset?: number; resultPositionOnPage?: number; numResultsPerPage?: number; analyticsTags?: Record; @@ -179,6 +182,7 @@ declare class Tracker { section?: string; resultCount?: number; resultPage?: number; + resultOffset?: number; resultId?: string; selectedFilters?: object; sortOrder?: string; @@ -200,6 +204,7 @@ declare class Tracker { resultId?: string; resultCount?: number; resultPage?: number; + resultOffset?: number; resultPositionOnPage?: number; numResultsPerPage?: number; selectedFilters?: object; @@ -216,6 +221,7 @@ declare class Tracker { itemName?: string; variationId?: string; section?: string; + resultOffset?: number; analyticsTags?: Record; }, networkParameters?: NetworkParameters @@ -230,6 +236,7 @@ declare class Tracker { section?: string; resultCount?: number; resultPage?: number; + resultOffset?: number; resultId?: string; items: ItemTracked[]; }, @@ -247,6 +254,7 @@ declare class Tracker { section?: string; resultCount?: number; resultPage?: number; + resultOffset?: number; resultId?: string; resultPositionOnPage?: number; numResultsPerPage?: number; From f18b8cd93e43c51633c991989772e103a029471e Mon Sep 17 00:00:00 2001 From: Sherzod Date: Wed, 10 Jun 2026 19:58:12 +0500 Subject: [PATCH 2/5] replace Object.assign with spread operator to avoid mutating requiredParameters --- spec/src/modules/tracker.js | 88 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index be76ee2f..57767a9d 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -1076,7 +1076,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAutocompleteSelect(term, Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackAutocompleteSelect(term, { ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -1475,7 +1475,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackItemDetailLoad(Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackItemDetailLoad({ ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -1501,7 +1501,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackItemDetailLoad(Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackItemDetailLoad({ ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -1886,7 +1886,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackSearchSubmit(term, Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackSearchSubmit(term, { ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should respond with a valid response when term, required and optional parameters are provided', (done) => { @@ -1914,7 +1914,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackSearchSubmit(term, Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackSearchSubmit(term, { ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when invalid term is provided', () => { @@ -2473,7 +2473,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackSearchResultsLoaded(term, Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackSearchResultsLoaded(term, { ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -4317,7 +4317,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackPurchase(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackPurchase({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should respond with a valid response when optional parameters are provided', (done) => { @@ -4344,7 +4344,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackPurchase(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackPurchase({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should respond with a valid response when required parameters and segments are provided', (done) => { @@ -4551,10 +4551,11 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { addOrderIdRecord({ orderId, apiKey }); - expect(tracker.trackPurchase(Object.assign(requiredParameters, { + expect(tracker.trackPurchase({ + ...requiredParameters, ...optionalParameters, orderId, - }))).to.equal(false); + })).to.equal(false); setTimeout(() => { // Request @@ -4591,10 +4592,11 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { addOrderIdRecord({ orderId: '239402919', apiKey: testApiKey }); addOrderIdRecord({ orderId: '482039192', apiKey: testApiKey }); - expect(tracker.trackPurchase(Object.assign(requiredParameters, { + expect(tracker.trackPurchase({ + ...requiredParameters, ...optionalParameters, orderId: '328192019', - }))).to.equal(true); + })).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { @@ -4974,7 +4976,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackRecommendationView(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackRecommendationView({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { @@ -5709,7 +5711,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackRecommendationClick(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackRecommendationClick({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { @@ -6335,7 +6337,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackBrowseResultsLoaded(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackBrowseResultsLoaded({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { @@ -6464,7 +6466,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackBrowseResultsLoaded(Object.assign(requiredParameters, parameters))).to.equal(true); + expect(tracker.trackBrowseResultsLoaded({ ...requiredParameters, ...parameters })).to.equal(true); }); it('Should not encode body parameters', (done) => { @@ -6834,7 +6836,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackBrowseRedirect(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackBrowseRedirect({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { @@ -7254,7 +7256,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackBrowseResultClick(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackBrowseResultClick({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should respond with a valid response when required parameters and non-existent item id are provided', (done) => { @@ -7781,7 +7783,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackGenericResultClick(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackGenericResultClick({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should respond with a valid response when required parameters and non-existent item id are provided', (done) => { @@ -8287,7 +8289,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackQuizResultsLoaded(Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackQuizResultsLoaded({ ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -8856,7 +8858,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackQuizResultClick(Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackQuizResultClick({ ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -9414,7 +9416,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackQuizConversion(Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackQuizConversion({ ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -9898,7 +9900,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAgentSubmit(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAgentSubmit({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -10215,7 +10217,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); // eslint-disable-next-line max-len - expect(tracker.trackAgentResultLoadStarted(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAgentResultLoadStarted({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -10533,7 +10535,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); // eslint-disable-next-line max-len - expect(tracker.trackAgentResultLoadFinished(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAgentResultLoadFinished({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -10859,7 +10861,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAgentResultClick(Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackAgentResultClick({ ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -11203,7 +11205,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAgentResultView(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAgentResultView({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { @@ -11521,7 +11523,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAgentSearchSubmit(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAgentSearchSubmit({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -11835,7 +11837,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAssistantSubmit(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAssistantSubmit({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -12152,7 +12154,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); // eslint-disable-next-line max-len - expect(tracker.trackAssistantResultLoadStarted(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAssistantResultLoadStarted({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -12470,7 +12472,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); // eslint-disable-next-line max-len - expect(tracker.trackAssistantResultLoadFinished(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAssistantResultLoadFinished({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -12796,7 +12798,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAssistantResultClick(Object.assign(requiredParameters, optionalParameters))) + expect(tracker.trackAssistantResultClick({ ...requiredParameters, ...optionalParameters })) .to.equal(true); }); @@ -13140,7 +13142,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAssistantResultView(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAssistantResultView({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { @@ -13458,7 +13460,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackAssistantSearchSubmit(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + expect(tracker.trackAssistantSearchSubmit({ ...requiredParameters, ...optionalParameters })).to.equal(true); }); it('Should throw an error when no parameters are provided', () => { @@ -13797,7 +13799,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentViews( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -14126,7 +14128,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentView( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -14448,7 +14450,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentOutOfView( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -14768,7 +14770,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentFocus( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -15089,7 +15091,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentQuestionClick( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -15410,7 +15412,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentQuestionSubmit( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -15734,7 +15736,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentAnswerView( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -16056,7 +16058,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentAnswerFeedback( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -16293,7 +16295,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect( tracker.trackMediaImpressionView( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, ), ).to.equal(true); }); From 222c3dc48094369c2dd08afb77fbaa04c4beb50d Mon Sep 17 00:00:00 2001 From: Sherzod Date: Wed, 10 Jun 2026 20:21:28 +0500 Subject: [PATCH 3/5] fix trackGenericResultClick test --- spec/src/modules/tracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 57767a9d..119b8cc6 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -7771,7 +7771,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { // Request expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('section').to.equal(requiredParameters.section); + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); expect(requestParams).to.have.property('analytics_tags').to.deep.equal(testAnalyticsTag); From 911e89fcc50987b606ae767e42f6fcff939cab42 Mon Sep 17 00:00:00 2001 From: Sherzod Date: Thu, 11 Jun 2026 00:39:54 +0500 Subject: [PATCH 4/5] update jsdoc for trackGenericResultClick --- src/modules/tracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 5bd83962..cb8e1513 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -2083,7 +2083,7 @@ class Tracker { * @param {string} [parameters.itemName] - Product item name * @param {string} [parameters.variationId] - Product item variation unique identifier * @param {string} [parameters.section="Products"] - Index section - * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites * @param {object} [parameters.analyticsTags] - Pass additional analytics data * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) From e299c754b00c7e822c6ec7b29fcf71a737e1d715 Mon Sep 17 00:00:00 2001 From: Sherzod Date: Fri, 12 Jun 2026 17:42:51 +0500 Subject: [PATCH 5/5] add result_offset to trackSearchResultClickV2 --- spec/src/modules/tracker.js | 81 +++++++++++++++++++++++++++++++++++++ src/modules/tracker.js | 4 ++ 2 files changed, 85 insertions(+) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index f3fbc92c..f6555b13 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -3001,6 +3001,87 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackSearchResultClickV2(term, { ...requiredParameters, ...optionalParameters, ...v2Parameters })).to.equal(true); }); + it('V2 Should return valid response when resultOffset is passed as an integer', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_offset').to.equal(1); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackSearchResultClickV2(term, { + ...requiredParameters, + resultOffset: 1, + })).to.equal(true); + }); + + it('V2 Should return valid response and omit result_offset when resultOffset is not provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('result_offset'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackSearchResultClickV2(term, { + ...requiredParameters, + })).to.equal(true); + }); + + it('V2 Should receive error from backend when both resultPage and resultOffset are passed', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('error', (error) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('result_page'); + expect(requestParams).to.have.property('result_offset'); + + // Response + expect(error).to.have.property('message').to.equal('Invalid parameters'); + + done(); + }); + + expect(tracker.trackSearchResultClickV2(term, { + ...requiredParameters, + resultPage: 10, + resultOffset: 1, + })).to.equal(true); + }); + it('Backwards Compatibility - Should respond with a valid response when term and snake cased parameters are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey, diff --git a/src/modules/tracker.js b/src/modules/tracker.js index cb8e1513..1475a618 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -800,6 +800,7 @@ class Tracker { * @param {string} [parameters.resultId] - Search result identifier (returned in response from Constructor) * @param {number} [parameters.resultCount] - Number of results in total * @param {number} [parameters.resultPage] - Current page of results + * @param {number} [parameters.resultOffset] - Current offset of results, used on scrolling sites. Cannot be used with `resultPage` * @param {string} [parameters.resultPositionOnPage] - Position of selected items on page * @param {string} [parameters.numResultsPerPage] - Number of results per page * @param {object} [parameters.selectedFilters] - Key - Value map of selected filters @@ -845,6 +846,8 @@ class Tracker { resultCount = num_results || result_count, result_page, resultPage = result_page, + result_offset, + resultOffset = result_offset, result_position_on_page, resultPositionOnPage = result_position_on_page, num_results_per_page, @@ -865,6 +868,7 @@ class Tracker { result_id: resultId, result_count: resultCount, result_page: resultPage, + result_offset: resultOffset, result_position_on_page: resultPositionOnPage, num_results_per_page: numResultsPerPage, selected_filters: selectedFilters,