diff --git a/spec/src/modules/agent.js b/spec/src/modules/agent.js new file mode 100644 index 00000000..7810d0a3 --- /dev/null +++ b/spec/src/modules/agent.js @@ -0,0 +1,268 @@ +/* eslint-disable no-unused-expressions, import/no-unresolved */ +const dotenv = require('dotenv'); +const chai = require('chai'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); +const EventSource = require('eventsource'); +const { ReadableStream } = require('web-streams-polyfill'); +const qs = require('qs'); +const { createAgentUrl, setupEventListeners } = require('../../../src/modules/agent'); +const Agent = require('../../../src/modules/agent'); +let ConstructorIO = require('../../../test/constructorio'); // eslint-disable-line import/extensions +const { encodeURIComponentRFC3986 } = require('../../../src/utils/helpers'); +const jsdom = require('../utils/jsdom-global'); + +const bundled = process.env.BUNDLED === 'true'; +const bundledDescriptionSuffix = bundled ? ' - Bundled' : ''; + +chai.use(sinonChai); +dotenv.config(); + +const testApiKey = process.env.TEST_REQUEST_API_KEY; +const clientVersion = 'cio-mocha'; + +const defaultOptions = { + apiKey: testApiKey, + version: clientVersion, + agentServiceUrl: 'https://agent.cnstrc.com', + clientId: '123', + sessionId: 123, +}; + +const defaultParameters = { + domain: 'agent', +}; + +describe(`ConstructorIO - Agent${bundledDescriptionSuffix}`, () => { + const jsdomOptions = { url: 'http://localhost' }; + let cleanup; + + beforeEach(() => { + cleanup = jsdom(jsdomOptions); + global.CLIENT_VERSION = clientVersion; + window.CLIENT_VERSION = clientVersion; + + if (bundled) { + ConstructorIO = window.ConstructorioClient; + } + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + delete window.CLIENT_VERSION; + cleanup(); + }); + + // createAgentUrl util Tests + describe('Test createAgentUrl', () => { + + it('should throw an error if intent is not provided', () => { + expect(() => createAgentUrl('', defaultParameters, defaultOptions)).throw('intent is a required parameter of type string'); + }); + + it('should throw an error if domain is not provided in parameters', () => { + expect(() => createAgentUrl('testIntent', {}, defaultOptions)).throw('parameters.domain is a required parameter of type string'); + }); + + it('should correctly construct a URL with minimal valid inputs', () => { + const intent = 'testIntent'; + const url = createAgentUrl(intent, defaultParameters, defaultOptions); + + expect(url).contain('https://agent.cnstrc.com/v1/intent/'); + expect(url).contain(`intent/${encodeURIComponentRFC3986(intent)}`); + + const requestedUrlParams = qs.parse(url.split('?')?.[1]); + + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('i'); + expect(requestedUrlParams).to.have.property('s'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + expect(requestedUrlParams).to.have.property('_dt'); + + }); + + it('should clean and encode parameters correctly', () => { + const intentWithSpaces = 'test/ [Intent)'; + const url = createAgentUrl( + intentWithSpaces, + { ...defaultParameters }, + defaultOptions, + ); + + // Ensure spaces are encoded and parameters are cleaned + expect(url).not.contain(' '); + expect(url).contain(encodeURIComponentRFC3986(intentWithSpaces)); + }); + }); + + // setupEventListeners util Tests + describe('Test setupEventListeners', () => { + let mockEventSource; + let mockStreamController; + + beforeEach(() => { + mockEventSource = { + addEventListener: sinon.stub(), + close: sinon.stub(), + }; + + mockStreamController = { + enqueue: sinon.stub(), + close: sinon.stub(), + error: sinon.stub(), + }; + }); + + afterEach(() => { + sinon.restore(); // Restore all mocks + }); + + it('should set up event listeners for all event types', () => { + const eventTypes = Agent.EventTypes; + + setupEventListeners(mockEventSource, mockStreamController, eventTypes); + Object.values(Agent.EventTypes).forEach((event) => { + expect(mockEventSource.addEventListener.calledWith(event)).to.be.true; + }); + }); + + it('should enqueue event data into the stream', (done) => { + const eventTypes = Agent.EventTypes; + const eventType = Agent.EventTypes.SEARCH_RESULT; + const eventData = { data: 'Hello, world!' }; + + setupEventListeners(mockEventSource, mockStreamController, eventTypes); + + // Simulate an event being emitted + const allEventsCallbacks = mockEventSource.addEventListener.getCalls(); + const searchResultsCallback = allEventsCallbacks.find((call) => call.args[0] === eventType).args[1]; + + searchResultsCallback({ data: JSON.stringify(eventData) }); + + setImmediate(() => { // Ensure stream processing completes + expect(mockStreamController.enqueue.calledWith({ type: eventType, data: eventData })).to.be.true; + done(); + }); + }); + + it('should close the EventSource and the stream when END event is received', () => { + const eventTypes = Agent.EventTypes; + const eventType = Agent.EventTypes.END; + + setupEventListeners(mockEventSource, mockStreamController, eventTypes); + + // Simulate the END event being emitted + const endEventCallback = mockEventSource.addEventListener.getCalls() + .find((call) => call.args[0] === eventType).args[1]; + + endEventCallback(); + + expect(mockEventSource.close.called).to.be.true; + expect(mockStreamController.close.called).to.be.true; + }); + + it('should handle errors from the EventSource', () => { + const eventTypes = { START: 'start', END: 'end' }; + const mockError = new Error('Test Error'); + + setupEventListeners(mockEventSource, mockStreamController, eventTypes); + + // Directly trigger the onerror handler + mockEventSource.onerror(mockError); + + // Assert that controller.error was called with the mock error + sinon.assert.calledWith(mockStreamController.error, mockError); + + // Assert that the event source was closed + sinon.assert.calledOnce(mockEventSource.close); + }); + + it('should correctly handle and enqueue events with specific data structures', (done) => { + const eventType = Agent.EventTypes.SEARCH_RESULT; + const complexData = { intent_result_id: 123, response: { results: [{ name: 'Item 1' }, { name: 'Item 2' }] } }; + const eventTypes = Agent.EventTypes; + + setupEventListeners(mockEventSource, mockStreamController, eventTypes); + + // Simulate an event with a complex data structure being emitted + const dataStructureEventCallback = mockEventSource.addEventListener.getCalls() + .find((call) => call.args[0] === eventType).args[1]; + + dataStructureEventCallback({ data: JSON.stringify(complexData) }); + + // Verify that the complex data structure was correctly enqueued + setImmediate(() => { + expect(mockStreamController.enqueue.calledWith({ type: eventType, data: complexData })).to.be.true; + done(); + }); + }); + }); + + describe('getAgentResultsStream', () => { + beforeEach(() => { + global.EventSource = EventSource; + global.ReadableStream = ReadableStream; + window.EventSource = EventSource; + window.ReadableStream = ReadableStream; + }); + + afterEach(() => { + delete global.EventSource; + delete global.ReadableStream; + delete window.EventSource; + delete window.ReadableStream; + }); + + it('should create a readable stream', () => { + const { agent } = new ConstructorIO(defaultOptions); + const stream = agent.getAgentResultsStream('I want shoes', { domain: 'assistant' }); + + // Assert it return a stream object + expect(stream).to.have.property('getReader'); + }); + + it('should throw an error if missing domain parameter', () => { + const { agent } = new ConstructorIO(defaultOptions); + + expect(() => agent.getAgentResultsStream('I want shoes', {})).throw('parameters.domain is a required parameter of type string'); + }); + + it('should throw an error if missing intent', () => { + const { agent } = new ConstructorIO(defaultOptions); + + expect(() => agent.getAgentResultsStream('', {})).throw('intent is a required parameter of type string'); + }); + + it('should push expected data to the stream', async () => { + const { agent } = new ConstructorIO(defaultOptions); + const stream = await agent.getAgentResultsStream('query', { domain: 'assistant' }); + const reader = stream.getReader(); + const { value, done } = await reader.read(); + + // Assert that the stream is not empty and the first chunk contains expected data + expect(done).to.be.false; + expect(value.type).to.equal('start'); + reader.releaseLock(); + }); + + it('should handle cancel to the stream gracefully', async () => { + const { agent } = new ConstructorIO(defaultOptions); + const stream = await agent.getAgentResultsStream('query', { domain: 'assistant' }); + const reader = stream.getReader(); + const { value, done } = await reader.read(); + + // Assert that the stream is not empty and the first chunk contains expected data + expect(done).to.be.false; + expect(value.type).to.equal('start'); + reader.cancel(); + }); + + it('should handle pre maturely cancel before reading any data', async () => { + const { agent } = new ConstructorIO(defaultOptions); + const stream = await agent.getAgentResultsStream('query', { domain: 'assistant' }); + const reader = stream.getReader(); + + reader.cancel(); + }); + }); +}); diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 5f14fc9b..cf6b1142 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -8303,6 +8303,1793 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); }); + describe('trackAgentSubmit', () => { + const requiredParameters = { intent: 'Show me cookie recipes' }; + const optionalParameters = { + section: 'Products', + }; + + it('Should respond with a valid response when intent 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('intent').to.equal(requiredParameters.intent); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackAgentSubmit(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.trackAgentSubmit(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.trackAgentSubmit(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.trackAgentSubmit(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 requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal('Products'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackAgentSubmit(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.trackAgentSubmit()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentSubmit()).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.trackAgentSubmit(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.trackAgentSubmit(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.trackAgentSubmit(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.trackAgentSubmit(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.trackAgentSubmit(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.trackAgentSubmit(requiredParameters)).to.equal(true); + }); + }); + + describe('trackAgentResultLoadStarted', () => { + const requiredParameters = { intent: 'Show me cookie recipes' }; + const optionalParameters = { + section: 'Products', + intentResultId: '123451', + }; + + 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('intent').to.equal(requiredParameters.intent); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(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 requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(requestParams).to.have.property('intent_result_id').to.equal(optionalParameters.intentResultId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + // eslint-disable-next-line max-len + expect(tracker.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentResultLoadStarted()).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.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(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.trackAgentResultLoadStarted(requiredParameters)).to.equal(true); + }); + }); + + describe('trackAgentResultLoadFinished', () => { + const requiredParameters = { intent: 'Show me cookie recipes', searchResultCount: 15 }; + const optionalParameters = { + section: 'Products', + intentResultId: '123451', + }; + + 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('intent').to.equal(requiredParameters.intent); + expect(requestParams).to.have.property('search_result_count').to.equal(requiredParameters.searchResultCount); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(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 requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(requestParams).to.have.property('intent_result_id').to.equal(optionalParameters.intentResultId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + // eslint-disable-next-line max-len + expect(tracker.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentResultLoadFinished()).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.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(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.trackAgentResultLoadFinished(requiredParameters)).to.equal(true); + }); + }); + + describe('trackAgentResultClick', () => { + const requiredParameters = { + intent: 'Show me cookie recipes', + searchResultId: '12341cd', + itemName: 'espresso', + itemId: '1123', + }; + const optionalParameters = { + section: 'Products', + intentResultId: '12312', + variationId: '123123', + }; + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + validateOriginReferrer(requestParams); + expect(requestParams).to.have.property('intent').to.equal(requiredParameters.intent); + expect(requestParams).to.have.property('search_result_id').to.equal(requiredParameters.searchResultId); + expect(requestParams).to.have.property('item_name').to.equal(requiredParameters.itemName); + expect(requestParams).to.have.property('item_id').to.equal(requiredParameters.itemId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackAgentResultClick(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractUrlParamsFromFetch(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.trackAgentResultClick(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.extractUrlParamsFromFetch(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.trackAgentResultClick(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.extractUrlParamsFromFetch(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.trackAgentResultClick(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 requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('intent_result_id').to.equal(optionalParameters.intentResultId); + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(requestParams).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.trackAgentResultClick(Object.assign(requiredParameters, optionalParameters))) + .to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentResultClick([])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentResultClick()).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.extractUrlParamsFromFetch(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.trackAgentResultClick(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.extractUrlParamsFromFetch(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.trackAgentResultClick(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.trackAgentResultClick(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.trackAgentResultClick(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.extractUrlParamsFromFetch(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.trackAgentResultClick(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.extractUrlParamsFromFetch(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.trackAgentResultClick(requiredParameters)).to.equal(true); + }); + }); + + describe('trackAgentResultView', () => { + const requiredParameters = { + intent: 'Show me cookie recipes', + numResultsViewed: 5, + searchResultId: '123123123', + }; + const optionalParameters = { + intentResultId: 'result-id', + section: 'Products', + items: [ + { + itemId: '123', + variationId: '456', + }, + { + itemName: 'product test', + itemId: '789', + }, + ], + }; + const snakeCaseItems = [ + { + item_id: '123', + variation_id: '456', + }, + { + item_name: 'product test', + item_id: '789', + }, + ]; + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('intent').to.equal(requiredParameters.intent); + expect(requestParams).to.have.property('search_result_id').to.equal(requiredParameters.searchResultId); + expect(requestParams).to.have.property('num_results_viewed').to.equal(requiredParameters.numResultsViewed); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackAgentResultView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when required parameters and segments are provided', (done) => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + 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'); + + done(); + }); + + expect(tracker.trackAgentResultView(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'); + + done(); + }); + + expect(tracker.trackAgentResultView(requiredParameters)).to.equal(true); + }); + + it('Should respond with a valid response when 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'); + + done(); + }); + + expect(tracker.trackAgentResultView(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 requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('intent_result_id').to.equal(optionalParameters.intentResultId); + expect(requestParams).to.have.property('items').to.deep.equal(snakeCaseItems); + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackAgentResultView(Object.assign(requiredParameters, optionalParameters))).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentResultView([])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentResultView()).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.extractUrlParamsFromFetch(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.trackAgentResultView(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.extractUrlParamsFromFetch(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.trackAgentResultView(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.trackAgentResultView(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.trackAgentResultView(requiredParameters)).to.equal(true); + }); + } + + it('Should not encode body 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.trackAgentResultView(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.trackAgentResultView(requiredParameters)).to.equal(true); + }); + }); + + describe('trackAgentSearchSubmit', () => { + const requiredParameters = { intent: 'Show me cookie recipes', searchTerm: 'Flour', searchResultId: '123' }; + const optionalParameters = { + section: 'Products', + intentResultId: '1234', + }; + + 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('intent').to.equal(requiredParameters.intent); + expect(requestParams).to.have.property('search_term').to.equal(requiredParameters.searchTerm); + expect(requestParams).to.have.property('search_result_id').to.equal(requiredParameters.searchResultId); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackAgentSearchSubmit(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.trackAgentSearchSubmit(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.trackAgentSearchSubmit(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.trackAgentSearchSubmit(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 requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('section').to.equal(optionalParameters.section); + expect(requestParams).to.have.property('intent_result_id').to.equal(optionalParameters.intentResultId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackAgentSearchSubmit(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.trackAgentSearchSubmit()).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackAgentSearchSubmit()).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.trackAgentSearchSubmit(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.trackAgentSearchSubmit(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.trackAgentSearchSubmit(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.trackAgentSearchSubmit(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.trackAgentSearchSubmit(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.trackAgentSearchSubmit(requiredParameters)).to.equal(true); + }); + }); + describe('trackAssistantSubmit', () => { const requiredParameters = { intent: 'Show me cookie recipes' }; const optionalParameters = { diff --git a/src/constructorio.js b/src/constructorio.js index fbf820a7..958f94cd 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -11,6 +11,7 @@ const EventDispatcher = require('./utils/event-dispatcher'); const helpers = require('./utils/helpers'); const { default: packageVersion } = require('./version'); const Quizzes = require('./modules/quizzes'); +const Agent = require('./modules/agent'); const Assistant = require('./modules/assistant'); // Compute package version string @@ -38,7 +39,8 @@ class ConstructorIO { * @param {string} parameters.apiKey - Constructor.io API key * @param {string} [parameters.serviceUrl='https://ac.cnstrc.com'] - API URL endpoint * @param {string} [parameters.quizzesServiceUrl='https://quizzes.cnstrc.com'] - Quizzes API URL endpoint - * @param {string} [parameters.assistantServiceUrl='https://assistant.cnstrc.com'] - AI Assistant API URL endpoint + * @param {string} [parameters.agentServiceUrl='https://agent.cnstrc.com'] - AI Shopping Agent API URL endpoint + * @param {string} [parameters.assistantServiceUrl='https://assistant.cnstrc.com'] - AI Shopping Assistant API URL endpoint @deprecated This parameter is deprecated and will be removed in a future version. Use parameters.agentServiceUrl instead. * @param {array} [parameters.segments] - User segments * @param {object} [parameters.testCells] - User test cells * @param {string} [parameters.clientId] - Client ID, defaults to value supplied by 'constructorio-id' module @@ -60,7 +62,8 @@ class ConstructorIO { * @property {object} recommendations - Interface to {@link module:recommendations} * @property {object} tracker - Interface to {@link module:tracker} * @property {object} quizzes - Interface to {@link module:quizzes} - * @property {object} assistant - Interface to {@link module:assistant} + * @property {object} agent - Interface to {@link module:agent} + * @property {object} assistant - Interface to {@link module:assistant} @deprecated This property is deprecated and will be removed in a future version. Use the agent property instead. * @returns {class} */ constructor(options = {}) { @@ -69,6 +72,7 @@ class ConstructorIO { version: versionFromOptions, serviceUrl, quizzesServiceUrl, + agentServiceUrl, assistantServiceUrl, segments, testCells, @@ -115,6 +119,7 @@ class ConstructorIO { version: versionFromOptions || versionFromGlobal || computePackageVersion(), serviceUrl: helpers.addHTTPSToString(normalizedServiceUrl) || 'https://ac.cnstrc.com', quizzesServiceUrl: (quizzesServiceUrl && quizzesServiceUrl.replace(/\/$/, '')) || 'https://quizzes.cnstrc.com', + agentServiceUrl: (agentServiceUrl && agentServiceUrl.replace(/\/$/, '')) || 'https://agent.cnstrc.com', assistantServiceUrl: (assistantServiceUrl && assistantServiceUrl.replace(/\/$/, '')) || 'https://assistant.cnstrc.com', sessionId: sessionId || session_id, clientId: clientId || client_id, @@ -137,6 +142,7 @@ class ConstructorIO { this.recommendations = new Recommendations(this.options); this.tracker = new Tracker(this.options); this.quizzes = new Quizzes(this.options); + this.agent = new Agent(this.options); this.assistant = new Assistant(this.options); // Dispatch initialization event diff --git a/src/modules/agent.js b/src/modules/agent.js new file mode 100644 index 00000000..3358938c --- /dev/null +++ b/src/modules/agent.js @@ -0,0 +1,187 @@ +const { cleanParams, trimNonBreakingSpaces, encodeURIComponentRFC3986, stringify } = require('../utils/helpers'); + +// Create URL from supplied intent (term) and parameters +function createAgentUrl(intent, parameters, options) { + const { + apiKey, + version, + sessionId, + clientId, + userId, + segments, + testCells, + agentServiceUrl, + assistantServiceUrl, + } = options; + let queryParams = { c: version }; + queryParams.key = apiKey; + queryParams.i = clientId; + queryParams.s = sessionId; + + const serviceUrl = agentServiceUrl || assistantServiceUrl; + + // Validate intent is provided + if (!intent || typeof intent !== 'string') { + throw new Error('intent is a required parameter of type string'); + } + + // Validate domain is provided + if (!parameters.domain || typeof parameters.domain !== 'string') { + throw new Error('parameters.domain is a required parameter of type string'); + } + + // Pull test cells from options + if (testCells) { + Object.keys(testCells).forEach((testCellKey) => { + queryParams[`ef-${testCellKey}`] = testCells[testCellKey]; + }); + } + + // Pull user segments from options + if (segments && segments.length) { + queryParams.us = segments; + } + + // Pull user id from options and ensure string + if (userId) { + queryParams.ui = String(userId); + } + + if (parameters) { + const { domain, numResultsPerPage } = parameters; + + // Pull domain from parameters + if (domain) { + queryParams.domain = domain; + } + + // Pull results number from parameters + if (numResultsPerPage) { + queryParams.num_results_per_page = numResultsPerPage; + } + } + + // eslint-disable-next-line no-underscore-dangle + queryParams._dt = Date.now(); + queryParams = cleanParams(queryParams); + + const queryString = stringify(queryParams); + const cleanedQuery = intent.replace(/^\//, '|'); // For compatibility with backend API + + return `${serviceUrl}/v1/intent/${encodeURIComponentRFC3986(trimNonBreakingSpaces(cleanedQuery))}?${queryString}`; +} + +// Add event listeners to custom SSE that pushes data to the stream +function setupEventListeners(eventSource, controller, eventTypes) { + const addListener = (type) => { + eventSource.addEventListener(type, (event) => { + const data = JSON.parse(event.data); + + controller.enqueue({ type, data }); // Enqueue data into the stream + }); + }; + + // Set up listeners for all event types except END + Object.values(eventTypes).forEach((type) => { + if (type !== eventTypes.END) { + addListener(type); + } + }); + + // Handle the END event separately to close the stream + eventSource.addEventListener(eventTypes.END, () => { + controller.close(); // Close the stream + eventSource.close(); // Close the EventSource connection + }); + + // Handle errors from the EventSource + // eslint-disable-next-line no-param-reassign + eventSource.onerror = (error) => { + controller.error(error); // Pass the error to the stream + eventSource.close(); // Close the EventSource connection + }; +} + +/** + * Interface to agent SSE. + * Replaces the previous Assistant module. + * + * @module agent + * @inner + * @returns {object} + */ +class Agent { + constructor(options) { + this.options = options || {}; + } + + static EventTypes = { + START: 'start', // Denotes the start of the stream + GROUP: 'group', // Represents a semantic grouping of search results, optionally having textual explanation + SEARCH_RESULT: 'search_result', // Represents a set of results with metadata (used to show results with search refinements) + ARTICLE_REFERENCE: 'article_reference', // Represents a set of content with metadata + RECIPE_INFO: 'recipe_info', // Represents recipes' auxiliary information like cooking times & serving sizes + RECIPE_INSTRUCTIONS: 'recipe_instructions', // Represents recipe instructions + SERVER_ERROR: 'server_error', // Server Error event + IMAGE_META: 'image_meta', // This event type is used for enhancing recommendations with media content such as images + END: 'end', // Represents the end of data stream + }; + + /** + * Retrieve agent results from EventStream + * + * @function getAgentResultsStream + * @description Retrieve a stream of agent results from Constructor.io API + * @param {string} intent - Intent to use to perform an intent based recommendations + * @param {object} [parameters] - Additional parameters to refine result set + * @param {string} [parameters.domain] - domain name e.g. swimming sports gear, groceries + * @param {number} [parameters.numResultsPerPage] - The total number of results to return + * @returns {ReadableStream} Returns a ReadableStream. + * @example + * const readableStream = constructorio.agent.getAgentResultsStream('I want to get shoes', { + * domain: "nike_sportswear", + * }); + * const reader = readableStream.getReader(); + * const { value, done } = await reader.read(); + */ + getAgentResultsStream(query, parameters) { + let eventSource; + let readableStream; + + try { + const requestUrl = createAgentUrl(query, parameters, this.options); + + // Create an EventSource that connects to the Server Sent Events API + eventSource = new EventSource(requestUrl); + + // Create a readable stream that data will be pushed into + readableStream = new ReadableStream({ + // To be called on stream start + start(controller) { + // Listen to events emitted from ASA Server Sent Events and push data to the ReadableStream + setupEventListeners(eventSource, controller, Agent.EventTypes); + }, + // To be called on stream cancelling + cancel() { + // Close the EventSource connection when the stream is prematurely canceled + eventSource.close(); + }, + }); + } catch (e) { + if (readableStream) { + readableStream?.cancel(); + } else { + // If the stream was not successfully created, close the EventSource directly + eventSource?.close(); + } + + throw new Error(e.message); + } + + return readableStream; + } +} + +module.exports = Agent; +module.exports.createAgentUrl = createAgentUrl; +module.exports.setupEventListeners = setupEventListeners; diff --git a/src/modules/assistant.js b/src/modules/assistant.js index f67e05ee..49821d4a 100644 --- a/src/modules/assistant.js +++ b/src/modules/assistant.js @@ -1,132 +1,21 @@ -const { cleanParams, trimNonBreakingSpaces, encodeURIComponentRFC3986, stringify } = require('../utils/helpers'); - -// Create URL from supplied intent (term) and parameters -function createAssistantUrl(intent, parameters, options) { - const { - apiKey, - version, - sessionId, - clientId, - userId, - segments, - testCells, - assistantServiceUrl, - } = options; - let queryParams = { c: version }; - - queryParams.key = apiKey; - queryParams.i = clientId; - queryParams.s = sessionId; - - // Validate intent is provided - if (!intent || typeof intent !== 'string') { - throw new Error('intent is a required parameter of type string'); - } - - // Validate domain is provided - if (!parameters.domain || typeof parameters.domain !== 'string') { - throw new Error('parameters.domain is a required parameter of type string'); - } - - // Pull test cells from options - if (testCells) { - Object.keys(testCells).forEach((testCellKey) => { - queryParams[`ef-${testCellKey}`] = testCells[testCellKey]; - }); - } - - // Pull user segments from options - if (segments && segments.length) { - queryParams.us = segments; - } - - // Pull user id from options and ensure string - if (userId) { - queryParams.ui = String(userId); - } - - if (parameters) { - const { domain, numResultsPerPage } = parameters; - - // Pull domain from parameters - if (domain) { - queryParams.domain = domain; - } - - // Pull results number from parameters - if (numResultsPerPage) { - queryParams.num_results_per_page = numResultsPerPage; - } - } - - // eslint-disable-next-line no-underscore-dangle - queryParams._dt = Date.now(); - queryParams = cleanParams(queryParams); - - const queryString = stringify(queryParams); - const cleanedQuery = intent.replace(/^\//, '|'); // For compatibility with backend API - - return `${assistantServiceUrl}/v1/intent/${encodeURIComponentRFC3986(trimNonBreakingSpaces(cleanedQuery))}?${queryString}`; -} - -// Add event listeners to custom SSE that pushes data to the stream -function setupEventListeners(eventSource, controller, eventTypes) { - const addListener = (type) => { - eventSource.addEventListener(type, (event) => { - const data = JSON.parse(event.data); - - controller.enqueue({ type, data }); // Enqueue data into the stream - }); - }; - - // Set up listeners for all event types except END - Object.values(eventTypes).forEach((type) => { - if (type !== eventTypes.END) { - addListener(type); - } - }); - - // Handle the END event separately to close the stream - eventSource.addEventListener(eventTypes.END, () => { - controller.close(); // Close the stream - eventSource.close(); // Close the EventSource connection - }); - - // Handle errors from the EventSource - // eslint-disable-next-line no-param-reassign - eventSource.onerror = (error) => { - controller.error(error); // Pass the error to the stream - eventSource.close(); // Close the EventSource connection - }; -} +const Agent = require('./agent'); +const { createAgentUrl, setupEventListeners } = require('./agent'); /** + * @deprecated This module is deprecated and will be removed in a future version. Use the Agent module instead. * Interface to assistant SSE. * * @module assistant * @inner * @returns {object} */ -class Assistant { - constructor(options) { - this.options = options || {}; - } - - static EventTypes = { - START: 'start', // Denotes the start of the stream - GROUP: 'group', // Represents a semantic grouping of search results, optionally having textual explanation - SEARCH_RESULT: 'search_result', // Represents a set of results with metadata (used to show results with search refinements) - ARTICLE_REFERENCE: 'article_reference', // Represents a set of content with metadata - RECIPE_INFO: 'recipe_info', // Represents recipes' auxiliary information like cooking times & serving sizes - RECIPE_INSTRUCTIONS: 'recipe_instructions', // Represents recipe instructions - SERVER_ERROR: 'server_error', // Server Error event - IMAGE_META: 'image_meta', // This event type is used for enhancing recommendations with media content such as images - END: 'end', // Represents the end of data stream - }; +class Assistant extends Agent { + EventTypes = Agent.EventTypes; /** * Retrieve assistant results from EventStream * + * @deprecated Use getAssistantResultsStream from the Agent module instead. * @function getAssistantResultsStream * @description Retrieve a stream of assistant results from Constructor.io API * @param {string} intent - Intent to use to perform an intent based recommendations @@ -142,43 +31,10 @@ class Assistant { * const { value, done } = await reader.read(); */ getAssistantResultsStream(query, parameters) { - let eventSource; - let readableStream; - - try { - const requestUrl = createAssistantUrl(query, parameters, this.options); - - // Create an EventSource that connects to the Server Sent Events API - eventSource = new EventSource(requestUrl); - - // Create a readable stream that data will be pushed into - readableStream = new ReadableStream({ - // To be called on stream start - start(controller) { - // Listen to events emitted from ASA Server Sent Events and push data to the ReadableStream - setupEventListeners(eventSource, controller, Assistant.EventTypes); - }, - // To be called on stream cancelling - cancel() { - // Close the EventSource connection when the stream is prematurely canceled - eventSource.close(); - }, - }); - } catch (e) { - if (readableStream) { - readableStream?.cancel(); - } else { - // If the stream was not successfully created, close the EventSource directly - eventSource?.close(); - } - - throw new Error(e.message); - } - - return readableStream; + return this.getAgentResultsStream(query, parameters); } } module.exports = Assistant; -module.exports.createAssistantUrl = createAssistantUrl; +module.exports.createAssistantUrl = createAgentUrl; module.exports.setupEventListeners = setupEventListeners; diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 75b03d65..065d35e1 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -2403,23 +2403,23 @@ class Tracker { /** * Send ASA request submitted event * - * @function trackAssistantSubmit + * @function trackAgentSubmit * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.intent - Intent of user request * @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 an assistant search - * (pressing enter within assistant input element, or clicking assistant submit element) + * @description User submitted an agent search + * (pressing enter within agent input element, or clicking agent submit element) * @example - * constructorio.tracker.trackAssistantSubmit( + * constructorio.tracker.trackAgentSubmit( * { * intent: 'show me a recipe for a cookie', * }, * ); */ - trackAssistantSubmit(parameters, networkParameters = {}) { + trackAgentSubmit(parameters, networkParameters = {}) { if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { // Ensure parameters are provided (required) const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/assistant_submit?`; @@ -2454,9 +2454,9 @@ class Tracker { } /** - * Send assistant results page load started + * Send agent results page load started * - * @function trackAssistantResultLoadStarted + * @function trackAgentResultLoadStarted * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.intent - Intent of user request * @param {string} [parameters.section] - The section name for the item Ex. "Products" @@ -2464,16 +2464,16 @@ class Tracker { * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} - * @description Assistant results page load begun (but has not necessarily loaded completely) + * @description Agent results page load begun (but has not necessarily loaded completely) * @example - * constructorio.tracker.trackAssistantResultLoadStarted( + * constructorio.tracker.trackAgentResultLoadStarted( * { * intent: 'show me a recipe for a cookie', * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', * }, * ); */ - trackAssistantResultLoadStarted(parameters, networkParameters = {}) { + trackAgentResultLoadStarted(parameters, networkParameters = {}) { if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { // Ensure parameters are provided (required) const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/assistant_result_load_start?`; @@ -2510,9 +2510,9 @@ class Tracker { } /** - * Send assistant results page load finished + * Send agent results page load finished * - * @function trackAssistantResultLoadFinished + * @function trackAgentResultLoadFinished * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.intent - Intent of user request * @param {number} parameters.searchResultCount - Number of search results loaded @@ -2521,9 +2521,9 @@ class Tracker { * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} - * @description Assistant results page load finished + * @description Agent results page load finished * @example - * constructorio.tracker.trackAssistantResultLoadFinished( + * constructorio.tracker.trackAgentResultLoadFinished( * { * intent: 'show me a recipe for a cookie', * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', @@ -2531,7 +2531,7 @@ class Tracker { * }, * ); */ - trackAssistantResultLoadFinished(parameters, networkParameters = {}) { + trackAgentResultLoadFinished(parameters, networkParameters = {}) { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const baseUrl = `${this.options.serviceUrl}/v2/behavioral_action/assistant_result_load_finish?`; @@ -2570,9 +2570,9 @@ class Tracker { } /** - * Send assistant result click event to API + * Send agent result click event to API * - * @function trackAssistantResultClick + * @function trackAgentResultClick * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.intent - intent of the user * @param {string} parameters.searchResultId - result_id of the specific search result the clicked item belongs to @@ -2584,9 +2584,9 @@ class Tracker { * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} - * @description User clicked a result that appeared within an assistant search result + * @description User clicked a result that appeared within an agent search result * @example - * constructorio.tracker.trackAssistantResultClick( + * constructorio.tracker.trackAgentResultClick( * { * variationId: 'KMH879-7632', * searchResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', @@ -2596,7 +2596,7 @@ class Tracker { * }, * ); */ - trackAssistantResultClick(parameters, networkParameters = {}) { + trackAgentResultClick(parameters, networkParameters = {}) { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/assistant_search_result_click?`; @@ -2641,9 +2641,9 @@ class Tracker { } /** - * Send assistant search result view event to API + * Send agent search result view event to API * - * @function trackAssistantResultView + * @function trackAgentResultView * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.intent - intent of the user * @param {string} parameters.searchResultId - result_id of the specific search result the clicked item belongs to @@ -2654,9 +2654,9 @@ class Tracker { * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} - * @description User viewed a search result within an assistant result + * @description User viewed a search result within an agent result * @example - * constructorio.tracker.trackAssistantResultView( + * constructorio.tracker.trackAgentResultView( * { * searchResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', @@ -2666,7 +2666,7 @@ class Tracker { * }, * ); */ - trackAssistantResultView(parameters, networkParameters = {}) { + trackAgentResultView(parameters, networkParameters = {}) { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/assistant_search_result_view?`; @@ -2711,19 +2711,19 @@ class Tracker { /** * Send ASA search submitted event * - * @function trackAssistantSearchSubmit + * @function trackAgentSearchSubmit * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.intent - Intent of user request - * @param {string} parameters.searchTerm - Term of submitted assistant search event + * @param {string} parameters.searchTerm - Term of submitted agent search event * @param {string} parameters.searchResultId - resultId of search result the clicked item belongs to * @param {string} [parameters.section] - The section name for the item Ex. "Products" * @param {string} [parameters.intentResultId] - intentResultId from the ASA response * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {(true|Error)} - * @description User submitted an alternative assistant search result search term + * @description User submitted an alternative agent search result search term * @example - * constructorio.tracker.trackAssistantSearchSubmit({ + * constructorio.tracker.trackAgentSearchSubmit({ * { * searchResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', @@ -2732,7 +2732,7 @@ class Tracker { * }, * ); */ - trackAssistantSearchSubmit(parameters, networkParameters = {}) { + trackAgentSearchSubmit(parameters, networkParameters = {}) { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { // Ensure parameters are provided (required) @@ -2774,6 +2774,174 @@ class Tracker { return new Error('parameters is a required parameter of type object'); } + /** + * Send ASA request submitted event + * + * @deprecated This method will be removed in a future version. Use trackAgentSubmit instead. + * @function trackAssistantSubmit + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.intent - Intent of user request + * @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 an assistant search + * (pressing enter within assistant input element, or clicking assistant submit element) + * @example + * constructorio.tracker.trackAssistantSubmit( + * { + * intent: 'show me a recipe for a cookie', + * }, + * ); + */ + trackAssistantSubmit(parameters, networkParameters = {}) { + return this.trackAgentSubmit(parameters, networkParameters); + } + + /** + * Send assistant results page load started + * + * @deprecated This method will be removed in a future version. Use trackAgentResultLoadStarted instead. + * @function trackAssistantResultLoadStarted + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.intent - Intent of user request + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {string} [parameters.intentResultId] - The intent result id from the ASA response + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description Assistant results page load begun (but has not necessarily loaded completely) + * @example + * constructorio.tracker.trackAssistantResultLoadStarted( + * { + * intent: 'show me a recipe for a cookie', + * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', + * }, + * ); + */ + trackAssistantResultLoadStarted(parameters, networkParameters = {}) { + return this.trackAgentResultLoadStarted(parameters, networkParameters); + } + + /** + * Send assistant results page load finished + * + * @deprecated This method will be removed in a future version. Use trackAgentResultLoadFinished instead. + * @function trackAssistantResultLoadFinished + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.intent - Intent of user request + * @param {number} parameters.searchResultCount - Number of search results loaded + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {string} [parameters.intentResultId] - The intent result id from the ASA response + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description Assistant results page load finished + * @example + * constructorio.tracker.trackAssistantResultLoadFinished( + * { + * intent: 'show me a recipe for a cookie', + * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', + * searchResultCount: 5, + * }, + * ); + */ + trackAssistantResultLoadFinished(parameters, networkParameters = {}) { + return this.trackAgentResultLoadFinished(parameters, networkParameters); + } + + /** + * Send assistant result click event to API + * + * @deprecated This method will be removed in a future version. Use trackAgentResultClick instead. + * @function trackAssistantResultClick + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.intent - intent of the user + * @param {string} parameters.searchResultId - result_id of the specific search result the clicked item belongs to + * @param {string} parameters.itemId - Product item unique identifier + * @param {string} parameters.itemName - Product item name + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {string} [parameters.variationId] - Product item variation unique identifier + * @param {string} [parameters.intentResultId] - Browse result identifier (returned in response from Constructor) + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User clicked a result that appeared within an assistant search result + * @example + * constructorio.tracker.trackAssistantResultClick( + * { + * variationId: 'KMH879-7632', + * searchResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', + * intent: 'show me a recipe for a cookie', + * itemId: 'KMH876', + * }, + * ); + */ + trackAssistantResultClick(parameters, networkParameters = {}) { + return this.trackAgentResultClick(parameters, networkParameters); + } + + /** + * Send assistant search result view event to API + * + * @deprecated This method will be removed in a future version. Use trackAgentResultView instead. + * @function trackAssistantResultView + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.intent - intent of the user + * @param {string} parameters.searchResultId - result_id of the specific search result the clicked item belongs to + * @param {number} parameters.numResultsViewed - Number of items viewed in this search result + * @param {object[]} [parameters.items] - List of product item objects viewed + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {string} [parameters.intentResultId] - Browse result identifier (returned in response from Constructor) + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User viewed a search result within an assistant result + * @example + * constructorio.tracker.trackAssistantResultView( + * { + * searchResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', + * intent: 'show me a recipe for a cookie', + * numResultsViewed: 5, + * items: [{itemId: 'KMH876'}, {itemId: 'KMH140'}, {itemId: 'KMH437'}], + * }, + * ); + */ + trackAssistantResultView(parameters, networkParameters = {}) { + return this.trackAgentResultView(parameters, networkParameters); + } + + /** + * Send ASA search submitted event + * + * @deprecated This method will be removed in a future version. Use trackAgentSearchSubmit instead. + * @function trackAssistantSearchSubmit + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.intent - Intent of user request + * @param {string} parameters.searchTerm - Term of submitted assistant search event + * @param {string} parameters.searchResultId - resultId of search result the clicked item belongs to + * @param {string} [parameters.section] - The section name for the item Ex. "Products" + * @param {string} [parameters.intentResultId] - intentResultId from the ASA response + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User submitted an alternative assistant search result search term + * @example + * constructorio.tracker.trackAssistantSearchSubmit({ + * { + * searchResultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2', + * intentResultId: 'Zde93fd-f955-4020-8b8d-6b21b93cb5a2', + * intent: 'show me a recipe for a cookie', + * searchTerm: 'flour', + * }, + * ); + */ + trackAssistantSearchSubmit(parameters, networkParameters = {}) { + return this.trackAgentSearchSubmit(parameters, networkParameters); + } + /** * Send product insights agent view events * diff --git a/src/types/agent.d.ts b/src/types/agent.d.ts new file mode 100644 index 00000000..eeb63f25 --- /dev/null +++ b/src/types/agent.d.ts @@ -0,0 +1,22 @@ +import { + ConstructorClientOptions, +} from '.'; + +export default Agent; + +export interface IAgentParameters { + domain: string; + numResultsPerPage?: number; + filters?: Record; +} + +declare class Agent { + constructor(options: ConstructorClientOptions); + + options: ConstructorClientOptions; + + getAgentResultsStream( + intent: string, + parameters?: IAgentParameters, + ): ReadableStream; +} diff --git a/src/types/constructorio.d.ts b/src/types/constructorio.d.ts index 17f9896a..f68df4af 100644 --- a/src/types/constructorio.d.ts +++ b/src/types/constructorio.d.ts @@ -3,6 +3,7 @@ import Browse from './browse'; import Autocomplete from './autocomplete'; import Recommendations from './recommendations'; import Quizzes from './quizzes'; +import Agent from './agent'; import Assistant from './assistant'; import Tracker from './tracker'; import { ConstructorClientOptions } from '.'; @@ -24,6 +25,8 @@ declare class ConstructorIO { quizzes: Quizzes; + agent: Agent; + assistant: Assistant; tracker: Tracker; @@ -32,5 +35,5 @@ declare class ConstructorIO { } declare namespace ConstructorIO { - export { Search, Browse, Autocomplete, Recommendations, Quizzes, Tracker, Assistant }; + export { Search, Browse, Autocomplete, Recommendations, Quizzes, Tracker, Agent, Assistant }; } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index c0eab5ea..1d5f0a52 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -47,7 +47,8 @@ export interface ConstructorClientOptions { version?: string; serviceUrl?: string; quizzesServiceUrl?: string; - assistantServiceUrl?: string, + agentServiceUrl?: string; + assistantServiceUrl?: string; sessionId?: number; clientId?: string; userId?: string; diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index d508298a..75f08487 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -266,7 +266,7 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; - trackAssistantSubmit( + trackAgentSubmit( parameters: { intent: string; section?: string; @@ -274,7 +274,7 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; - trackAssistantResultLoadStarted( + trackAgentResultLoadStarted( parameters: { intent: string; section?: string; @@ -283,7 +283,7 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; - trackAssistantResultLoadFinished( + trackAgentResultLoadFinished( parameters: { intent: string; searchResultCount: number; @@ -293,7 +293,7 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; - trackAssistantResultClick( + trackAgentResultClick( parameters: { intent: string; searchResultId: string; @@ -306,7 +306,7 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; - trackAssistantResultView( + trackAgentResultView( parameters: { intent: string; searchResultId: string; @@ -318,7 +318,7 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; - trackAssistantSearchSubmit( + trackAgentSearchSubmit( parameters: { intent: string; searchTerm: string; @@ -331,71 +331,107 @@ 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; + trackAssistantSubmit: typeof Tracker.prototype.trackAgentSubmit; + + trackAssistantResultLoadStarted: typeof Tracker.prototype.trackAgentResultLoadStarted; + + trackAssistantResultLoadFinished: typeof Tracker.prototype.trackAgentResultLoadFinished; + + trackAssistantResultClick: typeof Tracker.prototype.trackAgentResultClick; + + trackAssistantResultView: typeof Tracker.prototype.trackAgentResultView; + + trackAssistantSearchSubmit: typeof Tracker.prototype.trackAgentSearchSubmit; + + 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; }