From 1722b03cb8812b4e45712f527e177ad33667b049 Mon Sep 17 00:00:00 2001 From: Sherzod Date: Tue, 2 Jun 2026 20:57:59 +0500 Subject: [PATCH 1/4] Add optional seedItemIds to trackRecommendationClick --- spec/src/modules/tracker.js | 119 +++++++++++++++++++++++++++++++++--- src/modules/tracker.js | 12 ++++ src/types/tracker.d.ts | 3 +- 3 files changed, 123 insertions(+), 11 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 7519645c..26bbdbf6 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -2785,7 +2785,8 @@ describe('ConstructorIO - Tracker', () => { expect(tracker.trackSearchResultsLoaded(term, { ...requiredParameters, - ...optionalParameters }, userParameters)).to.equal(true); + ...optionalParameters, + }, userParameters)).to.equal(true); }); it('Should respond with a valid response when term, required parameters and user identifier are provided', (done) => { @@ -4398,10 +4399,12 @@ describe('ConstructorIO - Tracker', () => { apiKey: testApiKey, fetch: fetchSpy, }); - const fullParameters = { ...requiredParameters, + const fullParameters = { + ...requiredParameters, type: 'add_to_loves', displayName: 'Add To Loves List', - isCustomType: true }; + isCustomType: true, + }; tracker.on('success', (responseParams) => { const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -4457,9 +4460,11 @@ describe('ConstructorIO - Tracker', () => { apiKey: testApiKey, fetch: fetchSpy, }); - const fullParameters = { ...requiredParameters, + const fullParameters = { + ...requiredParameters, display_name: 'Add To Loves List', - is_custom_type: true }; + is_custom_type: true, + }; tracker.on('success', (responseParams) => { const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -4525,9 +4530,11 @@ describe('ConstructorIO - Tracker', () => { apiKey: testApiKey, fetch: fetchSpy, }); - const fullParameters = { ...requiredParameters, + const fullParameters = { + ...requiredParameters, type: 'add_to_loves', - is_custom_type: true }; + is_custom_type: true, + }; tracker.on('error', (responseParams) => { const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -4694,7 +4701,7 @@ describe('ConstructorIO - Tracker', () => { const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); try { - // Request + // Request expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('key'); expect(requestParams).to.have.property('i'); @@ -4729,7 +4736,7 @@ describe('ConstructorIO - Tracker', () => { const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); try { - // Request + // Request expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('key'); expect(requestParams).to.have.property('i'); @@ -6341,6 +6348,98 @@ describe('ConstructorIO - Tracker', () => { expect(tracker.trackRecommendationClick(requiredParameters, { ...userParameters, userId })).to.equal(true); }); + + it('Should return valid response and omit seed_item_ids if no seedItemIds provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('seed_item_ids'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationClick(requiredParameters, userParameters)).to.equal(true); + }); + it('Should return valid response with seed_item_ids if seedItemIds is an array', (done) => { + const seedItemIds = ['123', '456']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('seed_item_ids').to.deep.equal(seedItemIds); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); + it('Should return valid response with seed_item_ids if seedItemIds is a string', (done) => { + const seedItemIds = '123'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('seed_item_ids').to.deep.equal([seedItemIds]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); + it('Should return valid response and omit seed_items_id if seedItemIds is not string or array', (done) => { + const seedItemIds = { seedItemIds: ['123', '456'] }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('seed_item_ids'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); }); describe('trackBrowseResultsLoaded', () => { @@ -8095,7 +8194,7 @@ describe('ConstructorIO - Tracker', () => { it('Should throw an error when providing no messageType parameter', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.on(null, () => {})).to.be.an('error'); + expect(tracker.on(null, () => { })).to.be.an('error'); }); it('Should throw an error when providing an invalid callback parameter', () => { diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 7dc73486..271460fe 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1193,6 +1193,7 @@ class Tracker { * @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 + * @param {string[]|string} [parameters.seedItemIds] - Item ID(s) used to generate recommendations * @param {object} userParameters - Parameters relevant to the user request * @param {number} userParameters.sessionId - Session ID, utilized to personalize results * @param {string} userParameters.clientId - Client ID, utilized to personalize results @@ -1221,6 +1222,7 @@ class Tracker { * podId: '019927c2-f955-4020', * strategyId: 'complimentary', * itemId: 'KMH876', + * seedItemIds: ['UIH976'] * }, * { * sessionId: 1, @@ -1259,6 +1261,8 @@ class Tracker { item_name, itemName = item_name, analyticsTags, + seed_item_ids, + seedItemIds = seed_item_ids, } = parameters; if (variationId) { @@ -1311,6 +1315,14 @@ class Tracker { bodyParams.analytics_tags = analyticsTags; } + if (seedItemIds?.length) { + if (typeof seedItemIds === 'string') { + bodyParams.seed_item_ids = [seedItemIds]; + } else if (Array.isArray(seedItemIds)) { + bodyParams.seed_item_ids = seedItemIds; + } + } + const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`; const requestMethod = 'POST'; const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index f73c3261..587cb17e 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { ConstructorClientOptions, NetworkParameters, ItemTracked, ItemTrackedPurchased } from '.'; +import { ConstructorClientOptions, ItemTracked, ItemTrackedPurchased, NetworkParameters } from '.'; export default Tracker; @@ -163,6 +163,7 @@ declare class Tracker { resultPositionOnPage?: number; numResultsPerPage?: number; analyticsTags?: Record; + seedItemIds?: string[] | string; }, userParameters?: TrackerUserParameters, networkParameters?: NetworkParameters From 930a4d55cd512d14ed3b44151c72817e3501367e Mon Sep 17 00:00:00 2001 From: Sherzod Date: Tue, 2 Jun 2026 23:54:43 +0500 Subject: [PATCH 2/4] Add optional seedItemIds to trackRecommendationView --- spec/src/modules/tracker.js | 94 ++++++++++++++++++++++++++++++++++++- src/modules/tracker.js | 12 +++++ src/types/tracker.d.ts | 1 + 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 26bbdbf6..38338f5c 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -5779,6 +5779,98 @@ describe('ConstructorIO - Tracker', () => { expect(tracker.trackRecommendationView(requiredParameters, { ...userParameters, userId })).to.equal(true); }); + + it('Should return valid response and omit seed_item_ids if no seedItemIds provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('seed_item_ids'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationView(requiredParameters, userParameters)).to.equal(true); + }); + it('Should return valid response with seed_item_ids if seedItemIds is an array', (done) => { + const seedItemIds = ['123', '456']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('seed_item_ids').to.deep.equal(seedItemIds); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); + it('Should return valid response with seed_item_ids if seedItemIds is a string', (done) => { + const seedItemIds = '123'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('seed_item_ids').to.deep.equal([seedItemIds]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); + it('Should return valid response and omit seed_item_ids if seedItemIds is not string or array', (done) => { + const seedItemIds = { seedItemIds: ['123', '456'] }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('seed_item_ids'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); }); describe('trackRecommendationClick', () => { @@ -6417,7 +6509,7 @@ describe('ConstructorIO - Tracker', () => { expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); }); - it('Should return valid response and omit seed_items_id if seedItemIds is not string or array', (done) => { + it('Should return valid response and omit seed_item_ids if seedItemIds is not string or array', (done) => { const seedItemIds = { seedItemIds: ['123', '456'] }; const { tracker } = new ConstructorIO({ apiKey: testApiKey, diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 271460fe..d7a85363 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1061,6 +1061,7 @@ class Tracker { * @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 + * @param {string[]|string} [parameters.seedItemIds] - Item ID(s) used to generate recommendations * @param {object} userParameters - Parameters relevant to the user request * @param {number} userParameters.sessionId - Session ID, utilized to personalize results * @param {string} userParameters.clientId - Client ID, utilized to personalize results @@ -1087,6 +1088,7 @@ class Tracker { * url: 'https://demo.constructor.io/sandbox/farmstand', * podId: '019927c2-f955-4020', * numResultsViewed: 3, + * seedItemIds: ['UIH976'] * }, * { * sessionId: 1, @@ -1117,6 +1119,8 @@ class Tracker { numResultsViewed = num_results_viewed, items, analyticsTags, + seed_item_ids, + seedItemIds = seed_item_ids, } = parameters; if (!helpers.isNil(resultCount)) { @@ -1157,6 +1161,14 @@ class Tracker { bodyParams.analytics_tags = analyticsTags; } + if (seedItemIds?.length) { + if (typeof seedItemIds === 'string') { + bodyParams.seed_item_ids = [seedItemIds]; + } else if (Array.isArray(seedItemIds)) { + bodyParams.seed_item_ids = seedItemIds; + } + } + const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`; const requestMethod = 'POST'; const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index 587cb17e..1ae94df1 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -144,6 +144,7 @@ declare class Tracker { resultId?: string; section?: string; analyticsTags?: Record; + seedItemIds?: string[] | string; }, userParameters?: TrackerUserParameters, networkParameters?: NetworkParameters From 811f30af28a3eaa7ffec4c19c28c3747fcd3d3ec Mon Sep 17 00:00:00 2001 From: Sherzod Date: Fri, 5 Jun 2026 18:07:42 +0500 Subject: [PATCH 3/4] add support for number type of seedItemIds --- spec/src/modules/tracker.js | 50 +++++++++++++++++++++++++++++++++++-- src/modules/tracker.js | 24 ++++++++---------- src/types/tracker.d.ts | 4 +-- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 38338f5c..50163b80 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -5848,7 +5848,30 @@ describe('ConstructorIO - Tracker', () => { expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); }); - it('Should return valid response and omit seed_item_ids if seedItemIds is not string or array', (done) => { + it('Should return valid response with seed_item_ids if seedItemIds is a number', (done) => { + const seedItemIds = 123; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('seed_item_ids').to.deep.equal([String(seedItemIds)]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); + it('Should return valid response and omit seed_item_ids if seedItemIds is not string, number, or array', (done) => { const seedItemIds = { seedItemIds: ['123', '456'] }; const { tracker } = new ConstructorIO({ apiKey: testApiKey, @@ -6509,7 +6532,30 @@ describe('ConstructorIO - Tracker', () => { expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); }); - it('Should return valid response and omit seed_item_ids if seedItemIds is not string or array', (done) => { + it('Should return valid response with seed_item_ids if seedItemIds is a number', (done) => { + const seedItemIds = 123; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('seed_item_ids').to.deep.equal([String(seedItemIds)]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true); + }); + it('Should return valid response and omit seed_item_ids if seedItemIds is not string, number, or array', (done) => { const seedItemIds = { seedItemIds: ['123', '456'] }; const { tracker } = new ConstructorIO({ apiKey: testApiKey, diff --git a/src/modules/tracker.js b/src/modules/tracker.js index d7a85363..4cf0930e 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1061,7 +1061,7 @@ class Tracker { * @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 - * @param {string[]|string} [parameters.seedItemIds] - Item ID(s) used to generate recommendations + * @param {string[]|string|number} [parameters.seedItemIds] - Item ID(s) used to generate recommendations * @param {object} userParameters - Parameters relevant to the user request * @param {number} userParameters.sessionId - Session ID, utilized to personalize results * @param {string} userParameters.clientId - Client ID, utilized to personalize results @@ -1161,12 +1161,10 @@ class Tracker { bodyParams.analytics_tags = analyticsTags; } - if (seedItemIds?.length) { - if (typeof seedItemIds === 'string') { - bodyParams.seed_item_ids = [seedItemIds]; - } else if (Array.isArray(seedItemIds)) { - bodyParams.seed_item_ids = seedItemIds; - } + if ((typeof seedItemIds === 'string' || typeof seedItemIds === 'number') && String(seedItemIds).length) { + bodyParams.seed_item_ids = [String(seedItemIds)]; + } else if (Array.isArray(seedItemIds) && seedItemIds.length) { + bodyParams.seed_item_ids = seedItemIds.map(String); } const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`; @@ -1205,7 +1203,7 @@ class Tracker { * @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 - * @param {string[]|string} [parameters.seedItemIds] - Item ID(s) used to generate recommendations + * @param {string[]|string|number} [parameters.seedItemIds] - Item ID(s) used to generate recommendations * @param {object} userParameters - Parameters relevant to the user request * @param {number} userParameters.sessionId - Session ID, utilized to personalize results * @param {string} userParameters.clientId - Client ID, utilized to personalize results @@ -1327,12 +1325,10 @@ class Tracker { bodyParams.analytics_tags = analyticsTags; } - if (seedItemIds?.length) { - if (typeof seedItemIds === 'string') { - bodyParams.seed_item_ids = [seedItemIds]; - } else if (Array.isArray(seedItemIds)) { - bodyParams.seed_item_ids = seedItemIds; - } + if ((typeof seedItemIds === 'string' || typeof seedItemIds === 'number') && String(seedItemIds).length) { + bodyParams.seed_item_ids = [String(seedItemIds)]; + } else if (Array.isArray(seedItemIds) && seedItemIds.length) { + bodyParams.seed_item_ids = seedItemIds.map(String); } const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`; diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index 1ae94df1..cffb636f 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -144,7 +144,7 @@ declare class Tracker { resultId?: string; section?: string; analyticsTags?: Record; - seedItemIds?: string[] | string; + seedItemIds?: string[] | string | number; }, userParameters?: TrackerUserParameters, networkParameters?: NetworkParameters @@ -164,7 +164,7 @@ declare class Tracker { resultPositionOnPage?: number; numResultsPerPage?: number; analyticsTags?: Record; - seedItemIds?: string[] | string; + seedItemIds?: string[] | string | number; }, userParameters?: TrackerUserParameters, networkParameters?: NetworkParameters From 9dcc597d851fb754465320b9722ecbc88c728445 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Fri, 5 Jun 2026 12:26:36 -0400 Subject: [PATCH 4/4] Bump ci version --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 85303492..b5975b3d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ["18.13.0"] + node: ["22.18.0"] steps: - name: Check out code uses: actions/checkout@v3