From e300aa1ba74f74c14040f746a26c4a2c332e6e11 Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Fri, 1 May 2026 21:55:54 +0500 Subject: [PATCH 01/10] [NO-REF] - add pia recommendations tracking --- src/modules/tracker.js | 150 +++++++++++++++++++++++++++++++++++++++++ src/types/tracker.d.ts | 24 +++++++ 2 files changed, 174 insertions(+) diff --git a/src/modules/tracker.js b/src/modules/tracker.js index f8c7588c..8b04f62a 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3683,6 +3683,156 @@ class Tracker { return new Error('parameters is a required parameter of type object'); } + /** + * Send product insights agent recommendation view event + * + * @function trackProductInsightsAgentRecommendationView + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {object[]} [parameters.items] - List of recommended product items displayed + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User was shown product recommendations in the PIA widget + * @example + * constructorio.tracker.trackProductInsightsAgentRecommendationView( + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'items': [{ 'itemId': '10', 'itemName': 'rec1' }], + * }, + * ); + */ + trackProductInsightsAgentRecommendationView(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_recommendation_view?`; + const { + section, + itemId, + itemName, + variationId, + items, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + }; + + if (items && Array.isArray(items)) { + bodyParams.items = items.slice(0, 100).map((item) => helpers.toSnakeCaseKeys(item, false)); + } + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent recommendation click event + * + * @function trackProductInsightsAgentRecommendationClick + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} [parameters.clickedItemId] - Product id of the clicked recommendation + * @param {string} [parameters.clickedItemName] - Product name of the clicked recommendation + * @param {number} [parameters.resultPositionOnPage] - Position of the clicked item in the list + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User clicked a recommended product in the PIA widget + * @example + * constructorio.tracker.trackProductInsightsAgentRecommendationClick( + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'clickedItemId': '10', + * 'clickedItemName': 'rec1', + * 'resultPositionOnPage': 1, + * }, + * ); + */ + trackProductInsightsAgentRecommendationClick(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_recommendation_click?`; + const { + section, + itemId, + itemName, + variationId, + clickedItemId, + clickedItemName, + resultPositionOnPage, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + clicked_item_id: clickedItemId, + clicked_item_name: clickedItemName, + }; + + if (!helpers.isNil(resultPositionOnPage)) { + bodyParams.result_position_on_page = resultPositionOnPage; + } + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + /** * Subscribe to success or error messages emitted by tracking requests * diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index 4d07568f..1ed636c2 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -437,6 +437,30 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; + trackProductInsightsAgentRecommendationView( + parameters: { + itemId: string; + itemName: string; + items?: ItemTracked[]; + variationId?: string; + section?: string; + }, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentRecommendationClick( + parameters: { + itemId: string; + itemName: string; + clickedItemId?: string; + clickedItemName?: string; + resultPositionOnPage?: number; + variationId?: string; + section?: string; + }, + networkParameters?: NetworkParameters + ): true | Error; + trackMediaImpressionView(parameters: { bannerAdId: string; placementId: string; From b8d28bc53c77ddbb6f01a736241fe215033a406b Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Tue, 12 May 2026 19:13:40 +0500 Subject: [PATCH 02/10] [NO-REF] - add pia recommendations tracking --- spec/src/modules/tracker.js | 333 ++++++++++++++++++++++++++++++++++++ src/modules/tracker.js | 29 ++-- src/types/tracker.d.ts | 4 +- 3 files changed, 344 insertions(+), 22 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 8659f0b5..4c2171ee 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -15536,6 +15536,339 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); }); + describe('trackProductInsightsAgentRecommendationView', () => { + const requiredParameters = { itemId: '1', itemName: 'item1' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + items: [{ itemId: '10', itemName: 'rec1' }, { itemId: '20', itemName: 'rec2' }], + }; + + 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('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('items').to.be.an('array').with.lengthOf(2); + expect(bodyParams.items[0]).to.have.property('item_id').to.equal('10'); + expect(bodyParams.items[0]).to.have.property('item_name').to.equal('rec1'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationView( + { ...requiredParameters, ...optionalParameters }, + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentRecommendationView()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentRecommendationView()).to.be.an('error'); + }); + + 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) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); + }); + } + }); + + describe('trackProductInsightsAgentRecommendationClick', () => { + const requiredParameters = { itemId: '1', itemName: 'item1' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + position: 1, + }; + + 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('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('position').to.equal(optionalParameters.position); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationClick( + { ...requiredParameters, ...optionalParameters }, + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentRecommendationClick()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentRecommendationClick()).to.be.an('error'); + }); + + 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) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + // eslint-disable-next-line max-len + expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + }); + } + }); + describe('trackMediaImpressionView', () => { let requiredParameters; diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 8b04f62a..31a6ca86 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3759,12 +3759,10 @@ class Tracker { * * @function trackProductInsightsAgentRecommendationClick * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.itemId - Product id whose page we are on - * @param {string} parameters.itemName - Product name whose page we are on - * @param {string} [parameters.clickedItemId] - Product id of the clicked recommendation - * @param {string} [parameters.clickedItemName] - Product name of the clicked recommendation - * @param {number} [parameters.resultPositionOnPage] - Position of the clicked item in the list - * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} parameters.itemId - Product id of the clicked recommendation + * @param {string} parameters.itemName - Product name of the clicked recommendation + * @param {number} [parameters.position] - Position of the clicked item in the list + * @param {string} [parameters.variationId] - Variation id of the clicked recommendation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -3773,12 +3771,9 @@ class Tracker { * @example * constructorio.tracker.trackProductInsightsAgentRecommendationClick( * { - * 'itemId': '1', - * 'itemName': 'item1', - * 'variationId': '2', - * 'clickedItemId': '10', - * 'clickedItemName': 'rec1', - * 'resultPositionOnPage': 1, + * 'itemId': '10', + * 'itemName': 'rec1', + * 'position': 1, * }, * ); */ @@ -3791,21 +3786,17 @@ class Tracker { itemId, itemName, variationId, - clickedItemId, - clickedItemName, - resultPositionOnPage, + position, } = parameters; const queryParams = {}; const bodyParams = { item_id: itemId, item_name: itemName, variation_id: variationId, - clicked_item_id: clickedItemId, - clicked_item_name: clickedItemName, }; - if (!helpers.isNil(resultPositionOnPage)) { - bodyParams.result_position_on_page = resultPositionOnPage; + if (!helpers.isNil(position)) { + bodyParams.position = position; } if (section) { diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index 1ed636c2..803fa747 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -452,9 +452,7 @@ declare class Tracker { parameters: { itemId: string; itemName: string; - clickedItemId?: string; - clickedItemName?: string; - resultPositionOnPage?: number; + position?: number; variationId?: string; section?: string; }, From fa42da35a64ba89c9009f4829f39997c1abc73d7 Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Thu, 18 Jun 2026 18:13:05 +0500 Subject: [PATCH 03/10] Add tests and new click event --- spec/src/modules/tracker.js | 209 +++++------------------------------- src/modules/tracker.js | 138 +++++++++++------------- src/types/tracker.d.ts | 25 ++--- 3 files changed, 106 insertions(+), 266 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 4c2171ee..d9431cb1 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -13290,6 +13290,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const optionalParameters = { section: 'Products', variationId: '2', + threadId: 'thread-123', }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -13442,6 +13443,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -13451,7 +13453,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentView( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -14896,6 +14898,9 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { section: 'Products', variationId: '2', qnaResultId: '0daf0015-fc29-4727-9140-8d5313a1902c', + threadId: 'thread-123', + items: [{ itemId: 'rec1', itemName: 'Rec Product 1' }, { itemId: 'rec2', itemName: 'Rec Product 2' }], + followUpQuestions: [{ question: 'What about size?' }, { question: 'Is it machine washable?' }], }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -15050,6 +15055,12 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); expect(bodyParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + expect(bodyParams).to.have.property('items').to.be.an('array').with.lengthOf(2); + expect(bodyParams.items[0]).to.have.property('item_id').to.equal('rec1'); + expect(bodyParams.items[0]).to.have.property('item_name').to.equal('Rec Product 1'); + expect(bodyParams).to.have.property('follow_up_questions').to.be.an('array').with.lengthOf(2); + expect(bodyParams.follow_up_questions[0]).to.have.property('question').to.equal('What about size?'); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -15059,7 +15070,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentAnswerView( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -15220,6 +15231,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { section: 'Products', variationId: '2', qnaResultId: '0daf0015-fc29-4727-9140-8d5313a1902c', + threadId: 'thread-123', }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -15372,6 +15384,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -15381,7 +15394,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); expect(tracker.trackProductInsightsAgentAnswerFeedback( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -15536,179 +15549,14 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); }); - describe('trackProductInsightsAgentRecommendationView', () => { - const requiredParameters = { itemId: '1', itemName: 'item1' }; - const optionalParameters = { - section: 'Products', - variationId: '2', - items: [{ itemId: '10', itemName: 'rec1' }, { itemId: '20', itemName: 'rec2' }], - }; - - 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('item_id').to.equal(requiredParameters.itemId); - expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); - validateOriginReferrer(requestParams); - - // Response - expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message').to.equal('ok'); - - done(); - }); - - expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); - }); - - it('Should respond with a valid response when required and optional parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ - apiKey: testApiKey, - fetch: fetchSpy, - ...requestQueueOptions, - }); - - tracker.on('success', (responseParams) => { - const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); - const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); - // Request - expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); - expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); - expect(bodyParams).to.have.property('items').to.be.an('array').with.lengthOf(2); - expect(bodyParams.items[0]).to.have.property('item_id').to.equal('10'); - expect(bodyParams.items[0]).to.have.property('item_name').to.equal('rec1'); - - // Response - expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message').to.equal('ok'); - - done(); - }); - - expect(tracker.trackProductInsightsAgentRecommendationView( - { ...requiredParameters, ...optionalParameters }, - )).to.equal(true); - }); - - it('Should throw an error when no parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.trackProductInsightsAgentRecommendationView()).to.be.an('error'); - }); - - it('Should throw an error when invalid parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.trackProductInsightsAgentRecommendationView()).to.be.an('error'); - }); - - 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) => { - const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); - - // Request - expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('us').to.deep.equal(segments); - - // Response - expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message').to.equal('ok'); - - done(); - }); - - expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); - }); - - it('Should respond with a valid response when required parameters and userId are provided', (done) => { - const userId = 'user-id'; - const { tracker } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - ...requestQueueOptions, - }); - - tracker.on('success', (responseParams) => { - const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); - - // Request - expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('ui').to.equal(userId); - - // Response - expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message').to.equal('ok'); - - done(); - }); - - expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); - }); - - if (!skipNetworkTimeoutTests) { - it('Should be rejected when network request timeout is provided and reached', (done) => { - const { tracker } = new ConstructorIO({ - apiKey: testApiKey, - ...requestQueueOptions, - }); - - tracker.on('error', ({ message }) => { - expect(message).to.equal(timeoutRejectionMessage); - done(); - }); - - expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters, { timeout: 10 })).to.equal(true); - }); - - it('Should be rejected when global network request timeout is provided and reached', (done) => { - const { tracker } = new ConstructorIO({ - apiKey: testApiKey, - networkParameters: { - timeout: 20, - }, - ...requestQueueOptions, - }); - - tracker.on('error', ({ message }) => { - expect(message).to.equal(timeoutRejectionMessage); - done(); - }); - - expect(tracker.trackProductInsightsAgentRecommendationView(requiredParameters)).to.equal(true); - }); - } - }); - - describe('trackProductInsightsAgentRecommendationClick', () => { + describe('trackProductInsightsAgentResultClick', () => { const requiredParameters = { itemId: '1', itemName: 'item1' }; const optionalParameters = { section: 'Products', variationId: '2', position: 1, + threadId: 'thread-123', + qnaResultId: '0daf0015-fc29-4727-9140-8d5313a1902c', }; it('Should respond with a valid response when required parameters are provided', (done) => { @@ -15739,7 +15587,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters)).to.equal(true); }); it('Should respond with a valid response when required and optional parameters are provided', (done) => { @@ -15757,6 +15605,8 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); expect(bodyParams).to.have.property('position').to.equal(optionalParameters.position); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + expect(bodyParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -15765,7 +15615,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackProductInsightsAgentRecommendationClick( + expect(tracker.trackProductInsightsAgentResultClick( { ...requiredParameters, ...optionalParameters }, )).to.equal(true); }); @@ -15773,13 +15623,13 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.trackProductInsightsAgentRecommendationClick()).to.be.an('error'); + expect(tracker.trackProductInsightsAgentResultClick()).to.be.an('error'); }); it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.trackProductInsightsAgentRecommendationClick()).to.be.an('error'); + expect(tracker.trackProductInsightsAgentResultClick()).to.be.an('error'); }); it('Should respond with a valid response when required parameters and segments are provided', (done) => { @@ -15805,7 +15655,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters)).to.equal(true); }); it('Should respond with a valid response when required parameters and userId are provided', (done) => { @@ -15831,7 +15681,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters)).to.equal(true); }); if (!skipNetworkTimeoutTests) { @@ -15846,8 +15696,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - // eslint-disable-next-line max-len - expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters, { timeout: 10 })).to.equal(true); + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { timeout: 10 })).to.equal(true); }); it('Should be rejected when global network request timeout is provided and reached', (done) => { @@ -15864,7 +15713,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { done(); }); - expect(tracker.trackProductInsightsAgentRecommendationClick(requiredParameters)).to.equal(true); + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters)).to.equal(true); }); } }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 31a6ca86..52bc9155 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3164,6 +3164,7 @@ class Tracker { itemName, variationId, viewTimespans, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3174,6 +3175,10 @@ class Tracker { view_timespans: viewTimespans, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3237,6 +3242,7 @@ class Tracker { itemId, itemName, variationId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3246,6 +3252,10 @@ class Tracker { variation_id: variationId, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3302,6 +3312,7 @@ class Tracker { itemId, itemName, variationId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3310,6 +3321,10 @@ class Tracker { variation_id: variationId, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3366,6 +3381,7 @@ class Tracker { itemId, itemName, variationId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3374,6 +3390,10 @@ class Tracker { variation_id: variationId, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3433,6 +3453,7 @@ class Tracker { itemName, variationId, question, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3442,6 +3463,10 @@ class Tracker { question, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3501,6 +3526,7 @@ class Tracker { itemName, variationId, question, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3510,6 +3536,10 @@ class Tracker { question, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3575,6 +3605,9 @@ class Tracker { question, answerText, qnaResultId, + items, + followUpQuestions, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3586,6 +3619,18 @@ class Tracker { qna_result_id: qnaResultId, }; + if (items && Array.isArray(items)) { + bodyParams.items = items.slice(0, 100).map((item) => helpers.toSnakeCaseKeys(item, false)); + } + + if (followUpQuestions && Array.isArray(followUpQuestions)) { + bodyParams.follow_up_questions = followUpQuestions.map((q) => helpers.toSnakeCaseKeys(q, false)); + } + + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3648,6 +3693,7 @@ class Tracker { variationId, feedbackLabel, qnaResultId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3658,75 +3704,8 @@ class Tracker { qna_result_id: qnaResultId, }; - if (section) { - queryParams.section = section; - } - - const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; - const requestMethod = 'POST'; - const requestBody = applyParams(bodyParams, { - ...this.options, - requestMethod, - }); - this.requests.queue( - requestURL, - requestMethod, - requestBody, - networkParameters, - ); - this.requests.send(); - return true; - } - - this.requests.send(); - - return new Error('parameters is a required parameter of type object'); - } - - /** - * Send product insights agent recommendation view event - * - * @function trackProductInsightsAgentRecommendationView - * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.itemId - Product id whose page we are on - * @param {string} parameters.itemName - Product name whose page we are on - * @param {object[]} [parameters.items] - List of recommended product items displayed - * @param {string} [parameters.variationId] - Variation id whose page we are on - * @param {string} [parameters.section] - The section name for the item Ex. "Products" - * @param {object} [networkParameters] - Parameters relevant to the network request - * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) - * @returns {(true|Error)} - * @description User was shown product recommendations in the PIA widget - * @example - * constructorio.tracker.trackProductInsightsAgentRecommendationView( - * { - * 'itemId': '1', - * 'itemName': 'item1', - * 'variationId': '2', - * 'items': [{ 'itemId': '10', 'itemName': 'rec1' }], - * }, - * ); - */ - trackProductInsightsAgentRecommendationView(parameters, networkParameters = {}) { - // Ensure parameters are provided (required) - if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { - const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_recommendation_view?`; - const { - section, - itemId, - itemName, - variationId, - items, - } = parameters; - const queryParams = {}; - const bodyParams = { - item_id: itemId, - item_name: itemName, - variation_id: variationId, - }; - - if (items && Array.isArray(items)) { - bodyParams.items = items.slice(0, 100).map((item) => helpers.toSnakeCaseKeys(item, false)); + if (threadId) { + bodyParams.thread_id = threadId; } if (section) { @@ -3755,21 +3734,22 @@ class Tracker { } /** - * Send product insights agent recommendation click event + * Send product insights agent result click event * - * @function trackProductInsightsAgentRecommendationClick + * @function trackProductInsightsAgentResultClick * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.itemId - Product id of the clicked recommendation * @param {string} parameters.itemName - Product name of the clicked recommendation * @param {number} [parameters.position] - Position of the clicked item in the list * @param {string} [parameters.variationId] - Variation id of the clicked recommendation + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} * @description User clicked a recommended product in the PIA widget * @example - * constructorio.tracker.trackProductInsightsAgentRecommendationClick( + * constructorio.tracker.trackProductInsightsAgentResultClick( * { * 'itemId': '10', * 'itemName': 'rec1', @@ -3777,16 +3757,18 @@ class Tracker { * }, * ); */ - trackProductInsightsAgentRecommendationClick(parameters, networkParameters = {}) { + trackProductInsightsAgentResultClick(parameters, networkParameters = {}) { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { - const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_recommendation_click?`; + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_result_click?`; const { section, itemId, itemName, variationId, position, + qnaResultId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3799,6 +3781,14 @@ class Tracker { bodyParams.position = position; } + if (qnaResultId) { + bodyParams.qna_result_id = qnaResultId; + } + + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index 803fa747..bc44253b 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -354,6 +354,7 @@ declare class Tracker { itemName: string; viewTimespans: TimeSpan[]; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -365,6 +366,7 @@ declare class Tracker { itemId: string; itemName: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -375,6 +377,7 @@ declare class Tracker { itemId: string; itemName: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -385,6 +388,7 @@ declare class Tracker { itemId: string; itemName: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -396,6 +400,7 @@ declare class Tracker { itemName: string; question: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -407,6 +412,7 @@ declare class Tracker { itemName: string; question: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -419,7 +425,10 @@ declare class Tracker { question: string; answerText: string; qnaResultId?: string; + items?: ItemTracked[]; + followUpQuestions?: Array<{ question: string }>; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -432,28 +441,20 @@ declare class Tracker { feedbackLabel: string; qnaResultId?: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters ): true | Error; - trackProductInsightsAgentRecommendationView( - parameters: { - itemId: string; - itemName: string; - items?: ItemTracked[]; - variationId?: string; - section?: string; - }, - networkParameters?: NetworkParameters - ): true | Error; - - trackProductInsightsAgentRecommendationClick( + trackProductInsightsAgentResultClick( parameters: { itemId: string; itemName: string; position?: number; + qnaResultId?: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters From 1da9c95ba4ac6239909f33a16eb4f040c64a536b Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Thu, 18 Jun 2026 18:24:45 +0500 Subject: [PATCH 04/10] address review --- src/modules/tracker.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 52bc9155..9bae5589 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3124,6 +3124,7 @@ class Tracker { * @param {array.<{start: string | undefined, * end: string | undefined}>} parameters.viewTimespans - List of timestamp pairs in ISO_8601 format * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -3213,6 +3214,7 @@ class Tracker { * @param {string} parameters.itemId - Product id whose page we are on * @param {string} parameters.itemName - Product name whose page we are on * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -3289,6 +3291,7 @@ class Tracker { * @param {string} parameters.itemId - Product id whose page we are on * @param {string} parameters.itemName - Product name whose page we are on * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -3358,6 +3361,7 @@ class Tracker { * @param {string} parameters.itemId - Product id whose page we are on * @param {string} parameters.itemName - Product name whose page we are on * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -3428,6 +3432,7 @@ class Tracker { * @param {string} parameters.itemName - Product name whose page we are on * @param {string} parameters.question - Question a user clicked on * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -3501,6 +3506,7 @@ class Tracker { * @param {string} parameters.itemName - Product name whose page we are on * @param {string} parameters.question - Question a user submitted * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) @@ -3575,14 +3581,17 @@ class Tracker { * @param {string} parameters.question - Question a user submitted * @param {string} parameters.answerText - Answer text of the question * @param {string} [parameters.qnaResultId] - Answer result id returned + * @param {Array} [parameters.items] - Array of recommended items shown with the answer (max 100) + * @param {Array} [parameters.followUpQuestions] - Follow-up questions displayed with the answer * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} * @description User viewed the answer provided by the product insights agent * @example - * constructorio.tracker.trackProductInsightsAgentAnswerView({ + * constructorio.tracker.trackProductInsightsAgentAnswerView( * { * 'itemId': '1', * 'itemName': 'item1', @@ -3666,13 +3675,14 @@ class Tracker { * @param {string} parameters.feedbackLabel - Feedback value: either "thumbs_up" or "thumbs_down" * @param {string} [parameters.qnaResultId] - Answer result id returned * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} * @description A user provided feedback on an answers usefulness * @example - * constructorio.tracker.trackProductInsightsAgentAnswerFeedback({ + * constructorio.tracker.trackProductInsightsAgentAnswerFeedback( * { * 'itemId': '1', * 'itemName': 'item1', From cdae1cd07e601640b79ba81dfe2c46fab2f24fd6 Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Thu, 18 Jun 2026 18:29:40 +0500 Subject: [PATCH 05/10] another review --- src/modules/tracker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 9bae5589..3657ba70 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3751,6 +3751,7 @@ class Tracker { * @param {string} parameters.itemId - Product id of the clicked recommendation * @param {string} parameters.itemName - Product name of the clicked recommendation * @param {number} [parameters.position] - Position of the clicked item in the list + * @param {string} [parameters.qnaResultId] - Answer result id linking the click to a specific answer * @param {string} [parameters.variationId] - Variation id of the clicked recommendation * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation * @param {string} [parameters.section] - The section name for the item Ex. "Products" From 3fa607b74216dac348f42f4199da545653be0361 Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Thu, 18 Jun 2026 18:32:33 +0500 Subject: [PATCH 06/10] fix copilot review --- 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 3657ba70..3df9498e 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3633,7 +3633,7 @@ class Tracker { } if (followUpQuestions && Array.isArray(followUpQuestions)) { - bodyParams.follow_up_questions = followUpQuestions.map((q) => helpers.toSnakeCaseKeys(q, false)); + bodyParams.follow_up_questions = followUpQuestions.slice(0, 100).map((q) => helpers.toSnakeCaseKeys(q, false)); } if (threadId) { From 0cda737d4b581b43eef30fb95e0da01663ae9c75 Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Tue, 30 Jun 2026 23:41:03 +0500 Subject: [PATCH 07/10] Add more fields --- spec/src/modules/tracker.js | 10 ++++++++-- src/modules/tracker.js | 24 ++++++++++++++++++++++++ src/types/tracker.d.ts | 6 +++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 53adc5d3..7272ed33 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -15623,7 +15623,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { qnaResultId: '0daf0015-fc29-4727-9140-8d5313a1902c', threadId: 'thread-123', items: [{ itemId: 'rec1', itemName: 'Rec Product 1' }, { itemId: 'rec2', itemName: 'Rec Product 2' }], - followUpQuestions: [{ question: 'What about size?' }, { question: 'Is it machine washable?' }], + followUpQuestions: [{ value: 'What about size?' }, { value: 'Is it machine washable?' }], }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -16273,11 +16273,13 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); describe('trackProductInsightsAgentResultClick', () => { - const requiredParameters = { itemId: '1', itemName: 'item1' }; + const requiredParameters = { itemId: '1', itemName: 'item1', question: 'Why choose this?', seedItemId: 'seed-1' }; const optionalParameters = { section: 'Products', variationId: '2', position: 1, + seedItemName: 'Seed Product', + seedVariationId: 'seed-var-1', threadId: 'thread-123', qnaResultId: '0daf0015-fc29-4727-9140-8d5313a1902c', }; @@ -16301,6 +16303,8 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(requestParams).to.have.property('_dt'); expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + expect(requestParams).to.have.property('seed_item_id').to.equal(requiredParameters.seedItemId); validateOriginReferrer(requestParams); // Response @@ -16328,6 +16332,8 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); expect(bodyParams).to.have.property('position').to.equal(optionalParameters.position); + expect(bodyParams).to.have.property('seed_item_name').to.equal(optionalParameters.seedItemName); + expect(bodyParams).to.have.property('seed_variation_id').to.equal(optionalParameters.seedVariationId); expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); expect(bodyParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index df8ce04c..53205ff8 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3909,7 +3909,11 @@ class Tracker { * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.itemId - Product id of the clicked recommendation * @param {string} parameters.itemName - Product name of the clicked recommendation + * @param {string} parameters.question - Question that produced the answer containing this recommendation + * @param {string} parameters.seedItemId - Product id of the page the PIA widget is on * @param {number} [parameters.position] - Position of the clicked item in the list + * @param {string} [parameters.seedItemName] - Product name of the page the PIA widget is on + * @param {string} [parameters.seedVariationId] - Variation id of the page the PIA widget is on * @param {string} [parameters.qnaResultId] - Answer result id linking the click to a specific answer * @param {string} [parameters.variationId] - Variation id of the clicked recommendation * @param {string} [parameters.threadId] - Thread ID for grouping events within a conversation @@ -3937,6 +3941,10 @@ class Tracker { itemName, variationId, position, + question, + seedItemId, + seedItemName, + seedVariationId, qnaResultId, threadId, } = parameters; @@ -3951,6 +3959,22 @@ class Tracker { bodyParams.position = position; } + if (question) { + bodyParams.question = question; + } + + if (seedItemId) { + bodyParams.seed_item_id = seedItemId; + } + + if (seedItemName) { + bodyParams.seed_item_name = seedItemName; + } + + if (seedVariationId) { + bodyParams.seed_variation_id = seedVariationId; + } + if (qnaResultId) { bodyParams.qna_result_id = qnaResultId; } diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index c2bcc778..ff777078 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -435,7 +435,7 @@ declare class Tracker { answerText: string; qnaResultId?: string; items?: ItemTracked[]; - followUpQuestions?: Array<{ question: string }>; + followUpQuestions?: Array<{ value: string }>; variationId?: string; threadId?: string; section?: string; @@ -460,7 +460,11 @@ declare class Tracker { parameters: { itemId: string; itemName: string; + question: string; + seedItemId: string; position?: number; + seedItemName?: string; + seedVariationId?: string; qnaResultId?: string; variationId?: string; threadId?: string; From 0c2776fb1c1b1e64ef80a4adb7e15735ca852552 Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Wed, 1 Jul 2026 20:26:48 +0500 Subject: [PATCH 08/10] Fix 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 7272ed33..26894fbe 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -15783,7 +15783,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(bodyParams.items[0]).to.have.property('item_id').to.equal('rec1'); expect(bodyParams.items[0]).to.have.property('item_name').to.equal('Rec Product 1'); expect(bodyParams).to.have.property('follow_up_questions').to.be.an('array').with.lengthOf(2); - expect(bodyParams.follow_up_questions[0]).to.have.property('question').to.equal('What about size?'); + expect(bodyParams.follow_up_questions[0]).to.have.property('value').to.equal('What about size?'); // Response expect(responseParams).to.have.property('method').to.equal('POST'); From d1e2553ceac4809a4e90162fb67c821e775b229f Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Thu, 2 Jul 2026 16:16:43 +0500 Subject: [PATCH 09/10] Address review --- spec/src/modules/tracker.js | 12 +++++++++++- src/modules/tracker.js | 16 ++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 26894fbe..9b244445 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -13683,6 +13683,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const optionalParameters = { section: 'Products', variationId: '2', + threadId: 'thread-123', }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -13836,6 +13837,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -14336,6 +14338,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const optionalParameters = { section: 'Products', variationId: '2', + threadId: 'thread-123', }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -14489,6 +14492,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -14658,6 +14662,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const optionalParameters = { section: 'Products', variationId: '2', + threadId: 'thread-123', }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -14809,6 +14814,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -14978,6 +14984,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const optionalParameters = { section: 'Products', variationId: '2', + threadId: 'thread-123', }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -15130,6 +15137,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -15299,6 +15307,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const optionalParameters = { section: 'Products', variationId: '2', + threadId: 'thread-123', }; it('Should respond with a valid response when term and required parameters are provided', (done) => { @@ -15451,6 +15460,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(fetchSpy).to.have.been.called; expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); // Response expect(responseParams).to.have.property('method').to.equal('POST'); @@ -16358,7 +16368,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.trackProductInsightsAgentResultClick()).to.be.an('error'); + expect(tracker.trackProductInsightsAgentResultClick('invalid')).to.be.an('error'); }); it('Should respond with a valid response when required parameters and segments are provided', (done) => { diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 53205ff8..30871cfa 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3788,11 +3788,11 @@ class Tracker { }; if (items && Array.isArray(items)) { - bodyParams.items = items.slice(0, 100).map((item) => helpers.toSnakeCaseKeys(item, false)); + bodyParams.items = items.map((item) => helpers.toSnakeCaseKeys(item, false)); } if (followUpQuestions && Array.isArray(followUpQuestions)) { - bodyParams.follow_up_questions = followUpQuestions.slice(0, 100).map((q) => helpers.toSnakeCaseKeys(q, false)); + bodyParams.follow_up_questions = followUpQuestions.map((q) => helpers.toSnakeCaseKeys(q, false)); } if (threadId) { @@ -3927,6 +3927,8 @@ class Tracker { * { * 'itemId': '10', * 'itemName': 'rec1', + * 'question': 'Why choose this?', + * 'seedItemId': '1', * 'position': 1, * }, * ); @@ -3953,20 +3955,14 @@ class Tracker { item_id: itemId, item_name: itemName, variation_id: variationId, + question, + seed_item_id: seedItemId, }; if (!helpers.isNil(position)) { bodyParams.position = position; } - if (question) { - bodyParams.question = question; - } - - if (seedItemId) { - bodyParams.seed_item_id = seedItemId; - } - if (seedItemName) { bodyParams.seed_item_name = seedItemName; } From 6c1c686c08594cc678f9cb52c92a973c50c0617b Mon Sep 17 00:00:00 2001 From: Vlad Saitov Date: Thu, 2 Jul 2026 16:25:10 +0500 Subject: [PATCH 10/10] Update types --- src/types/index.d.ts | 4 ++++ src/types/tracker.d.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/types/index.d.ts b/src/types/index.d.ts index bd093e43..f59aefa3 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -315,6 +315,10 @@ export interface Question { question: string; } +export interface FollowUpQuestion { + value: string; +} + export interface TimeSpan { start: string; end: string; diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index ff777078..80433441 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -1,4 +1,4 @@ -import { ConstructorClientOptions, ItemTracked, ItemTrackedPurchase, NetworkParameters, Question, TimeSpan } from '.'; +import { ConstructorClientOptions, FollowUpQuestion, ItemTracked, ItemTrackedPurchase, NetworkParameters, Question, TimeSpan } from '.'; import { EventEmitter } from './events'; import RequestQueue from './request-queue'; @@ -435,7 +435,7 @@ declare class Tracker { answerText: string; qnaResultId?: string; items?: ItemTracked[]; - followUpQuestions?: Array<{ value: string }>; + followUpQuestions?: FollowUpQuestion[]; variationId?: string; threadId?: string; section?: string;