diff --git a/cspell.json b/cspell.json index 65f1a318..1a988a7b 100644 --- a/cspell.json +++ b/cspell.json @@ -47,6 +47,7 @@ "rerank", "atcs", "testdata", - "Bytespider" + "Bytespider", + "Timespans" ] } diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 955bee1e..5f14fc9b 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -10089,4 +10089,2401 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackAssistantSearchSubmit(requiredParameters)).to.equal(true); }); }); + + describe('trackProductInsightsAgentViews', () => { + const requiredParameters = { + itemId: '1', + itemName: 'item1', + questions: [ + { question: 'Why choose this?' }, + { question: 'How is this product made?' }, + { question: 'What are the dimensions of this product?' }, + ], + viewTimespans: [ + { + start: '2025-05-19T14:30:00+02:00', + end: '2025-05-19T14:30:05+02:00', + }, + { + start: '2025-05-19T14:30:10+02:00', + end: '2025-05-19T14:30:15+02:00', + }, + ], + }; + const optionalParameters = { + section: 'Products', + variationId: '2', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('questions').to.deep.equal(requiredParameters.questions); + expect(requestParams).to.have.property('view_timespans').to.deep.equal(requiredParameters.viewTimespans); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentViews()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentViews(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentView', () => { + const requiredParameters = { + itemId: '1', + itemName: 'item1', + questions: [ + { question: 'Why choose this?' }, + { question: 'How is this product made?' }, + { question: 'What are the dimensions of this product?' }, + ], + }; + const optionalParameters = { + section: 'Products', + variationId: '2', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('questions').to.deep.equal(requiredParameters.questions); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentView()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentView()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentView(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentOutOfView', () => { + const requiredParameters = { itemId: '1', itemName: 'item1' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentOutOfView()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentOutOfView()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentOutOfView(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentFocus', () => { + const requiredParameters = { itemId: '1', itemName: 'item1' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentFocus()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentFocus()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentFocus(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentQuestionClick', () => { + const requiredParameters = { itemId: '1', itemName: 'item1', question: 'Why choose this?' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionClick()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionClick()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionClick(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentQuestionSubmit', () => { + const requiredParameters = { itemId: '1', itemName: 'item1', question: 'Why choose this?' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentQuestionSubmit(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentAnswerView', () => { + const requiredParameters = { itemId: '1', itemName: 'item1', question: 'Why choose this?', answerText: 'This product is awesome!' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + qnaResultId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('question').to.equal(requiredParameters.question); + expect(requestParams).to.have.property('answer_text').to.equal(requiredParameters.answerText); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + expect(bodyParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerView()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerView()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerView(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); + + describe('trackProductInsightsAgentAnswerFeedback', () => { + const requiredParameters = { itemId: '1', itemName: 'item1', feedbackLabel: 'thumbs_up' }; + const optionalParameters = { + section: 'Products', + variationId: '2', + qnaResultId: '0daf0015-fc29-4727-9140-8d5313a1902c', + }; + + it('Should respond with a valid response when term and required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('feedback_label').to.equal(requiredParameters.feedbackLabel); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when term, required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('us').to.deep.equal(segments); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and userId are provided', (done) => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and testCells are provided', (done) => { + const testCells = { foo: 'bar' }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + 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)).to.equal(true); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const bodyParams = helpers.extractBodyParamsFromFetch(fetchSpy); + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(bodyParams).to.have.property('qna_result_id').to.equal(optionalParameters.qnaResultId); + expect(bodyParams).to.have.property('variation_id').to.equal(optionalParameters.variationId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback( + Object.assign(requiredParameters, optionalParameters), + )).to.equal(true); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).to.equal(true); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).to.equal(true); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters, { timeout: 10 })).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).to.equal(true); + }); + } + + it('Should properly encode query parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).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, + userId, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(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)).to.equal(true); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 2a2b9922..75b03d65 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -2774,6 +2774,576 @@ class Tracker { return new Error('parameters is a required parameter of type object'); } + /** + * Send product insights agent view events + * + * @function trackProductInsightsAgentViews + * @param {object} parameters - Additional parameters to be sent with request + * @param {array.<{question: string}>} parameters.questions - List of pre-defined questions shown to the user + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {array.<{start: string | undefined, + * end: string | undefined}>} parameters.viewTimespans - List of timestamp pairs in ISO_8601 format + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description The product insights agent element appeared in the visible part of the page + * @example + * constructorio.tracker.trackProductInsightsAgentViews({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'questions': [ + * { question: 'Why choose this?' }, + * { question: 'How is this product made?' }, + * { question: 'What are the dimensions of this product?' } + * ], + * 'viewTimespans': [ + * { + * 'start': '2025-05-19T14:30:00+02:00', + * 'end': '2025-05-19T14:30:05+02:00' + * }, + * { + * 'start': '2025-05-19T14:30:10+02:00', + * 'end': '2025-05-19T14:30:15+02:00' + * } + * ] + * }, + * ); + */ + trackProductInsightsAgentViews(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_views?`; + const { + section, + questions, + itemId, + itemName, + variationId, + viewTimespans, + } = parameters; + const queryParams = {}; + const bodyParams = { + questions, + item_id: itemId, + item_name: itemName, + variation_id: variationId, + view_timespans: viewTimespans, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent view event + * + * @function trackProductInsightsAgentView + * @param {object} parameters - Additional parameters to be sent with request + * @param {array.<{question: string}>} parameters.questions - List of pre-defined questions shown to the user + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description The product insights agent element appeared in the visible part of the page + * @example + * constructorio.tracker.trackProductInsightsAgentView({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'questions': [ + * { question: 'Why choose this?' }, + * { question: 'How is this product made?' }, + * { question: 'What are the dimensions of this product?' } + * ], + * }, + * ); + */ + trackProductInsightsAgentView(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_view?`; + const { + section, + questions, + itemId, + itemName, + variationId, + } = parameters; + const queryParams = {}; + const bodyParams = { + questions, + item_id: itemId, + item_name: itemName, + variation_id: variationId, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent out of view event + * + * @function trackProductInsightsAgentOutOfView + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description The product insights agent element disappeared from the visible part of the page + * @example + * constructorio.tracker.trackProductInsightsAgentOutOfView({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * }, + * ); + */ + trackProductInsightsAgentOutOfView(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_out_of_view?`; + const { + section, + itemId, + itemName, + variationId, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent input focus event + * + * @function trackProductInsightsAgentFocus + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User focused on the product insights agent input element + * @example + * constructorio.tracker.trackProductInsightsAgentFocus({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * }, + * ); + */ + trackProductInsightsAgentFocus(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_focus?`; + const { + section, + itemId, + itemName, + variationId, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent question click event + * + * @function trackProductInsightsAgentQuestionClick + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} parameters.question - Question a user clicked on + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User clicked on a question within the product insights agent + * @example + * constructorio.tracker.trackProductInsightsAgentQuestionClick({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'question': 'Why choose this?' + * }, + * ); + */ + trackProductInsightsAgentQuestionClick(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_question_click?`; + const { + section, + itemId, + itemName, + variationId, + question, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + question, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent question submit event + * + * @function trackProductInsightsAgentQuestionSubmit + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} parameters.question - Question a user submitted + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User submitted a question to the product insights agent + * @example + * constructorio.tracker.trackProductInsightsAgentQuestionSubmit({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'question': 'Tell me some key highlights about this item?' + * }, + * ); + */ + trackProductInsightsAgentQuestionSubmit(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_question_submit?`; + const { + section, + itemId, + itemName, + variationId, + question, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + question, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent answer view event + * + * @function trackProductInsightsAgentAnswerView + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} parameters.question - Question a user submitted + * @param {string} parameters.answerText - Answer text of the question + * @param {string} [parameters.qnaResultId] - Answer result id returned + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User viewed the answer provided by the product insights agent + * @example + * constructorio.tracker.trackProductInsightsAgentAnswerView({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'question': 'Why choose this?', + * 'answerText': 'This product is awesome!', + * 'qnaResultId': '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * ); + */ + trackProductInsightsAgentAnswerView(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_answer_view?`; + const { + section, + itemId, + itemName, + variationId, + question, + answerText, + qnaResultId, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + question, + answer_text: answerText, + qna_result_id: qnaResultId, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + + /** + * Send product insights agent answer feedback event + * + * @function trackProductInsightsAgentAnswerFeedback + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Product id whose page we are on + * @param {string} parameters.itemName - Product name whose page we are on + * @param {string} parameters.feedbackLabel - Feedback value: either "thumbs_up" or "thumbs_down" + * @param {string} [parameters.qnaResultId] - Answer result id returned + * @param {string} [parameters.variationId] - Variation id whose page we are on + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description A user provided feedback on an answers usefulness + * @example + * constructorio.tracker.trackProductInsightsAgentAnswerFeedback({ + * { + * 'itemId': '1', + * 'itemName': 'item1', + * 'variationId': '2', + * 'feedbackLabel': 'thumbs_up', + * 'qnaResultId': '0daf0015-fc29-4727-9140-8d5313a1902c', + * }, + * ); + */ + trackProductInsightsAgentAnswerFeedback(parameters, networkParameters = {}) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/product_insights_agent_answer_feedback?`; + const { + section, + itemId, + itemName, + variationId, + feedbackLabel, + qnaResultId, + } = parameters; + const queryParams = {}; + const bodyParams = { + item_id: itemId, + item_name: itemName, + variation_id: variationId, + feedback_label: feedbackLabel, + qna_result_id: qnaResultId, + }; + + if (section) { + queryParams.section = section; + } + + const requestURL = `${baseUrl}${applyParamsAsString(queryParams, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { + ...this.options, + requestMethod, + }); + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + return true; + } + + this.requests.send(); + + return new Error('parameters is a required parameter of type object'); + } + /** * Subscribe to success or error messages emitted by tracking requests * diff --git a/src/types/index.d.ts b/src/types/index.d.ts index bb2d74ca..c0eab5ea 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -246,3 +246,12 @@ export interface ItemTracked { export interface ItemTrackedPurchase extends ItemTracked { count?: number; } + +export interface Question { + question: string; +} + +export interface TimeSpan { + start: string; + end: string; +} diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index d8846715..d508298a 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -1,5 +1,5 @@ import EventEmitter = require('events'); -import { ConstructorClientOptions, ItemTracked, ItemTrackedPurchase, NetworkParameters } from '.'; +import { ConstructorClientOptions, ItemTracked, ItemTrackedPurchase, Question, TimeSpan, NetworkParameters } from '.'; import RequestQueue = require('../utils/request-queue'); export default Tracker; @@ -331,5 +331,71 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; + trackProductInsightsAgentViews(parameters: { + questions: Question[]; + itemId: string; + itemName: string; + viewTimespans: TimeSpan[]; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + + trackProductInsightsAgentView(parameters: { + questions: Question[]; + itemId: string; + itemName: string; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + + trackProductInsightsAgentOutOfView(parameters: { + itemId: string; + itemName: string; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + + trackProductInsightsAgentFocus(parameters: { + itemId: string; + itemName: string; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + + trackProductInsightsAgentQuestionClick(parameters: { + itemId: string; + itemName: string; + question: string; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + + trackProductInsightsAgentQuestionSubmit(parameters: { + itemId: string; + itemName: string; + question: string; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + + trackProductInsightsAgentAnswerView(parameters: { + itemId: string; + itemName: string; + question: string; + answerText: string; + qnaResultId?: string; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + + trackProductInsightsAgentAnswerFeedback(parameters: { + itemId: string; + itemName: string; + feedbackLabel: string; + qnaResultId?: string; + variationId?: string; + section?: string; + }, networkParameters?: NetworkParameters): true | Error; + on(messageType: string, callback: Function): true | Error; }