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 diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 7519645c..50163b80 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'); @@ -5772,6 +5779,121 @@ 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 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, + 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', () => { @@ -6341,6 +6463,121 @@ 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 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, + 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 +8332,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..4cf0930e 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|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 @@ -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,12 @@ class Tracker { bodyParams.analytics_tags = analyticsTags; } + 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)}`; const requestMethod = 'POST'; const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); @@ -1193,6 +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|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 @@ -1221,6 +1232,7 @@ class Tracker { * podId: '019927c2-f955-4020', * strategyId: 'complimentary', * itemId: 'KMH876', + * seedItemIds: ['UIH976'] * }, * { * sessionId: 1, @@ -1259,6 +1271,8 @@ class Tracker { item_name, itemName = item_name, analyticsTags, + seed_item_ids, + seedItemIds = seed_item_ids, } = parameters; if (variationId) { @@ -1311,6 +1325,12 @@ class Tracker { bodyParams.analytics_tags = analyticsTags; } + 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)}`; 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..cffb636f 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; @@ -144,6 +144,7 @@ declare class Tracker { resultId?: string; section?: string; analyticsTags?: Record; + seedItemIds?: string[] | string | number; }, userParameters?: TrackerUserParameters, networkParameters?: NetworkParameters @@ -163,6 +164,7 @@ declare class Tracker { resultPositionOnPage?: number; numResultsPerPage?: number; analyticsTags?: Record; + seedItemIds?: string[] | string | number; }, userParameters?: TrackerUserParameters, networkParameters?: NetworkParameters