diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index b1351f9c..53adc5d3 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -14013,6 +14013,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) => { @@ -14165,6 +14166,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'); @@ -15619,6 +15621,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) => { @@ -15773,6 +15778,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'); @@ -15943,6 +15954,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) => { @@ -16095,6 +16107,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'); @@ -16259,6 +16272,175 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); }); + 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) => { + 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.trackProductInsightsAgentResultClick(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); + 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'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick( + { ...requiredParameters, ...optionalParameters }, + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + 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.trackProductInsightsAgentResultClick()).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.trackProductInsightsAgentResultClick(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.trackProductInsightsAgentResultClick(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.trackProductInsightsAgentResultClick(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.trackProductInsightsAgentResultClick(requiredParameters)).to.equal(true); + }); + } + }); + describe('trackResultsImpressionView', () => { const requiredParameters = { items: [ diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 69297821..df8ce04c 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -3283,6 +3283,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) @@ -3323,6 +3324,7 @@ class Tracker { itemName, variationId, viewTimespans, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3333,6 +3335,10 @@ class Tracker { view_timespans: viewTimespans, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3367,6 +3373,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) @@ -3396,6 +3403,7 @@ class Tracker { itemId, itemName, variationId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3405,6 +3413,10 @@ class Tracker { variation_id: variationId, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3438,6 +3450,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) @@ -3461,6 +3474,7 @@ class Tracker { itemId, itemName, variationId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3469,6 +3483,10 @@ class Tracker { variation_id: variationId, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3502,6 +3520,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) @@ -3525,6 +3544,7 @@ class Tracker { itemId, itemName, variationId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3533,6 +3553,10 @@ class Tracker { variation_id: variationId, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3567,6 +3591,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) @@ -3592,6 +3617,7 @@ class Tracker { itemName, variationId, question, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3601,6 +3627,10 @@ class Tracker { question, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3635,6 +3665,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) @@ -3660,6 +3691,7 @@ class Tracker { itemName, variationId, question, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3669,6 +3701,10 @@ class Tracker { question, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3704,14 +3740,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', @@ -3734,6 +3773,9 @@ class Tracker { question, answerText, qnaResultId, + items, + followUpQuestions, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3745,6 +3787,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.slice(0, 100).map((q) => helpers.toSnakeCaseKeys(q, false)); + } + + if (threadId) { + bodyParams.thread_id = threadId; + } + if (section) { queryParams.section = section; } @@ -3780,13 +3834,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', @@ -3807,6 +3862,7 @@ class Tracker { variationId, feedbackLabel, qnaResultId, + threadId, } = parameters; const queryParams = {}; const bodyParams = { @@ -3817,6 +3873,92 @@ class Tracker { qna_result_id: qnaResultId, }; + if (threadId) { + bodyParams.thread_id = threadId; + } + + 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 result click event + * + * @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.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" + * @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.trackProductInsightsAgentResultClick( + * { + * 'itemId': '10', + * 'itemName': 'rec1', + * 'position': 1, + * }, + * ); + */ + 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_result_click?`; + const { + section, + itemId, + itemName, + variationId, + position, + qnaResultId, + threadId, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + }; + + if (!helpers.isNil(position)) { + 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 9cbd7c43..c2bcc778 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -363,6 +363,7 @@ declare class Tracker { itemName: string; viewTimespans: TimeSpan[]; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -374,6 +375,7 @@ declare class Tracker { itemId: string; itemName: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -384,6 +386,7 @@ declare class Tracker { itemId: string; itemName: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -394,6 +397,7 @@ declare class Tracker { itemId: string; itemName: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -405,6 +409,7 @@ declare class Tracker { itemName: string; question: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -416,6 +421,7 @@ declare class Tracker { itemName: string; question: string; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -428,7 +434,10 @@ declare class Tracker { question: string; answerText: string; qnaResultId?: string; + items?: ItemTracked[]; + followUpQuestions?: Array<{ question: string }>; variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters @@ -441,6 +450,20 @@ declare class Tracker { feedbackLabel: string; qnaResultId?: string; variationId?: string; + threadId?: string; + section?: string; + }, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentResultClick( + parameters: { + itemId: string; + itemName: string; + position?: number; + qnaResultId?: string; + variationId?: string; + threadId?: string; section?: string; }, networkParameters?: NetworkParameters