diff --git a/cspell.json b/cspell.json index 1ec8fb2d..dbf515b4 100644 --- a/cspell.json +++ b/cspell.json @@ -46,6 +46,7 @@ "forcedelta", "forcepatchdelta", "skippable", - "timeofday" + "timeofday", + "timespans" ] } diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 50163b80..23dfdebc 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -8292,6 +8292,5612 @@ describe('ConstructorIO - Tracker', () => { }); }); + describe('trackProductInsightsAgentAnswerFeedback', () => { + const requiredParameters = { + feedbackLabel: 'thumbs_up', + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + qnaResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + const snakeCaseParameters = { + feedback_label: 'thumbs_down', + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + qna_result_id: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + }; + + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('feedback_label').to.equal(snakeCaseParameters.feedback_label); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('qna_result_id').to.equal(snakeCaseParameters.qna_result_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_answer_feedback?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('feedback_label').to.equal(requiredParameters.feedbackLabel); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when feedbackLabel and itemName are provided', (done) => { + const parameters = { + feedbackLabel: 'thumbs_down', + itemName: optionalParameters.itemName, + }; + 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('feedback_label').to.equal(parameters.feedbackLabel); + expect(requestParams).to.have.property('item_name').to.equal(parameters.itemName); + expect(requestParams).to.not.have.property('item_id'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(parameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('feedback_label').to.equal(requiredParameters.feedbackLabel); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(null, userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no feedbackLabel is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + { itemId: requiredParameters.itemId }, + userParameters, + )).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentAnswerFeedback( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentAnswerView', () => { + const requiredParameters = { + question: 'Is this product machine washable?', + answerText: 'Yes, it can be machine washed at 30 degrees.', + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + questionTopic: 'care', + qnaResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + question: 'Is this product machine washable?', + answer_text: 'Yes, it can be machine washed at 30 degrees.', + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + question_topic: 'care', + qna_result_id: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + follow_up_questions: [{ value: 'What sizes are available?' }], + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(snakeCaseParameters.question); + expect(requestParams).to.have.property('answer_text').to.equal(snakeCaseParameters.answer_text); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('question_topic').to.equal(snakeCaseParameters.question_topic); + expect(requestParams).to.have.property('qna_result_id').to.equal(snakeCaseParameters.qna_result_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + expect(requestParams) + .to.have.property('follow_up_questions') + .to.deep.equal(snakeCaseParameters.follow_up_questions); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_answer_view?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + expect(requestParams).to.have.property('answer_text').to.equal(requiredParameters.answerText); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('question_topic').to.equal(optionalParameters.questionTopic); + expect(requestParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and items are provided', (done) => { + const items = [ + { itemId: 'product-1', itemName: 'Product One', variationId: 'variation-1' }, + { itemId: 'product-2', slCampaignId: 'campaign-id', slCampaignOwner: 'campaign-owner' }, + ]; + 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('items').to.deep.equal([ + { item_id: 'product-1', item_name: 'Product One', variation_id: 'variation-1' }, + { item_id: 'product-2', sl_campaign_id: 'campaign-id', sl_campaign_owner: 'campaign-owner' }, + ]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, items }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response and send at most 100 items when more items are provided', (done) => { + const items = Array.from({ length: 101 }, (_, index) => ({ itemId: `product-${index}` })); + 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('items').to.have.lengthOf(100); + expect(requestParams.items[99]).to.deep.equal({ item_id: 'product-99' }); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, items }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and followUpQuestions are provided', (done) => { + const followUpQuestions = [{ value: 'What sizes are available?' }, { value: 'Is it available in blue?' }]; + 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('follow_up_questions').to.deep.equal(followUpQuestions); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, followUpQuestions }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response and send at most 100 followUpQuestions when more are provided', (done) => { + const followUpQuestions = Array.from({ length: 101 }, (_, index) => ({ value: `Question ${index}` })); + 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('follow_up_questions').to.have.lengthOf(100); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, followUpQuestions }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when an empty answerText is 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.have.property('answer_text').to.equal(''); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, answerText: '' }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerView([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerView(null, userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no question is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, question: undefined }, + userParameters, + )).to.be.an('error'); + }); + + it('Should throw an error when no answerText is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, answerText: undefined }, + userParameters, + )).to.be.an('error'); + }); + + it('Should throw an error when answerText is not a string', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerView( + { ...requiredParameters, answerText: 123 }, + userParameters, + )).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentAnswerView( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentAnswerView( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentOutOfView', () => { + const requiredParameters = { + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_out_of_view?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentOutOfView([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentOutOfView(null, userParameters)).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentOutOfView( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentOutOfView( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentFocus', () => { + const requiredParameters = { + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_focus?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentFocus([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentFocus(null, userParameters)).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentFocus( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentFocus( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentQuestionClick', () => { + const requiredParameters = { + question: 'Is this product machine washable?', + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + questionTopic: 'care', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + question: 'Is this product machine washable?', + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + question_topic: 'care', + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(snakeCaseParameters.question); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + expect(requestParams).to.have.property('question_topic').to.equal(snakeCaseParameters.question_topic); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_question_click?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + expect(requestParams).to.have.property('question_topic').to.equal(optionalParameters.questionTopic); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionClick([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionClick(null, userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no question is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + { ...requiredParameters, question: undefined }, + userParameters, + )).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentQuestionClick( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentQuestionSubmit', () => { + const requiredParameters = { + question: 'Is this product machine washable?', + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + questionTopic: 'care', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + question: 'Is this product machine washable?', + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + question_topic: 'care', + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(snakeCaseParameters.question); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + expect(requestParams).to.have.property('question_topic').to.equal(snakeCaseParameters.question_topic); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_question_submit?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + expect(requestParams).to.have.property('question_topic').to.equal(optionalParameters.questionTopic); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(null, userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no question is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + { ...requiredParameters, question: undefined }, + userParameters, + )).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentQuestionSubmit( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentResultClick', () => { + const requiredParameters = { + question: 'Is there a similar product in blue?', + seedItemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + itemId: 'recommended-item-id', + }; + const optionalParameters = { + seedItemName: 'Example Product Name', + seedVariationId: 'product-variation', + itemName: 'Recommended Product Name', + variationId: 'recommended-variation-id', + position: 3, + qnaResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + question: 'Is there a similar product in blue?', + seed_item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + seed_item_name: 'Example Product Name', + seed_variation_id: 'product-variation', + item_id: 'recommended-item-id', + item_name: 'Recommended Product Name', + variation_id: 'recommended-variation-id', + qna_result_id: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(snakeCaseParameters.question); + expect(requestParams).to.have.property('seed_item_id').to.equal(snakeCaseParameters.seed_item_id); + expect(requestParams).to.have.property('seed_item_name').to.equal(snakeCaseParameters.seed_item_name); + expect(requestParams).to.have.property('seed_variation_id').to.equal(snakeCaseParameters.seed_variation_id); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('qna_result_id').to.equal(snakeCaseParameters.qna_result_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_result_click?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + expect(requestParams).to.have.property('seed_item_id').to.equal(requiredParameters.seedItemId); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('seed_item_name').to.equal(optionalParameters.seedItemName); + expect(requestParams).to.have.property('seed_variation_id').to.equal(optionalParameters.seedVariationId); + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('position').to.equal(optionalParameters.position); + expect(requestParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + + // 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 }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when position is 0', (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.have.property('position').to.equal(0); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick( + { ...requiredParameters, position: 0 }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentResultClick([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentResultClick(null, userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no question is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentResultClick( + { ...requiredParameters, question: undefined }, + userParameters, + )).to.be.an('error'); + }); + + it('Should throw an error when no seedItemId is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentResultClick( + { ...requiredParameters, seedItemId: undefined }, + userParameters, + )).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentResultClick( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentResultClick(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentResultClick( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentView', () => { + const requiredParameters = { + questions: [{ question: 'Is this product machine washable?', questionTopic: 'care' }], + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + questions: [{ question: 'Is this product machine washable?', question_topic: 'care' }], + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('questions').to.deep.equal(snakeCaseParameters.questions); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_view?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('questions').to.deep.equal([ + { question: 'Is this product machine washable?', question_topic: 'care' }, + ]); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentView([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentView(null, userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no questions are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentView( + { ...requiredParameters, questions: undefined }, + userParameters, + )).to.be.an('error'); + }); + + it('Should throw an error when questions is not an array', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentView( + { ...requiredParameters, questions: 'not-an-array' }, + userParameters, + )).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentView( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentView( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentViews', () => { + const requiredParameters = { + questions: [{ question: 'Is this product machine washable?', questionTopic: 'care' }], + viewTimespans: [{ start: '2026-10-05T10:00:00.000Z', end: '2026-10-05T10:00:05.000Z' }], + itemId: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + }; + const optionalParameters = { + itemName: 'Example Product Name', + variationId: 'product-variation', + threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Backwards Compatibility - Should respond with a valid response when snake cased parameters are provided', (done) => { + const snakeCaseParameters = { + questions: [{ question: 'Is this product machine washable?', question_topic: 'care' }], + view_timespans: [{ start: '2026-10-05T10:00:00.000Z', end: '2026-10-05T10:00:05.000Z' }], + item_id: 'product0dbae320-3950-11ea-9251-8dee6d0eb3cd-new', + item_name: 'Example Product Name', + variation_id: 'product-variation', + thread_id: '0daf0015-fc29-4727-9140-8d5313a1902c', + feature_variants: { foo: 'bar' }, + analytics_tags: { foo: 'bar' }, + }; + 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('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('beacon').to.equal(true); + expect(requestParams).to.have.property('questions').to.deep.equal(snakeCaseParameters.questions); + expect(requestParams).to.have.property('view_timespans').to.deep.equal(snakeCaseParameters.view_timespans); + expect(requestParams).to.have.property('item_id').to.equal(snakeCaseParameters.item_id); + expect(requestParams).to.have.property('item_name').to.equal(snakeCaseParameters.item_name); + expect(requestParams).to.have.property('variation_id').to.equal(snakeCaseParameters.variation_id); + expect(requestParams).to.have.property('thread_id').to.equal(snakeCaseParameters.thread_id); + expect(requestParams) + .to.have.property('feature_variants') + .to.deep.equal(snakeCaseParameters.feature_variants); + expect(requestParams).to.have.property('analytics_tags').to.deep.equal(snakeCaseParameters.analytics_tags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(snakeCaseParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = helpers.extractUrlFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestUrl).to.include('/v2/behavioral_action/product_insights_agent_views?'); + 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('beacon').to.equal(true); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('questions').to.deep.equal([ + { question: 'Is this product machine washable?', question_topic: 'care' }, + ]); + expect(requestParams).to.have.property('view_timespans').to.deep.equal(requiredParameters.viewTimespans); + expect(requestQueryParams).to.have.property('key'); + expect(requestQueryParams).to.have.property('i'); + expect(requestQueryParams).to.have.property('s'); + expect(requestQueryParams).to.have.property('c').to.equal(clientVersion); + expect(requestQueryParams).to.have.property('_dt'); + expect(requestQueryParams).to.have.property('beacon').to.equal('true'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, userParameters)).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, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('item_name').to.equal(optionalParameters.itemName); + expect(requestParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(requestParams).to.have.property('thread_id').to.equal(optionalParameters.threadId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, ...optionalParameters }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and section are provided', (done) => { + const section = 'Products'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('section').to.equal(section); + expect(requestParams).to.not.have.property('section'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, section }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters, features and featureVariants are provided', (done) => { + const features = { foo: true }; + const featureVariants = { foo: 'bar' }; + 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('features').to.deep.equal(features); + expect(requestParams).to.have.property('feature_variants').to.deep.equal(featureVariants); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, features, featureVariants }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and analyticsTags are provided', (done) => { + const analyticsTags = { foo: 'bar' }; + 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('analytics_tags').to.deep.equal(analyticsTags); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, analyticsTags }, + userParameters, + )).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user identifier are provided', (done) => { + const userId = 'bd2d9d1f097614c4b4de'; + 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('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.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + userId, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + 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.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + segments, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and test cells are provided', (done) => { + const testCells = { foo: 'bar' }; + 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(`ef-${Object.keys(testCells)[0]}`) + .to.equal(Object.values(testCells)[0]); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + testCells, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and origin referrer are provided', (done) => { + const originReferrer = 'https://localhost'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('origin_referrer').to.equal(originReferrer); + expect(requestParams).to.have.property('origin_referrer').to.equal(originReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + originReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and document referrer are provided', (done) => { + const documentReferrer = 'https://localhost/previous-page'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('document_referrer').to.equal(documentReferrer); + expect(requestParams).to.have.property('document_referrer').to.equal(documentReferrer); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + documentReferrer, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and canonical url are provided', (done) => { + const canonicalUrl = 'https://localhost/product'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestQueryParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestQueryParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + canonicalUrl, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and security token are provided', (done) => { + const securityToken = '5219c4c62f24e9b39ef92979'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + securityToken, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('x-cnstrc-token').to.equal(securityToken); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, userParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user ip are provided', (done) => { + const userIp = '127.0.0.1'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Forwarded-For').to.equal(userIp); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + userIp, + })).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and user agent are provided', (done) => { + const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('User-Agent').to.equal(userAgent); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + userAgent, + })).to.equal(true); + }); + + it('Should respond with a valid response with dateTime', (done) => { + const dateTime = 123456789; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + tracker.on('success', (responseParams) => { + const requestedParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedParams).to.have.property('_dt').to.equal(dateTime.toString()); + expect(requestedBody).to.have.property('_dt').to.equal(dateTime); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { + ...userParameters, + dateTime, + })).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews([], userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews(null, userParameters)).to.be.an('error'); + }); + + it('Should throw an error when no questions are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, questions: undefined }, + userParameters, + )).to.be.an('error'); + }); + + it('Should throw an error when questions is not an array', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, questions: 'not-an-array' }, + userParameters, + )).to.be.an('error'); + }); + + it('Should throw an error when no viewTimespans are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, viewTimespans: undefined }, + userParameters, + )).to.be.an('error'); + }); + + it('Should throw an error when viewTimespans is not an array', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews( + { ...requiredParameters, viewTimespans: 'not-an-array' }, + userParameters, + )).to.be.an('error'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should throw an error when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentViews( + requiredParameters, + userParameters, + { timeout: 10 }, + )).to.equal(true); + }); + + it('Should throw an error when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { timeout: 20 }, + }); + + tracker.on('error', () => { done(); }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, userParameters)).to.equal(true); + }); + } + + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + 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('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.trackProductInsightsAgentViews( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = ' '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + 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('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews( + requiredParameters, + { ...userParameters, userId }, + )).to.equal(true); + }); + }); + describe('PII Detection', () => { const requiredParameters = { originalQuery: 'original-query' }; diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 4cf0930e..0974a875 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -5,6 +5,10 @@ const { AbortController } = require('node-abort-controller'); const EventEmitter = require('events'); const helpers = require('../utils/helpers'); +// Maps provided userParameters and options to the fields required by the API +// These are sent in query parameters on every event, and additionally within the body on POST events +// Fields are added onto the passed parameters object, mutating it in place +// A cleaned copy is what gets returned, so the returned object is not the one that was passed in function applyParams(parameters, userParameters, options) { const { apiKey, @@ -17,6 +21,8 @@ function applyParams(parameters, userParameters, options) { segments, testCells, originReferrer, + documentReferrer, + canonicalUrl, dateTime, } = userParameters || {}; let aggregateParams = Object.assign(parameters); @@ -65,6 +71,14 @@ function applyParams(parameters, userParameters, options) { aggregateParams.origin_referrer = originReferrer; } + if (documentReferrer) { + aggregateParams.document_referrer = documentReferrer; + } + + if (canonicalUrl) { + aggregateParams.canonical_url = canonicalUrl; + } + aggregateParams._dt = dateTime || Date.now(); aggregateParams.beacon = true; aggregateParams = helpers.cleanParams(aggregateParams); @@ -200,11 +214,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -241,11 +257,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -290,11 +308,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -391,11 +411,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -493,11 +515,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -587,11 +611,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -714,11 +740,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -830,11 +858,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -965,11 +995,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -1069,11 +1101,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -1211,11 +1245,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -1374,11 +1410,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -1527,11 +1565,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -1675,11 +1715,13 @@ class Tracker { * @param {string[]} [userParameters.segments] - User segments * @param {object} [userParameters.testCells] - User test cells * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page * @param {string} [userParameters.referer] - Client page URL (including path) * @param {string} [userParameters.userIp] - Client user IP * @param {string} [userParameters.userAgent] - Client user agent * @param {string} [userParameters.acceptLanguage] - Client accept language - * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} @@ -1750,6 +1792,1096 @@ class Tracker { return new Error('A parameters object with an "itemId" property is required.'); } + /** + * Send product insights agent answer feedback event to API + * + * @function trackProductInsightsAgentAnswerFeedback + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.feedbackLabel - Feedback value: either "thumbs_up" or "thumbs_down" + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.qnaResultId] - Questions and answers result identifier + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User provided feedback on the usefulness of a product insights agent answer + * @example + * constructorio.tracker.trackProductInsightsAgentAnswerFeedback( + * { + * feedbackLabel: 'thumbs_up', + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * variationId: 'KMH879-7632', + * qnaResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentAnswerFeedback(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + feedback_label, + feedbackLabel = feedback_label, + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + qna_result_id, + qnaResultId = qna_result_id, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + if (!feedbackLabel) { + return new Error('A parameters object with a "feedbackLabel" property is required.'); + } + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + qna_result_id: qnaResultId, + thread_id: threadId, + feedback_label: feedbackLabel, + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_answer_feedback?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent answer view event to API + * + * @function trackProductInsightsAgentAnswerView + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.question - Question that was asked + * @param {string} parameters.answerText - The answer to the given question + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.questionTopic] - Topic category of the question, as assigned during generation + * @param {string} [parameters.qnaResultId] - Questions and answers result identifier + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object[]} [parameters.items] - List of recommended product items displayed alongside the answer (maximum 100) + * @param {object[]} [parameters.followUpQuestions] - List of follow-up questions displayed alongside the answer, each in the shape of { value } (maximum 100) + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User viewed a product insights agent answer + * @example + * constructorio.tracker.trackProductInsightsAgentAnswerView( + * { + * question: 'Is this t-shirt machine washable?', + * answerText: 'Yes, it can be machine washed at 30 degrees.', + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * qnaResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * items: [{ itemId: 'KMH877', itemName: 'Blue T-Shirt' }], + * followUpQuestions: [{ value: 'What sizes are available?' }], + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentAnswerView(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + question, + answer_text, + answerText = answer_text, + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + question_topic, + questionTopic = question_topic, + qna_result_id, + qnaResultId = qna_result_id, + thread_id, + threadId = thread_id, + items, + follow_up_questions, + followUpQuestions = follow_up_questions, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + if (!question) { + return new Error('A parameters object with a "question" property is required.'); + } + + // answer_text may be an empty string, so only the type is validated + if (typeof answerText !== 'string') { + return new Error('A parameters object with an "answerText" property of type string is required.'); + } + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + question, + question_topic: questionTopic, + qna_result_id: qnaResultId, + thread_id: threadId, + answer_text: answerText, + }; + + 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); + } + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_answer_view?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent out of view event to API + * + * @function trackProductInsightsAgentOutOfView + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description The product insights agent was scrolled out of view + * @example + * constructorio.tracker.trackProductInsightsAgentOutOfView( + * { + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * variationId: 'KMH879-7632', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentOutOfView(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + thread_id: threadId, + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_out_of_view?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent focus event to API + * + * @function trackProductInsightsAgentFocus + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User focused on the product insights agent + * @example + * constructorio.tracker.trackProductInsightsAgentFocus( + * { + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * variationId: 'KMH879-7632', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentFocus(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + thread_id: threadId, + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_focus?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent question click event to API + * + * @function trackProductInsightsAgentQuestionClick + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.question - Question submitted by the user + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.questionTopic] - Topic category of the question, as assigned during generation + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User clicked on a question in the product insights agent + * @example + * constructorio.tracker.trackProductInsightsAgentQuestionClick( + * { + * question: 'Is this t-shirt machine washable?', + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * variationId: 'KMH879-7632', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentQuestionClick(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + question, + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + question_topic, + questionTopic = question_topic, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + if (!question) { + return new Error('A parameters object with a "question" property is required.'); + } + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + question, + question_topic: questionTopic, + thread_id: threadId, + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_question_click?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent question submit event to API + * + * @function trackProductInsightsAgentQuestionSubmit + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.question - Question submitted by the user + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.questionTopic] - Topic category of the question, as assigned during generation + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User submitted a question to the product insights agent + * @example + * constructorio.tracker.trackProductInsightsAgentQuestionSubmit( + * { + * question: 'Is this t-shirt machine washable?', + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * variationId: 'KMH879-7632', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentQuestionSubmit(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + question, + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + question_topic, + questionTopic = question_topic, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + if (!question) { + return new Error('A parameters object with a "question" property is required.'); + } + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + question, + question_topic: questionTopic, + thread_id: threadId, + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_question_submit?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent result click event to API + * + * @function trackProductInsightsAgentResultClick + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.question - Question that produced the answer containing this recommendation + * @param {string} parameters.seedItemId - Product id of the page the product insights agent widget is on + * @param {string} [parameters.seedItemName] - Product name of the page the product insights agent widget is on + * @param {string} [parameters.seedVariationId] - Variation id of the page the product insights agent widget is on + * @param {string} [parameters.itemId] - Clicked product item unique identifier + * @param {string} [parameters.itemName] - Clicked product item name + * @param {string} [parameters.variationId] - Clicked product item variation unique identifier + * @param {number} [parameters.position] - Position of the clicked item in the recommendations list + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.qnaResultId] - Questions and answers result identifier + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User clicked on a recommended item within a product insights agent answer + * @example + * constructorio.tracker.trackProductInsightsAgentResultClick( + * { + * question: 'Is there a similar t-shirt in blue?', + * seedItemId: 'KMH876', + * seedItemName: 'Red T-Shirt', + * itemId: 'KMH877', + * itemName: 'Blue T-Shirt', + * position: 0, + * qnaResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentResultClick(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + question, + seed_item_id, + seedItemId = seed_item_id, + seed_item_name, + seedItemName = seed_item_name, + seed_variation_id, + seedVariationId = seed_variation_id, + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + position, + features, + feature_variants, + featureVariants = feature_variants, + qna_result_id, + qnaResultId = qna_result_id, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + if (!question) { + return new Error('A parameters object with a "question" property is required.'); + } + + if (!seedItemId) { + return new Error('A parameters object with a "seedItemId" property is required.'); + } + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + qna_result_id: qnaResultId, + thread_id: threadId, + position, + question, + seed_item_id: seedItemId, + seed_item_name: seedItemName, + seed_variation_id: seedVariationId, + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_result_click?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent view event to API + * + * @function trackProductInsightsAgentView + * @param {object} parameters - Additional parameters to be sent with request + * @param {object[]} parameters.questions - List of pre-generated questions shown to the user, each in the shape of { question, questionTopic } + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description The product insights agent, with its pre-generated questions, was shown to the user + * @example + * constructorio.tracker.trackProductInsightsAgentView( + * { + * questions: [ + * { question: 'Is this t-shirt machine washable?', questionTopic: 'care' }, + * { question: 'What sizes are available?' }, + * ], + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * variationId: 'KMH879-7632', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentView(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + questions, + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + if (!questions || !Array.isArray(questions)) { + return new Error('A parameters object with a "questions" property of type array is required.'); + } + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + thread_id: threadId, + questions: questions.map((question) => helpers.toSnakeCaseKeys(question, false)), + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_view?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + + /** + * Send product insights agent views event to API + * + * @function trackProductInsightsAgentViews + * @param {object} parameters - Additional parameters to be sent with request + * @param {object[]} parameters.questions - List of pre-generated questions shown to the user, each in the shape of { question, questionTopic } + * @param {object[]} parameters.viewTimespans - List of timespans the product insights agent was in the visible part of the screen, each in the shape of { start, end } with ISO 8601 timestamps including a timezone + * @param {string} [parameters.itemId] - Product item unique identifier + * @param {string} [parameters.itemName] - Product item name + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {object} [parameters.features] - Dictionary of feature flags + * @param {object} [parameters.featureVariants] - Dictionary of feature variants + * @param {string} [parameters.threadId] - Thread identifier for grouping events within a conversation + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {string} [parameters.section] - Index section + * @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 + * @param {string} [userParameters.userId] - User ID, utilized to personalize results + * @param {string[]} [userParameters.segments] - User segments + * @param {object} [userParameters.testCells] - User test cells + * @param {string} [userParameters.originReferrer] - Client page URL (including path) + * @param {string} [userParameters.documentReferrer] - Client page URL the event originated from + * @param {string} [userParameters.canonicalUrl] - Canonical URL of the client page + * @param {string} [userParameters.referer] - Client page URL (including path) + * @param {string} [userParameters.userIp] - Client user IP + * @param {string} [userParameters.userAgent] - Client user agent + * @param {string} [userParameters.acceptLanguage] - Client accept language + * @param {number} [userParameters.dateTime] - Time since epoch in milliseconds + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description The product insights agent was in the visible part of the screen for the given timespans + * @example + * constructorio.tracker.trackProductInsightsAgentViews( + * { + * questions: [ + * { question: 'Is this t-shirt machine washable?', questionTopic: 'care' }, + * ], + * viewTimespans: [ + * { start: '2026-10-05T10:00:00.000Z', end: '2026-10-05T10:00:05.000Z' }, + * { start: '2026-10-05T10:01:00.000Z', end: '2026-10-05T10:01:10.000Z' }, + * ], + * itemId: 'KMH876', + * itemName: 'Red T-Shirt', + * threadId: '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * { + * sessionId: 1, + * clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392', + * testCells: { + * testName: 'cellName', + * }, + * }, + * ); + */ + trackProductInsightsAgentViews(parameters, userParameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) { + return new Error('parameters are required of type object'); + } + + const { + // accept snake_case aliases alongside camelCase + questions, + view_timespans, + viewTimespans = view_timespans, + item_id, + itemId = item_id, + item_name, + itemName = item_name, + variation_id, + variationId = variation_id, + features, + feature_variants, + featureVariants = feature_variants, + thread_id, + threadId = thread_id, + analytics_tags, + analyticsTags = analytics_tags, + section, + } = parameters; + + if (!questions || !Array.isArray(questions)) { + return new Error('A parameters object with a "questions" property of type array is required.'); + } + + if (!viewTimespans || !Array.isArray(viewTimespans)) { + return new Error('A parameters object with a "viewTimespans" property of type array is required.'); + } + + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + features, + feature_variants: featureVariants, + analytics_tags: analyticsTags, + thread_id: threadId, + questions: questions.map((question) => helpers.toSnakeCaseKeys(question, false)), + view_timespans: viewTimespans, + }; + + // query params that are not assigned in the applyParams() + const queryParams = { + section, + }; + + const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_views?`; + const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`; + const requestMethod = 'POST'; + // POST events must include common parameters (key, i, s, c, ui, _dt, origin_referrer, canonical_url, document_referrer) both in body and query string + const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod }); + + send.call( + this, + requestUrl, + userParameters, + networkParameters, + requestMethod, + requestBody, + ); + + return true; + } + /** * 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 cffb636f..f87f482b 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -10,6 +10,8 @@ export interface TrackerUserParameters { segments?: string | string[]; testCells?: Record; originReferrer?: string; + documentReferrer?: string; + canonicalUrl?: string; referer?: string; userIp?: string; userAgent?: string; @@ -220,5 +222,178 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; + trackProductInsightsAgentAnswerFeedback( + parameters: { + feedbackLabel: 'thumbs_up' | 'thumbs_down'; + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + qnaResultId?: string; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentAnswerView( + parameters: { + question: string; + answerText: string; + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + questionTopic?: string; + qnaResultId?: string; + threadId?: string; + items?: { + itemId?: string; + itemName?: string; + variationId?: string; + slCampaignId?: string; + slCampaignOwner?: string; + }[]; + followUpQuestions?: { + value: string; + }[]; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentOutOfView( + parameters: { + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentFocus( + parameters: { + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentQuestionClick( + parameters: { + question: string; + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + questionTopic?: string; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentQuestionSubmit( + parameters: { + question: string; + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + questionTopic?: string; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentResultClick( + parameters: { + question: string; + seedItemId: string; + seedItemName?: string; + seedVariationId?: string; + itemId?: string; + itemName?: string; + variationId?: string; + position?: number; + features?: Record; + featureVariants?: Record; + qnaResultId?: string; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentView( + parameters: { + questions: { + question: string; + questionTopic?: string; + }[]; + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + + trackProductInsightsAgentViews( + parameters: { + questions: { + question: string; + questionTopic?: string; + }[]; + viewTimespans: { + start: string; + end: string; + }[]; + itemId?: string; + itemName?: string; + variationId?: string; + features?: Record; + featureVariants?: Record; + threadId?: string; + analyticsTags?: Record; + section?: string; + }, + userParameters?: TrackerUserParameters, + networkParameters?: NetworkParameters + ): true | Error; + on(messageType: string, callback: Function): true | Error; }