From 196464d0f27678f46ab35ae824aed1832241e551 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Mon, 23 Sep 2019 18:52:48 -0400 Subject: [PATCH 01/56] Set up skeleton tracker module and implement sendSessionStart. --- src/constructorio.js | 4 ++- src/modules/search.js | 2 +- src/modules/tracker.js | 81 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/modules/tracker.js diff --git a/src/constructorio.js b/src/constructorio.js index 6a33dbf7..8bc925fe 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -5,7 +5,7 @@ const ConstructorioID = require('@constructor-io/constructorio-id'); const { search } = require('./modules/search'); const { autocomplete } = require('./modules/autocomplete'); const { recommendations } = require('./modules/recommendations'); - +const { tracker } = require('./modules/tracker'); const { version } = require('../package.json'); /** @@ -22,6 +22,7 @@ class ConstructorIO { * @property {object} [search] - Interface to {@link module:search} * @property {object} [autocomplete] - Interface to {@link module:autocomplete} * @property {object} [recommendations] - Interface to {@link module:recommendations} + * @property {object} [tracker] - Interface to {@link module:tracker} * @returns {class} */ constructor(options = {}) { @@ -55,6 +56,7 @@ class ConstructorIO { this.search = search(this.options); this.autocomplete = autocomplete(this.options); this.recommendations = recommendations(this.options); + this.tracker = tracker(this.options); } } diff --git a/src/modules/search.js b/src/modules/search.js index b20eefe5..aadc3079 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -192,7 +192,7 @@ export function search(options) { * @returns {Promise} * @see https://docs.constructor.io */ - getBrowseResults(parameters) { + getBrowseResults: (parameters) => { const requestUrl = createBrowseUrl(parameters); return fetch(requestUrl) diff --git a/src/modules/tracker.js b/src/modules/tracker.js new file mode 100644 index 00000000..173e1091 --- /dev/null +++ b/src/modules/tracker.js @@ -0,0 +1,81 @@ +/* eslint-disable import/prefer-default-export, object-curly-newline */ +import qs from 'qs'; +import fetchPonyfill from 'fetch-ponyfill'; +import Promise from 'es6-promise'; + +const { fetch } = fetchPonyfill({ Promise }); + +/** + * Interface to tracking related API calls. + * + * @module tracker + * @inner + * @returns {object} + */ +export function tracker(options) { + // Create URL from supplied parameters + const createTrackingUrl = (action) => { + const { apiKey, version, serviceUrl, sessionId, clientId } = options; + const queryParams = { c: version }; + const validActions = [ + 'session_start', + ]; + + // Ensure supplied action is valid + if (!action || validActions.indexOf(action) === -1) { + throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); + } + + queryParams.key = apiKey; + queryParams.i = clientId; + queryParams.s = sessionId; + + const queryString = qs.stringify(queryParams, { indices: false }); + + return `${serviceUrl}/behavior?${queryString}`; + }; + + return { + /** + * Send session start event to API + * + * @function sendSessionStart + * @returns {Promise} + */ + sendSessionStart: () => { + const requestUrl = createTrackingUrl('session_start'); + + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + + throw new Error(response.statusText); + }); + }, + + sendAutocompleteSelect: () => { + + }, + + sendAutocompleteSearch: () => { + + }, + + sendSearchResults: () => { + + }, + + sendSearchResultClick: () => { + + }, + + sendConversion: () => { + + }, + + sendPurchase: () => { + + }, + }; +} From 92a1a0ef127956691a379f07b892c22919983aab Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 24 Sep 2019 13:33:33 -0600 Subject: [PATCH 02/56] Add test for sendSessionStart. --- spec/src/modules/tracker.js | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 spec/src/modules/tracker.js diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js new file mode 100644 index 00000000..ca0d727a --- /dev/null +++ b/spec/src/modules/tracker.js @@ -0,0 +1,44 @@ +import jsdom from 'mocha-jsdom'; +import dotenv from 'dotenv'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import ConstructorIO from '../../../src/constructorio'; + +chai.use(chaiAsPromised); +dotenv.config(); + +const testApiKey = process.env.TEST_API_KEY; + +describe('ConstructorIO - Tracker', () => { + jsdom({ + url: 'http://localhost', + }); + + describe('sendSessionStart', () => { + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendSessionStart().then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendSessionStart()) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); +}); From ebbac98138c1756717861d16b0b5337d73eb27db Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 24 Sep 2019 14:33:19 -0600 Subject: [PATCH 03/56] Provide URL creation helper method for autocomplete endpoints. --- spec/src/modules/tracker.js | 66 ++++++++++++++++++++++- src/modules/tracker.js | 102 ++++++++++++++++++++++++++++++++++-- 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index ca0d727a..2db4e94f 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -9,7 +9,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe('ConstructorIO - Tracker', () => { +describe.only('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost', }); @@ -41,4 +41,68 @@ describe('ConstructorIO - Tracker', () => { .notify(done); }); }); + + describe('sendInputFocus', () => { + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendInputFocus().then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendInputFocus()) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); + + describe('sendAutocompleteSelect', () => { + const name = 'Where The Wild Things Are'; + const validParameters = { + tr: 'click', + autocompleteSection: 'Products', + resultId: '123-456-789', + originalQuery: 'books', + }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when name and parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendAutocompleteSelect(name, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendAutocompleteSelect(name, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 173e1091..989e5884 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -13,12 +13,13 @@ const { fetch } = fetchPonyfill({ Promise }); * @returns {object} */ export function tracker(options) { - // Create URL from supplied parameters - const createTrackingUrl = (action) => { + // Create behavior URL from supplied parameters + const createBehaviorUrl = (action) => { const { apiKey, version, serviceUrl, sessionId, clientId } = options; const queryParams = { c: version }; const validActions = [ 'session_start', + 'focus', ]; // Ensure supplied action is valid @@ -35,6 +36,63 @@ export function tracker(options) { return `${serviceUrl}/behavior?${queryString}`; }; + // Create autocomplete URL from supplied parameters + const createAutocompleteUrl = (action, name, parameters) => { + const { apiKey, version, serviceUrl, sessionId, clientId } = options; + const queryParams = { c: version }; + const validActions = [ + 'select', + 'search', + ]; + + // Ensure supplied action is valid + if (!action || validActions.indexOf(action) === -1) { + throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); + } + + // Validate product name is provided + if (!name || typeof name !== 'string') { + throw new Error('name is a required parameter of type string'); + } + + // Validate parameters are supplied and valid + if (!parameters || typeof parameters !== 'object' || !parameters.originalQuery || !parameters.resultId) { + throw new Error('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + } + + queryParams.key = apiKey; + queryParams.i = clientId; + queryParams.s = sessionId; + + if (parameters) { + const { originalQuery, tr, autocompleteSection, resultId } = parameters; + + // Pull original query from parameters + if (originalQuery) { + queryParams.original_query = originalQuery; + } + + // Pull trigger (tr) from parameters + if (tr) { + queryParams.tr = tr; + } + + // Pull autocomplete section from parameters + if (autocompleteSection) { + queryParams.autocomplete_section = autocompleteSection; + } + + // Pull result id from parameters + if (resultId) { + queryParams.result_id = resultId; + } + } + + const queryString = qs.stringify(queryParams, { indices: false }); + + return `${serviceUrl}/autocomplete/${encodeURIComponent(name)}/${action}?${queryString}`; + }; + return { /** * Send session start event to API @@ -43,7 +101,25 @@ export function tracker(options) { * @returns {Promise} */ sendSessionStart: () => { - const requestUrl = createTrackingUrl('session_start'); + const requestUrl = createBehaviorUrl('session_start'); + + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + + throw new Error(response.statusText); + }); + }, + + /** + * Send input focus event to API + * + * @function sendInputFocus + * @returns {Promise} + */ + sendInputFocus: () => { + const requestUrl = createBehaviorUrl('focus'); return fetch(requestUrl).then((response) => { if (response.ok) { @@ -54,8 +130,26 @@ export function tracker(options) { }); }, - sendAutocompleteSelect: () => { + /** + * Send autocomplete select event to API + * + * @function sendAutocompleteSelect + * @param {string} name - Name of selected product + * @param {object} [parameters] - Additional parameters to be sent with request + * @param {number} [parameters.page] - The page number of the results + * @param {number} [parameters.resultsPerPage] - The number of results per page to return + * @returns {Promise} + */ + sendAutocompleteSelect: (name, parameters) => { + const requestUrl = createAutocompleteUrl('select', name, parameters); + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + + throw new Error(response.statusText); + }); }, sendAutocompleteSearch: () => { From 010467960ed6632b3d91aad3e259857d506f3fa5 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 24 Sep 2019 15:03:34 -0600 Subject: [PATCH 04/56] Define red path tests for sendAutocompleteSelect. --- package.json | 1 + spec/src/modules/tracker.js | 65 ++++++++++++++++++++++++++++++++++++- src/modules/tracker.js | 13 ++++++-- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index c885f2c2..21eec29f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "eslint-plugin-import": "^2.18.2", "http-server": "^0.11.1", "jsdoc": "^3.6.3", + "lodash.clonedeep": "^4.5.0", "minami": "^1.2.3", "mocha": "^6.2.0", "mocha-jsdom": "^2.0.0", diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 2db4e94f..97fc1e5a 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -2,6 +2,7 @@ import jsdom from 'mocha-jsdom'; import dotenv from 'dotenv'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import cloneDeep from 'lodash.clonedeep'; import ConstructorIO from '../../../src/constructorio'; chai.use(chaiAsPromised); @@ -73,7 +74,6 @@ describe.only('ConstructorIO - Tracker', () => { describe('sendAutocompleteSelect', () => { const name = 'Where The Wild Things Are'; const validParameters = { - tr: 'click', autocompleteSection: 'Products', resultId: '123-456-789', originalQuery: 'books', @@ -96,6 +96,69 @@ describe.only('ConstructorIO - Tracker', () => { }); }); + it('Should respond with a valid response when name and parameters and tr are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendAutocompleteSelect(name, { + ...validParameters, + tr: 'click', + }).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid name is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSelect([], validParameters)).to.throw('name is a required parameter of type string'); + }); + + it('Should throw an error when no name is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSelect(null, validParameters)).to.throw('name is a required parameter of type string'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSelect(name, [])).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when no parameters are provided ', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSelect(name)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when no originalQuery parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.originalQuery; + + expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when no resultId parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.resultId; + + expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when no autocompleteSection parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.autocompleteSection; + + expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as is parameters.autocompleteSection'); + }); + it('Should throw an error when invalid apiKey is provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 989e5884..9bb21664 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -60,6 +60,11 @@ export function tracker(options) { throw new Error('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); } + // Autocomplete section is required for 'select' actions + if (parameters && action === 'select' && !parameters.autocompleteSection) { + throw new Error('parameters is a required object, as is parameters.autocompleteSection'); + } + queryParams.key = apiKey; queryParams.i = clientId; queryParams.s = sessionId; @@ -135,9 +140,11 @@ export function tracker(options) { * * @function sendAutocompleteSelect * @param {string} name - Name of selected product - * @param {object} [parameters] - Additional parameters to be sent with request - * @param {number} [parameters.page] - The page number of the results - * @param {number} [parameters.resultsPerPage] - The number of results per page to return + * @param {object} parameters - Additional parameters to be sent with request + * @param {number} parameters.originalQuery - The current autocomplete search query + * @param {number} parameters.resultId - Customer ID of the selected autocomplete item + * @param {number} parameters.autocompleteSection - Autocomplete section the item resides within + * @param {number} [parameters.tr] - Trigger used to select the autocomplete item (click, etc.) * @returns {Promise} */ sendAutocompleteSelect: (name, parameters) => { From 4d269f4dce9ac751e4f9f5ecb92223382a7a8344 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 24 Sep 2019 15:07:03 -0600 Subject: [PATCH 05/56] Define sendAutocompleteSearch + tests. --- spec/src/modules/tracker.js | 76 +++++++++++++++++++++++++++++++++++++ src/modules/tracker.js | 20 +++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 97fc1e5a..642c7e9e 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -168,4 +168,80 @@ describe.only('ConstructorIO - Tracker', () => { .notify(done); }); }); + + describe('sendAutocompleteSearch', () => { + const name = 'Where The Wild Things Are'; + const validParameters = { + resultId: '123-456-789', + originalQuery: 'books', + }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when name and parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendAutocompleteSearch(name, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid name is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSearch([], validParameters)).to.throw('name is a required parameter of type string'); + }); + + it('Should throw an error when no name is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSearch(null, validParameters)).to.throw('name is a required parameter of type string'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSearch(name, [])).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when no parameters are provided ', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendAutocompleteSearch(name)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when no originalQuery parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.originalQuery; + + expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when no resultId parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.resultId; + + expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendAutocompleteSearch(name, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 9bb21664..704a6019 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -159,8 +159,26 @@ export function tracker(options) { }); }, - sendAutocompleteSearch: () => { + /** + * Send autocomplete search event to API + * + * @function sendAutocompleteSearch + * @param {string} name - Name of selected product + * @param {object} parameters - Additional parameters to be sent with request + * @param {number} parameters.originalQuery - The current autocomplete search query + * @param {number} parameters.resultId - Customer ID of the selected autocomplete item + * @returns {Promise} + */ + sendAutocompleteSearch: (name, parameters) => { + const requestUrl = createAutocompleteUrl('search', name, parameters); + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + + throw new Error(response.statusText); + }); }, sendSearchResults: () => { From 7d39e9309edd9abd7b6befb953e37806e2a44a27 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 24 Sep 2019 16:54:40 -0600 Subject: [PATCH 06/56] Implement sendSearchResults. --- spec/src/modules/tracker.js | 2 +- src/modules/tracker.js | 67 ++++++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 642c7e9e..5cf87c91 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -10,7 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe.only('ConstructorIO - Tracker', () => { +describe('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost', }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 704a6019..28fda305 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -14,12 +14,13 @@ const { fetch } = fetchPonyfill({ Promise }); */ export function tracker(options) { // Create behavior URL from supplied parameters - const createBehaviorUrl = (action) => { + const createBehaviorUrl = (action, parameters) => { const { apiKey, version, serviceUrl, sessionId, clientId } = options; const queryParams = { c: version }; const validActions = [ 'session_start', 'focus', + 'search', ]; // Ensure supplied action is valid @@ -27,9 +28,41 @@ export function tracker(options) { throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); } + // Query (term) and num results are required for 'search' actions + if (action === 'search' + && ( + !parameters + || typeof parameters !== 'object' + || !parameters.query + || !parameters.numResults + ) + ) { + throw new Error('parameters is a required object, as is parameters.query and parameters.numResults'); + } + queryParams.key = apiKey; queryParams.i = clientId; queryParams.s = sessionId; + queryParams.action = action; + + if (parameters) { + const { query, numResults, customerIds } = parameters; + + // Pull query (term) from parameters + if (query) { + queryParams.term = query; + } + + // Pull number of results from parameters + if (numResults) { + queryParams.num_results = numResults; + } + + // Pull customer id's from parameters + if (customerIds) { + queryParams.customer_ids = customerIds; + } + } const queryString = qs.stringify(queryParams, { indices: false }); @@ -141,10 +174,10 @@ export function tracker(options) { * @function sendAutocompleteSelect * @param {string} name - Name of selected product * @param {object} parameters - Additional parameters to be sent with request - * @param {number} parameters.originalQuery - The current autocomplete search query - * @param {number} parameters.resultId - Customer ID of the selected autocomplete item - * @param {number} parameters.autocompleteSection - Autocomplete section the item resides within - * @param {number} [parameters.tr] - Trigger used to select the autocomplete item (click, etc.) + * @param {string} parameters.originalQuery - The current autocomplete search query + * @param {string} parameters.resultId - Customer ID of the selected autocomplete item + * @param {string} parameters.autocompleteSection - Autocomplete section the item resides within + * @param {string} [parameters.tr] - Trigger used to select the autocomplete item (click, etc.) * @returns {Promise} */ sendAutocompleteSelect: (name, parameters) => { @@ -165,8 +198,8 @@ export function tracker(options) { * @function sendAutocompleteSearch * @param {string} name - Name of selected product * @param {object} parameters - Additional parameters to be sent with request - * @param {number} parameters.originalQuery - The current autocomplete search query - * @param {number} parameters.resultId - Customer ID of the selected autocomplete item + * @param {string} parameters.originalQuery - The current autocomplete search query + * @param {string} parameters.resultId - Customer ID of the selected autocomplete item * @returns {Promise} */ sendAutocompleteSearch: (name, parameters) => { @@ -181,8 +214,26 @@ export function tracker(options) { }); }, - sendSearchResults: () => { + /** + * Send search results event to API + * + * @function sendSearchResults + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.query - The search query (term) + * @param {number} parameters.numResults - Number of search results in total + * @param {array} [parameters.customerIds] - List of customer item id's returned from search + * @returns {Promise} + */ + sendSearchResults: (parameters) => { + const requestUrl = createBehaviorUrl('search', parameters); + + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + throw new Error(response.statusText); + }); }, sendSearchResultClick: () => { From 801f2bd60338b89a936be58202de8800020bb2a0 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 25 Sep 2019 09:58:46 -0600 Subject: [PATCH 07/56] Add tests for sendSearchResults. --- spec/src/modules/tracker.js | 79 ++++++++++++++++++++++++++++++++++++- src/modules/tracker.js | 4 +- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 5cf87c91..29611169 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -10,7 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe('ConstructorIO - Tracker', () => { +describe.only('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost', }); @@ -96,7 +96,7 @@ describe('ConstructorIO - Tracker', () => { }); }); - it('Should respond with a valid response when name and parameters and tr are provided', (done) => { + it('Should respond with a valid response when name and parameters including tr are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); tracker.sendAutocompleteSelect(name, { @@ -244,4 +244,79 @@ describe('ConstructorIO - Tracker', () => { .notify(done); }); }); + + describe('sendSearchResults', () => { + const validParameters = { + query: 'books', + numResults: 1234, + }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendSearchResults(validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should respond with a valid response when parameters including customerIds are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendSearchResults({ + ...validParameters, + customerIds: ['foo', 'bar', 'baz'], + }).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResults([])).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + }); + + it('Should throw an error when no parameters are provided ', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResults()).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + }); + + it('Should throw an error when no query parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.query; + + expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + }); + + it('Should throw an error when no numResults parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.numResults; + + expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendSearchResults(validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 28fda305..04c2fab6 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -59,8 +59,8 @@ export function tracker(options) { } // Pull customer id's from parameters - if (customerIds) { - queryParams.customer_ids = customerIds; + if (customerIds && Array.isArray(customerIds)) { + queryParams.customer_ids = customerIds.join(','); } } From ee12347b1c1a74f5bb49472368fc4adbf15c6a3e Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 25 Sep 2019 10:36:24 -0600 Subject: [PATCH 08/56] Tests for sendSearchResultClick. --- spec/src/modules/tracker.js | 84 ++++++++++++++++++++++++++++-- src/modules/tracker.js | 100 +++++++++++++++++++++++++++++++----- 2 files changed, 167 insertions(+), 17 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 29611169..a91e14f6 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -123,13 +123,13 @@ describe.only('ConstructorIO - Tracker', () => { it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect(name, [])).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendAutocompleteSelect(name, [])).to.throw('parameters is a required object'); }); it('Should throw an error when no parameters are provided ', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect(name)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendAutocompleteSelect(name)).to.throw('parameters is a required object'); }); it('Should throw an error when no originalQuery parameter is provided', () => { @@ -208,13 +208,13 @@ describe.only('ConstructorIO - Tracker', () => { it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(name, [])).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendAutocompleteSearch(name, [])).to.throw('parameters is a required object'); }); it('Should throw an error when no parameters are provided ', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(name)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendAutocompleteSearch(name)).to.throw('parameters is a required object'); }); it('Should throw an error when no originalQuery parameter is provided', () => { @@ -319,4 +319,80 @@ describe.only('ConstructorIO - Tracker', () => { .notify(done); }); }); + + describe('sendSearchResultClick', () => { + const query = 'books'; + const validParameters = { + name: 'Where The Wild Things Are', + customerId: 1234, + }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when query and parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendSearchResultClick(query, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when no query is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResultClick(null, validParameters)).to.throw('query is a required parameter of type string'); + }); + + it('Should throw an error when invalid query is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResultClick([], validParameters)).to.throw('query is a required parameter of type string'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResultClick(query, [])).to.throw('parameters is a required object'); + }); + + it('Should throw an error when no parameters are provided ', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResultClick(query)).to.throw('parameters is a required object'); + }); + + it('Should throw an error when no name parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.name; + + expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); + }); + + it('Should throw an error when no customerId parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + delete parameters.customerId; + + expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendSearchResultClick(query, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 04c2fab6..fde06a16 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -48,12 +48,12 @@ export function tracker(options) { if (parameters) { const { query, numResults, customerIds } = parameters; - // Pull query (term) from parameters + // Pull query (term) from parameters (search) if (query) { queryParams.term = query; } - // Pull number of results from parameters + // Pull number of results from parameters (search) if (numResults) { queryParams.num_results = numResults; } @@ -69,8 +69,8 @@ export function tracker(options) { return `${serviceUrl}/behavior?${queryString}`; }; - // Create autocomplete URL from supplied parameters - const createAutocompleteUrl = (action, name, parameters) => { + // Create autocomplete URL from supplied parameters using name in directive + const createAutocompleteUrlByName = (action, name, parameters) => { const { apiKey, version, serviceUrl, sessionId, clientId } = options; const queryParams = { c: version }; const validActions = [ @@ -89,12 +89,17 @@ export function tracker(options) { } // Validate parameters are supplied and valid - if (!parameters || typeof parameters !== 'object' || !parameters.originalQuery || !parameters.resultId) { + if (!parameters || typeof parameters !== 'object') { + throw new Error('parameters is a required object'); + } + + // Original query and result id are required for 'select' and 'search' actions + if (!parameters.originalQuery || !parameters.resultId) { throw new Error('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); } // Autocomplete section is required for 'select' actions - if (parameters && action === 'select' && !parameters.autocompleteSection) { + if (action === 'select' && !parameters.autocompleteSection) { throw new Error('parameters is a required object, as is parameters.autocompleteSection'); } @@ -105,22 +110,22 @@ export function tracker(options) { if (parameters) { const { originalQuery, tr, autocompleteSection, resultId } = parameters; - // Pull original query from parameters + // Pull original query from parameters (select, search) if (originalQuery) { queryParams.original_query = originalQuery; } - // Pull trigger (tr) from parameters + // Pull trigger (tr) from parameters (select) if (tr) { queryParams.tr = tr; } - // Pull autocomplete section from parameters + // Pull autocomplete section from parameters (select) if (autocompleteSection) { queryParams.autocomplete_section = autocompleteSection; } - // Pull result id from parameters + // Pull result id from parameters (select, search) if (resultId) { queryParams.result_id = resultId; } @@ -131,6 +136,57 @@ export function tracker(options) { return `${serviceUrl}/autocomplete/${encodeURIComponent(name)}/${action}?${queryString}`; }; + // Create autocomplete URL from supplied parameters using query in directive + const createAutocompleteUrlByQuery = (action, query, parameters) => { + const { apiKey, version, serviceUrl, sessionId, clientId } = options; + const queryParams = { c: version }; + const validActions = [ + 'click_through', + ]; + + // Ensure supplied action is valid + if (!action || validActions.indexOf(action) === -1) { + throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); + } + + // Validate query (term) is provided + if (!query || typeof query !== 'string') { + throw new Error('query is a required parameter of type string'); + } + + // Validate parameters are supplied and valid + if (!parameters || typeof parameters !== 'object') { + throw new Error('parameters is a required object'); + } + + // Name and customer id are required for 'click_through' actions + if (!parameters.name || !parameters.customerId) { + throw new Error('parameters is a required object, as are parameters.name and parameters.customerId'); + } + + queryParams.key = apiKey; + queryParams.i = clientId; + queryParams.s = sessionId; + + if (parameters) { + const { name, customerId } = parameters; + + // Pull name from parameters (click_through) + if (name) { + queryParams.name = name; + } + + // Pull customer id from parameters (click_through) + if (customerId) { + queryParams.customer_id = customerId; + } + } + + const queryString = qs.stringify(queryParams, { indices: false }); + + return `${serviceUrl}/autocomplete/${encodeURIComponent(query)}/${action}?${queryString}`; + }; + return { /** * Send session start event to API @@ -181,7 +237,7 @@ export function tracker(options) { * @returns {Promise} */ sendAutocompleteSelect: (name, parameters) => { - const requestUrl = createAutocompleteUrl('select', name, parameters); + const requestUrl = createAutocompleteUrlByName('select', name, parameters); return fetch(requestUrl).then((response) => { if (response.ok) { @@ -203,7 +259,7 @@ export function tracker(options) { * @returns {Promise} */ sendAutocompleteSearch: (name, parameters) => { - const requestUrl = createAutocompleteUrl('search', name, parameters); + const requestUrl = createAutocompleteUrlByName('search', name, parameters); return fetch(requestUrl).then((response) => { if (response.ok) { @@ -236,8 +292,26 @@ export function tracker(options) { }); }, - sendSearchResultClick: () => { + /** + * Send click through event to API + * + * @function sendSearchResultClick + * @param {string} query - Current search query (term) + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.name - The name of the item that was clicked + * @param {string} parameters.customerId - The customer id of the item that was clicked + * @returns {Promise} + */ + sendSearchResultClick: (query, parameters) => { + const requestUrl = createAutocompleteUrlByQuery('click_through', query, parameters); + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + + throw new Error(response.statusText); + }); }, sendConversion: () => { From eabda65e26afb4c2e7b6fbde537e089138dc03fa Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 25 Sep 2019 13:35:28 -0600 Subject: [PATCH 09/56] Update sendAutocompleteSelect to use missing parameters, update tests. --- spec/src/modules/autocomplete.js | 4 +- spec/src/modules/recommendations.js | 4 +- spec/src/modules/search.js | 4 +- spec/src/modules/tracker.js | 383 ++++++++++++++-------------- src/modules/tracker.js | 84 +++++- src/utils.js | 34 +++ 6 files changed, 311 insertions(+), 202 deletions(-) create mode 100644 src/utils.js diff --git a/spec/src/modules/autocomplete.js b/spec/src/modules/autocomplete.js index dc69e4d7..1fc64181 100644 --- a/spec/src/modules/autocomplete.js +++ b/spec/src/modules/autocomplete.js @@ -10,9 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; describe('ConstructorIO - Autocomplete', () => { - jsdom({ - url: 'http://localhost', - }); + jsdom({ url: 'http://localhost' }); describe('getResults', () => { const query = 'drill'; diff --git a/spec/src/modules/recommendations.js b/spec/src/modules/recommendations.js index f1903812..822096ba 100644 --- a/spec/src/modules/recommendations.js +++ b/spec/src/modules/recommendations.js @@ -10,9 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; describe('ConstructorIO - Recommendations', () => { - jsdom({ - url: 'http://localhost', - }); + jsdom({ url: 'http://localhost' }); describe('getAlternativeItems', () => { const itemId = 'power_drill'; diff --git a/spec/src/modules/search.js b/spec/src/modules/search.js index 85becb86..cddae875 100644 --- a/spec/src/modules/search.js +++ b/spec/src/modules/search.js @@ -10,9 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; describe('ConstructorIO - Search', () => { - jsdom({ - url: 'http://localhost', - }); + jsdom({ url: 'http://localhost' }); describe('getSearchResults', () => { const query = 'drill'; diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index a91e14f6..04039d26 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -11,9 +11,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; describe.only('ConstructorIO - Tracker', () => { - jsdom({ - url: 'http://localhost', - }); + jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { beforeEach(() => { @@ -72,9 +70,9 @@ describe.only('ConstructorIO - Tracker', () => { }); describe('sendAutocompleteSelect', () => { - const name = 'Where The Wild Things Are'; + const term = 'Where The Wild Things Are'; const validParameters = { - autocompleteSection: 'Products', + section: 'Products', resultId: '123-456-789', originalQuery: 'books', }; @@ -87,16 +85,16 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when name and parameters are provided', (done) => { + it('Should respond with a valid response when term and parameters are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendAutocompleteSelect(name, validParameters).then((res) => { + tracker.sendAutocompleteSelect(term , validParameters).then((res) => { expect(res).to.equal(true); done(); }); }); - it('Should respond with a valid response when name and parameters including tr are provided', (done) => { + it('Should respond with a valid response when term and parameters including tr are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); tracker.sendAutocompleteSelect(name, { @@ -108,28 +106,41 @@ describe.only('ConstructorIO - Tracker', () => { }); }); - it('Should throw an error when invalid name is provided', () => { + it('Should respond with a valid response when term and parameters including group information are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendAutocompleteSelect(name, { + ...validParameters, + groupId: 'group-id', + displayName: 'display-name', + }).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect([], validParameters)).to.throw('name is a required parameter of type string'); + expect(() => tracker.sendAutocompleteSelect([], validParameters)).to.throw('term is a required parameter of type string'); }); - it('Should throw an error when no name is provided', () => { + it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect(null, validParameters)).to.throw('name is a required parameter of type string'); + expect(() => tracker.sendAutocompleteSelect(null, validParameters)).to.throw('term is a required parameter of type string'); }); it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect(name, [])).to.throw('parameters is a required object'); + expect(() => tracker.sendAutocompleteSelect(term, [])).to.throw('parameters is a required object'); }); it('Should throw an error when no parameters are provided ', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect(name)).to.throw('parameters is a required object'); + expect(() => tracker.sendAutocompleteSelect(term)).to.throw('parameters is a required object'); }); it('Should throw an error when no originalQuery parameter is provided', () => { @@ -138,7 +149,7 @@ describe.only('ConstructorIO - Tracker', () => { delete parameters.originalQuery; - expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); }); it('Should throw an error when no resultId parameter is provided', () => { @@ -147,252 +158,252 @@ describe.only('ConstructorIO - Tracker', () => { delete parameters.resultId; - expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); }); - it('Should throw an error when no autocompleteSection parameter is provided', () => { + it('Should throw an error when no section parameter is provided', () => { const parameters = cloneDeep(validParameters); const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.autocompleteSection; + delete parameters.section; - expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as is parameters.autocompleteSection'); + expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); }); it('Should throw an error when invalid apiKey is provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - return expect(tracker.sendAutocompleteSelect(name, validParameters)) + return expect(tracker.sendAutocompleteSelect(term, validParameters)) .to.eventually.be.rejectedWith('BAD REQUEST') .and.be.an.instanceOf(Error) .notify(done); }); }); - describe('sendAutocompleteSearch', () => { - const name = 'Where The Wild Things Are'; - const validParameters = { - resultId: '123-456-789', - originalQuery: 'books', - }; + //describe('sendAutocompleteSearch', () => { + //const name = 'Where The Wild Things Are'; + //const validParameters = { + //resultId: '123-456-789', + //originalQuery: 'books', + //}; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); + //beforeEach(() => { + //global.CLIENT_VERSION = 'cio-mocha'; + //}); - afterEach(() => { - delete global.CLIENT_VERSION; - }); + //afterEach(() => { + //delete global.CLIENT_VERSION; + //}); - it('Should respond with a valid response when name and parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should respond with a valid response when name and parameters are provided', (done) => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendAutocompleteSearch(name, validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); + //tracker.sendAutocompleteSearch(name, validParameters).then((res) => { + //expect(res).to.equal(true); + //done(); + //}); + //}); - it('Should throw an error when invalid name is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when invalid name is provided', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch([], validParameters)).to.throw('name is a required parameter of type string'); - }); + //expect(() => tracker.sendAutocompleteSearch([], validParameters)).to.throw('name is a required parameter of type string'); + //}); - it('Should throw an error when no name is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no name is provided', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(null, validParameters)).to.throw('name is a required parameter of type string'); - }); + //expect(() => tracker.sendAutocompleteSearch(null, validParameters)).to.throw('name is a required parameter of type string'); + //}); - it('Should throw an error when invalid parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when invalid parameters are provided', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(name, [])).to.throw('parameters is a required object'); - }); + //expect(() => tracker.sendAutocompleteSearch(name, [])).to.throw('parameters is a required object'); + //}); - it('Should throw an error when no parameters are provided ', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no parameters are provided ', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(name)).to.throw('parameters is a required object'); - }); + //expect(() => tracker.sendAutocompleteSearch(name)).to.throw('parameters is a required object'); + //}); - it('Should throw an error when no originalQuery parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no originalQuery parameter is provided', () => { + //const parameters = cloneDeep(validParameters); + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.originalQuery; + //delete parameters.originalQuery; - expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - }); + //expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + //}); - it('Should throw an error when no resultId parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no resultId parameter is provided', () => { + //const parameters = cloneDeep(validParameters); + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.resultId; + //delete parameters.resultId; - expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - }); + //expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + //}); - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + //it('Should throw an error when invalid apiKey is provided', (done) => { + //const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - return expect(tracker.sendAutocompleteSearch(name, validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); - }); - }); + //return expect(tracker.sendAutocompleteSearch(name, validParameters)) + //.to.eventually.be.rejectedWith('BAD REQUEST') + //.and.be.an.instanceOf(Error) + //.notify(done); + //}); + //}); - describe('sendSearchResults', () => { - const validParameters = { - query: 'books', - numResults: 1234, - }; + //describe('sendSearchResults', () => { + //const validParameters = { + //query: 'books', + //numResults: 1234, + //}; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); + //beforeEach(() => { + //global.CLIENT_VERSION = 'cio-mocha'; + //}); - afterEach(() => { - delete global.CLIENT_VERSION; - }); + //afterEach(() => { + //delete global.CLIENT_VERSION; + //}); - it('Should respond with a valid response when parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should respond with a valid response when parameters are provided', (done) => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendSearchResults(validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); + //tracker.sendSearchResults(validParameters).then((res) => { + //expect(res).to.equal(true); + //done(); + //}); + //}); - it('Should respond with a valid response when parameters including customerIds are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should respond with a valid response when parameters including customerIds are provided', (done) => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendSearchResults({ - ...validParameters, - customerIds: ['foo', 'bar', 'baz'], - }).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); + //tracker.sendSearchResults({ + //...validParameters, + //customerIds: ['foo', 'bar', 'baz'], + //}).then((res) => { + //expect(res).to.equal(true); + //done(); + //}); + //}); - it('Should throw an error when invalid parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when invalid parameters are provided', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResults([])).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - }); + //expect(() => tracker.sendSearchResults([])).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + //}); - it('Should throw an error when no parameters are provided ', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no parameters are provided ', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResults()).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - }); + //expect(() => tracker.sendSearchResults()).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + //}); - it('Should throw an error when no query parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no query parameter is provided', () => { + //const parameters = cloneDeep(validParameters); + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.query; + //delete parameters.query; - expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - }); + //expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + //}); - it('Should throw an error when no numResults parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no numResults parameter is provided', () => { + //const parameters = cloneDeep(validParameters); + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.numResults; + //delete parameters.numResults; - expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - }); + //expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); + //}); - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + //it('Should throw an error when invalid apiKey is provided', (done) => { + //const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - return expect(tracker.sendSearchResults(validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); - }); - }); + //return expect(tracker.sendSearchResults(validParameters)) + //.to.eventually.be.rejectedWith('BAD REQUEST') + //.and.be.an.instanceOf(Error) + //.notify(done); + //}); + //}); - describe('sendSearchResultClick', () => { - const query = 'books'; - const validParameters = { - name: 'Where The Wild Things Are', - customerId: 1234, - }; + //describe('sendSearchResultClick', () => { + //const query = 'books'; + //const validParameters = { + //name: 'Where The Wild Things Are', + //customerId: 1234, + //}; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); + //beforeEach(() => { + //global.CLIENT_VERSION = 'cio-mocha'; + //}); - afterEach(() => { - delete global.CLIENT_VERSION; - }); + //afterEach(() => { + //delete global.CLIENT_VERSION; + //}); - it('Should respond with a valid response when query and parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should respond with a valid response when query and parameters are provided', (done) => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendSearchResultClick(query, validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); + //tracker.sendSearchResultClick(query, validParameters).then((res) => { + //expect(res).to.equal(true); + //done(); + //}); + //}); - it('Should throw an error when no query is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no query is provided', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResultClick(null, validParameters)).to.throw('query is a required parameter of type string'); - }); + //expect(() => tracker.sendSearchResultClick(null, validParameters)).to.throw('query is a required parameter of type string'); + //}); - it('Should throw an error when invalid query is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when invalid query is provided', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResultClick([], validParameters)).to.throw('query is a required parameter of type string'); - }); + //expect(() => tracker.sendSearchResultClick([], validParameters)).to.throw('query is a required parameter of type string'); + //}); - it('Should throw an error when invalid parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when invalid parameters are provided', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResultClick(query, [])).to.throw('parameters is a required object'); - }); + //expect(() => tracker.sendSearchResultClick(query, [])).to.throw('parameters is a required object'); + //}); - it('Should throw an error when no parameters are provided ', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no parameters are provided ', () => { + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResultClick(query)).to.throw('parameters is a required object'); - }); + //expect(() => tracker.sendSearchResultClick(query)).to.throw('parameters is a required object'); + //}); - it('Should throw an error when no name parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no name parameter is provided', () => { + //const parameters = cloneDeep(validParameters); + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.name; + //delete parameters.name; - expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); - }); + //expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); + //}); - it('Should throw an error when no customerId parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + //it('Should throw an error when no customerId parameter is provided', () => { + //const parameters = cloneDeep(validParameters); + //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.customerId; + //delete parameters.customerId; - expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); - }); + //expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); + //}); - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + //it('Should throw an error when invalid apiKey is provided', (done) => { + //const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - return expect(tracker.sendSearchResultClick(query, validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); - }); - }); + //return expect(tracker.sendSearchResultClick(query, validParameters)) + //.to.eventually.be.rejectedWith('BAD REQUEST') + //.and.be.an.instanceOf(Error) + //.notify(done); + //}); + //}); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index fde06a16..1c4c3e28 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1,7 +1,8 @@ -/* eslint-disable import/prefer-default-export, object-curly-newline */ +/* eslint-disable import/prefer-default-export, object-curly-newline, no-underscore-dangle */ import qs from 'qs'; import fetchPonyfill from 'fetch-ponyfill'; import Promise from 'es6-promise'; +import utils from '../utils'; const { fetch } = fetchPonyfill({ Promise }); @@ -44,6 +45,7 @@ export function tracker(options) { queryParams.i = clientId; queryParams.s = sessionId; queryParams.action = action; + queryParams._dt = Date.now(); if (parameters) { const { query, numResults, customerIds } = parameters; @@ -69,6 +71,70 @@ export function tracker(options) { return `${serviceUrl}/behavior?${queryString}`; }; + // Create autocomplete select URL from supplied parameters using name in directive + const createAutocompleteSelectUrl = (term, parameters) => { + const { apiKey, version, serviceUrl, sessionId, clientId } = options; + let queryParams = { c: version }; + + // Validate product name is provided + if (!term || typeof term !== 'string') { + throw new Error('term is a required parameter of type string'); + } + + // Validate parameters are supplied and valid + if (!parameters || typeof parameters !== 'object') { + throw new Error('parameters is a required object'); + } + + // Original query, result id and section are required + if (!parameters.originalQuery || !parameters.resultId || !parameters.section) { + throw new Error('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); + } + + queryParams.key = apiKey; + queryParams.i = clientId; + queryParams.s = sessionId; + queryParams._dt = Date.now(); + + if (parameters) { + const { originalQuery, resultId, section, tr, groupId, displayName } = parameters; + + // Pull original query from parameters + if (originalQuery) { + queryParams.original_query = originalQuery; + } + + // Pull result id from parameters + if (resultId) { + queryParams.result_id = resultId; + } + + // Pull section from parameters + if (section) { + queryParams.autocomplete_section = section; + } + + // Pull trigger (tr) from parameters + if (tr) { + queryParams.tr = tr; + } + + // Pull group id and display name from parameters + if (groupId && displayName) { + queryParams.group = { + group_id: groupId, + display_name: displayName, + }; + } + } + + queryParams = utils.cleanParams(queryParams); + + const queryString = qs.stringify(queryParams, { indices: false }); + + return `${serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/select?${queryString}`; + }; + // Create autocomplete URL from supplied parameters using name in directive const createAutocompleteUrlByName = (action, name, parameters) => { const { apiKey, version, serviceUrl, sessionId, clientId } = options; @@ -106,6 +172,7 @@ export function tracker(options) { queryParams.key = apiKey; queryParams.i = clientId; queryParams.s = sessionId; + queryParams._dt = Date.now(); if (parameters) { const { originalQuery, tr, autocompleteSection, resultId } = parameters; @@ -167,6 +234,7 @@ export function tracker(options) { queryParams.key = apiKey; queryParams.i = clientId; queryParams.s = sessionId; + queryParams._dt = Date.now(); if (parameters) { const { name, customerId } = parameters; @@ -228,16 +296,18 @@ export function tracker(options) { * Send autocomplete select event to API * * @function sendAutocompleteSelect - * @param {string} name - Name of selected product + * @param {string} term - term of selected autocomplete item * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.originalQuery - The current autocomplete search query - * @param {string} parameters.resultId - Customer ID of the selected autocomplete item - * @param {string} parameters.autocompleteSection - Autocomplete section the item resides within - * @param {string} [parameters.tr] - Trigger used to select the autocomplete item (click, etc.) + * @param {string} parameters.resultId - Customer id of the selected autocomplete item + * @param {string} parameters.section - Section the selected item resides within + * @param {string} [parameters.tr] - Trigger used to select the item (click, etc.) + * @param {string} [parameters.groupId] - Group identifier of selected item + * @param {string} [parameters.displayName] - Display name of group of selected item * @returns {Promise} */ - sendAutocompleteSelect: (name, parameters) => { - const requestUrl = createAutocompleteUrlByName('select', name, parameters); + sendAutocompleteSelect: (term, parameters) => { + const requestUrl = createAutocompleteSelectUrl(term, parameters); return fetch(requestUrl).then((response) => { if (response.ok) { diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 00000000..f3550e4d --- /dev/null +++ b/src/utils.js @@ -0,0 +1,34 @@ +import qs from 'qs'; + +const utils = { + ourEncodeURIComponent: (str) => { + if (str) { + const parsedStrObj = qs.parse(`s=${str.replace(/&/g, '%26')}`); + parsedStrObj.s = parsedStrObj.s.replace(/\s/g, ' '); + + return qs.stringify(parsedStrObj).split('=')[1]; + } + + return null; + }, + + cleanParams: (paramsObj) => { + const cleanedParams = {}; + + Object.keys(paramsObj).forEach((paramKey) => { + const paramValue = paramsObj[paramKey]; + + if (typeof paramValue === 'string') { + // Replace non-breaking spaces (or any other type of spaces caught by the regex) + // - with a regular white space + cleanedParams[paramKey] = decodeURIComponent(utils.ourEncodeURIComponent(paramValue)); + } else { + cleanedParams[paramKey] = paramValue; + } + }); + + return cleanedParams; + }, +}; + +module.exports = utils; From a48c7a91730a02c53049bb9d88fe48cce40a8666 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 25 Sep 2019 13:58:02 -0600 Subject: [PATCH 10/56] Update tests for sendSearchSubmit. --- spec/src/modules/tracker.js | 129 ++++++++++++++++++++---------------- src/modules/tracker.js | 95 +++++--------------------- 2 files changed, 88 insertions(+), 136 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 04039d26..ac732b8d 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -149,7 +149,7 @@ describe.only('ConstructorIO - Tracker', () => { delete parameters.originalQuery; - expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); + expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); }); it('Should throw an error when no resultId parameter is provided', () => { @@ -158,7 +158,7 @@ describe.only('ConstructorIO - Tracker', () => { delete parameters.resultId; - expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); + expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); }); it('Should throw an error when no section parameter is provided', () => { @@ -167,7 +167,7 @@ describe.only('ConstructorIO - Tracker', () => { delete parameters.section; - expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); + expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as is parameters.section'); }); it('Should throw an error when invalid apiKey is provided', (done) => { @@ -180,81 +180,94 @@ describe.only('ConstructorIO - Tracker', () => { }); }); - //describe('sendAutocompleteSearch', () => { - //const name = 'Where The Wild Things Are'; - //const validParameters = { - //resultId: '123-456-789', - //originalQuery: 'books', - //}; + describe('sendAutocompleteSearch', () => { + const term = 'Where The Wild Things Are'; + const validParameters = { + resultId: '123-456-789', + originalQuery: 'books', + }; - //beforeEach(() => { - //global.CLIENT_VERSION = 'cio-mocha'; - //}); + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); - //afterEach(() => { - //delete global.CLIENT_VERSION; - //}); + afterEach(() => { + delete global.CLIENT_VERSION; + }); - //it('Should respond with a valid response when name and parameters are provided', (done) => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + it('Should respond with a valid response when term and parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //tracker.sendAutocompleteSearch(name, validParameters).then((res) => { - //expect(res).to.equal(true); - //done(); - //}); - //}); + tracker.sendAutocompleteSearch(term, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); - //it('Should throw an error when invalid name is provided', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + it('Should respond with a valid response when term and parameters including group information are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //expect(() => tracker.sendAutocompleteSearch([], validParameters)).to.throw('name is a required parameter of type string'); - //}); + tracker.sendAutocompleteSearch(term, { + ...validParameters, + groupId: 'group-id', + displayName: 'display-name', + }).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); - //it('Should throw an error when no name is provided', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + it('Should throw an error when invalid term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //expect(() => tracker.sendAutocompleteSearch(null, validParameters)).to.throw('name is a required parameter of type string'); - //}); + expect(() => tracker.sendAutocompleteSearch([], validParameters)).to.throw('term is a required parameter of type string'); + }); - //it('Should throw an error when invalid parameters are provided', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + it('Should throw an error when no term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //expect(() => tracker.sendAutocompleteSearch(name, [])).to.throw('parameters is a required object'); - //}); + expect(() => tracker.sendAutocompleteSearch(null, validParameters)).to.throw('term is a required parameter of type string'); + }); - //it('Should throw an error when no parameters are provided ', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //expect(() => tracker.sendAutocompleteSearch(name)).to.throw('parameters is a required object'); - //}); + expect(() => tracker.sendAutocompleteSearch(term, [])).to.throw('parameters is a required object'); + }); - //it('Should throw an error when no originalQuery parameter is provided', () => { - //const parameters = cloneDeep(validParameters); - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + it('Should throw an error when no parameters are provided ', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //delete parameters.originalQuery; + expect(() => tracker.sendAutocompleteSearch(term)).to.throw('parameters is a required object'); + }); - //expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - //}); + it('Should throw an error when no originalQuery parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //it('Should throw an error when no resultId parameter is provided', () => { - //const parameters = cloneDeep(validParameters); - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + delete parameters.originalQuery; - //delete parameters.resultId; + expect(() => tracker.sendAutocompleteSearch(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); - //expect(() => tracker.sendAutocompleteSearch(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - //}); + it('Should throw an error when no resultId parameter is provided', () => { + const parameters = cloneDeep(validParameters); + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - //it('Should throw an error when invalid apiKey is provided', (done) => { - //const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + delete parameters.resultId; - //return expect(tracker.sendAutocompleteSearch(name, validParameters)) - //.to.eventually.be.rejectedWith('BAD REQUEST') - //.and.be.an.instanceOf(Error) - //.notify(done); - //}); - //}); + expect(() => tracker.sendAutocompleteSearch(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendAutocompleteSearch(term, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); //describe('sendSearchResults', () => { //const validParameters = { diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 1c4c3e28..7704e65d 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -71,8 +71,8 @@ export function tracker(options) { return `${serviceUrl}/behavior?${queryString}`; }; - // Create autocomplete select URL from supplied parameters using name in directive - const createAutocompleteSelectUrl = (term, parameters) => { + // Create autocomplete select URL from supplied parameters using term in directive + const createAutocompleteUrl = (action, term, parameters) => { const { apiKey, version, serviceUrl, sessionId, clientId } = options; let queryParams = { c: version }; @@ -87,8 +87,13 @@ export function tracker(options) { } // Original query, result id and section are required - if (!parameters.originalQuery || !parameters.resultId || !parameters.section) { - throw new Error('parameters is a required object, as are parameters.originalQuery, parameters.resultId, parameters.section'); + if (!parameters.originalQuery || !parameters.resultId) { + throw new Error('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + } + + // Section is required for select + if (action === 'select' && !parameters.section) { + throw new Error('parameters is a required object, as is parameters.section'); } queryParams.key = apiKey; @@ -132,75 +137,7 @@ export function tracker(options) { const queryString = qs.stringify(queryParams, { indices: false }); - return `${serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/select?${queryString}`; - }; - - // Create autocomplete URL from supplied parameters using name in directive - const createAutocompleteUrlByName = (action, name, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId } = options; - const queryParams = { c: version }; - const validActions = [ - 'select', - 'search', - ]; - - // Ensure supplied action is valid - if (!action || validActions.indexOf(action) === -1) { - throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); - } - - // Validate product name is provided - if (!name || typeof name !== 'string') { - throw new Error('name is a required parameter of type string'); - } - - // Validate parameters are supplied and valid - if (!parameters || typeof parameters !== 'object') { - throw new Error('parameters is a required object'); - } - - // Original query and result id are required for 'select' and 'search' actions - if (!parameters.originalQuery || !parameters.resultId) { - throw new Error('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - } - - // Autocomplete section is required for 'select' actions - if (action === 'select' && !parameters.autocompleteSection) { - throw new Error('parameters is a required object, as is parameters.autocompleteSection'); - } - - queryParams.key = apiKey; - queryParams.i = clientId; - queryParams.s = sessionId; - queryParams._dt = Date.now(); - - if (parameters) { - const { originalQuery, tr, autocompleteSection, resultId } = parameters; - - // Pull original query from parameters (select, search) - if (originalQuery) { - queryParams.original_query = originalQuery; - } - - // Pull trigger (tr) from parameters (select) - if (tr) { - queryParams.tr = tr; - } - - // Pull autocomplete section from parameters (select) - if (autocompleteSection) { - queryParams.autocomplete_section = autocompleteSection; - } - - // Pull result id from parameters (select, search) - if (resultId) { - queryParams.result_id = resultId; - } - } - - const queryString = qs.stringify(queryParams, { indices: false }); - - return `${serviceUrl}/autocomplete/${encodeURIComponent(name)}/${action}?${queryString}`; + return `${serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/${action}?${queryString}`; }; // Create autocomplete URL from supplied parameters using query in directive @@ -296,7 +233,7 @@ export function tracker(options) { * Send autocomplete select event to API * * @function sendAutocompleteSelect - * @param {string} term - term of selected autocomplete item + * @param {string} term - Term of selected autocomplete item * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.originalQuery - The current autocomplete search query * @param {string} parameters.resultId - Customer id of the selected autocomplete item @@ -307,7 +244,7 @@ export function tracker(options) { * @returns {Promise} */ sendAutocompleteSelect: (term, parameters) => { - const requestUrl = createAutocompleteSelectUrl(term, parameters); + const requestUrl = createAutocompleteUrl('select', term, parameters); return fetch(requestUrl).then((response) => { if (response.ok) { @@ -322,14 +259,16 @@ export function tracker(options) { * Send autocomplete search event to API * * @function sendAutocompleteSearch - * @param {string} name - Name of selected product + * @param {string} term - Term of submitted autocomplete event * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.originalQuery - The current autocomplete search query * @param {string} parameters.resultId - Customer ID of the selected autocomplete item + * @param {string} [parameters.groupId] - Group identifier of selected item + * @param {string} [parameters.displayName] - Display name of group of selected item * @returns {Promise} */ - sendAutocompleteSearch: (name, parameters) => { - const requestUrl = createAutocompleteUrlByName('search', name, parameters); + sendAutocompleteSearch: (term, parameters) => { + const requestUrl = createAutocompleteUrl('search', term, parameters); return fetch(requestUrl).then((response) => { if (response.ok) { From 1c7b03b55a8cda883ac480709f1126c6a81c5655 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 25 Sep 2019 17:55:21 -0600 Subject: [PATCH 11/56] Loosen restrictions on required parameters, make parameter structure more consistent. --- spec/src/modules/tracker.js | 249 +++++++----------------------------- src/modules/tracker.js | 158 ++++++----------------- 2 files changed, 86 insertions(+), 321 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index ac732b8d..d3040b93 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -88,7 +88,7 @@ describe.only('ConstructorIO - Tracker', () => { it('Should respond with a valid response when term and parameters are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendAutocompleteSelect(term , validParameters).then((res) => { + tracker.sendAutocompleteSelect(term, validParameters).then((res) => { expect(res).to.equal(true); done(); }); @@ -97,7 +97,7 @@ describe.only('ConstructorIO - Tracker', () => { it('Should respond with a valid response when term and parameters including tr are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendAutocompleteSelect(name, { + tracker.sendAutocompleteSelect(term, { ...validParameters, tr: 'click', }).then((res) => { @@ -109,7 +109,7 @@ describe.only('ConstructorIO - Tracker', () => { it('Should respond with a valid response when term and parameters including group information are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendAutocompleteSelect(name, { + tracker.sendAutocompleteSelect(term, { ...validParameters, groupId: 'group-id', displayName: 'display-name', @@ -131,45 +131,6 @@ describe.only('ConstructorIO - Tracker', () => { expect(() => tracker.sendAutocompleteSelect(null, validParameters)).to.throw('term is a required parameter of type string'); }); - it('Should throw an error when invalid parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(() => tracker.sendAutocompleteSelect(term, [])).to.throw('parameters is a required object'); - }); - - it('Should throw an error when no parameters are provided ', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(() => tracker.sendAutocompleteSelect(term)).to.throw('parameters is a required object'); - }); - - it('Should throw an error when no originalQuery parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - delete parameters.originalQuery; - - expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - }); - - it('Should throw an error when no resultId parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - delete parameters.resultId; - - expect(() => tracker.sendAutocompleteSelect(name, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - }); - - it('Should throw an error when no section parameter is provided', () => { - const parameters = cloneDeep(validParameters); - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - delete parameters.section; - - expect(() => tracker.sendAutocompleteSelect(term, parameters)).to.throw('parameters is a required object, as is parameters.section'); - }); - it('Should throw an error when invalid apiKey is provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); @@ -229,194 +190,74 @@ describe.only('ConstructorIO - Tracker', () => { expect(() => tracker.sendAutocompleteSearch(null, validParameters)).to.throw('term is a required parameter of type string'); }); - it('Should throw an error when invalid parameters are provided', () => { + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendAutocompleteSearch(term, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); + + describe('sendSearchResults', () => { + const term = 'Cat in the Hat'; + const validParameters = { numResults: 1234 }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when parameters are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(term, [])).to.throw('parameters is a required object'); + tracker.sendSearchResults(term, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); }); - it('Should throw an error when no parameters are provided ', () => { + it('Should respond with a valid response when parameters including customerIds are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(term)).to.throw('parameters is a required object'); + tracker.sendSearchResults(term, { + ...validParameters, + customerIds: ['foo', 'bar', 'baz'], + }).then((res) => { + expect(res).to.equal(true); + done(); + }); }); - it('Should throw an error when no originalQuery parameter is provided', () => { + it('Should throw an error when invalid term parameter is provided', () => { const parameters = cloneDeep(validParameters); const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.originalQuery; + delete parameters.query; - expect(() => tracker.sendAutocompleteSearch(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendSearchResults([], parameters)).to.throw('term is a required parameter of type string'); }); - it('Should throw an error when no resultId parameter is provided', () => { + it('Should throw an error when no term parameter is provided', () => { const parameters = cloneDeep(validParameters); const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.resultId; + delete parameters.query; - expect(() => tracker.sendAutocompleteSearch(term, parameters)).to.throw('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); + expect(() => tracker.sendSearchResults(null, parameters)).to.throw('term is a required parameter of type string'); }); it('Should throw an error when invalid apiKey is provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - return expect(tracker.sendAutocompleteSearch(term, validParameters)) + return expect(tracker.sendSearchResults(term, validParameters)) .to.eventually.be.rejectedWith('BAD REQUEST') .and.be.an.instanceOf(Error) .notify(done); }); }); - - //describe('sendSearchResults', () => { - //const validParameters = { - //query: 'books', - //numResults: 1234, - //}; - - //beforeEach(() => { - //global.CLIENT_VERSION = 'cio-mocha'; - //}); - - //afterEach(() => { - //delete global.CLIENT_VERSION; - //}); - - //it('Should respond with a valid response when parameters are provided', (done) => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //tracker.sendSearchResults(validParameters).then((res) => { - //expect(res).to.equal(true); - //done(); - //}); - //}); - - //it('Should respond with a valid response when parameters including customerIds are provided', (done) => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //tracker.sendSearchResults({ - //...validParameters, - //customerIds: ['foo', 'bar', 'baz'], - //}).then((res) => { - //expect(res).to.equal(true); - //done(); - //}); - //}); - - //it('Should throw an error when invalid parameters are provided', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //expect(() => tracker.sendSearchResults([])).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - //}); - - //it('Should throw an error when no parameters are provided ', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //expect(() => tracker.sendSearchResults()).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - //}); - - //it('Should throw an error when no query parameter is provided', () => { - //const parameters = cloneDeep(validParameters); - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //delete parameters.query; - - //expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - //}); - - //it('Should throw an error when no numResults parameter is provided', () => { - //const parameters = cloneDeep(validParameters); - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //delete parameters.numResults; - - //expect(() => tracker.sendSearchResults(parameters)).to.throw('parameters is a required object, as is parameters.query and parameters.numResults'); - //}); - - //it('Should throw an error when invalid apiKey is provided', (done) => { - //const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - //return expect(tracker.sendSearchResults(validParameters)) - //.to.eventually.be.rejectedWith('BAD REQUEST') - //.and.be.an.instanceOf(Error) - //.notify(done); - //}); - //}); - - //describe('sendSearchResultClick', () => { - //const query = 'books'; - //const validParameters = { - //name: 'Where The Wild Things Are', - //customerId: 1234, - //}; - - //beforeEach(() => { - //global.CLIENT_VERSION = 'cio-mocha'; - //}); - - //afterEach(() => { - //delete global.CLIENT_VERSION; - //}); - - //it('Should respond with a valid response when query and parameters are provided', (done) => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //tracker.sendSearchResultClick(query, validParameters).then((res) => { - //expect(res).to.equal(true); - //done(); - //}); - //}); - - //it('Should throw an error when no query is provided', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //expect(() => tracker.sendSearchResultClick(null, validParameters)).to.throw('query is a required parameter of type string'); - //}); - - //it('Should throw an error when invalid query is provided', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //expect(() => tracker.sendSearchResultClick([], validParameters)).to.throw('query is a required parameter of type string'); - //}); - - //it('Should throw an error when invalid parameters are provided', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //expect(() => tracker.sendSearchResultClick(query, [])).to.throw('parameters is a required object'); - //}); - - //it('Should throw an error when no parameters are provided ', () => { - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //expect(() => tracker.sendSearchResultClick(query)).to.throw('parameters is a required object'); - //}); - - //it('Should throw an error when no name parameter is provided', () => { - //const parameters = cloneDeep(validParameters); - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //delete parameters.name; - - //expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); - //}); - - //it('Should throw an error when no customerId parameter is provided', () => { - //const parameters = cloneDeep(validParameters); - //const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - //delete parameters.customerId; - - //expect(() => tracker.sendSearchResultClick(query, parameters)).to.throw('parameters is a required object, as are parameters.name and parameters.customerId'); - //}); - - //it('Should throw an error when invalid apiKey is provided', (done) => { - //const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - //return expect(tracker.sendSearchResultClick(query, validParameters)) - //.to.eventually.be.rejectedWith('BAD REQUEST') - //.and.be.an.instanceOf(Error) - //.notify(done); - //}); - //}); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 7704e65d..0b8d361c 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1,4 +1,9 @@ -/* eslint-disable import/prefer-default-export, object-curly-newline, no-underscore-dangle */ +/* eslint-disable + import/prefer-default-export, + object-curly-newline, + no-underscore-dangle, + camelcase +*/ import qs from 'qs'; import fetchPonyfill from 'fetch-ponyfill'; import Promise from 'es6-promise'; @@ -15,13 +20,13 @@ const { fetch } = fetchPonyfill({ Promise }); */ export function tracker(options) { // Create behavior URL from supplied parameters - const createBehaviorUrl = (action, parameters) => { + const createBehaviorUrl = (action, term, parameters) => { const { apiKey, version, serviceUrl, sessionId, clientId } = options; - const queryParams = { c: version }; + let queryParams = { c: version }; const validActions = [ 'session_start', 'focus', - 'search', + 'search-results', ]; // Ensure supplied action is valid @@ -29,16 +34,9 @@ export function tracker(options) { throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); } - // Query (term) and num results are required for 'search' actions - if (action === 'search' - && ( - !parameters - || typeof parameters !== 'object' - || !parameters.query - || !parameters.numResults - ) - ) { - throw new Error('parameters is a required object, as is parameters.query and parameters.numResults'); + // Term is required for 'search' actions + if (action === 'search-results' && typeof term !== 'string') { + throw new Error('term is a required parameter of type string'); } queryParams.key = apiKey; @@ -47,25 +45,27 @@ export function tracker(options) { queryParams.action = action; queryParams._dt = Date.now(); - if (parameters) { - const { query, numResults, customerIds } = parameters; + // Append term to query params (search-results) + if (term) { + queryParams.term = term; + } - // Pull query (term) from parameters (search) - if (query) { - queryParams.term = query; - } + if (parameters) { + const { numResults, customerIds } = parameters; - // Pull number of results from parameters (search) + // Pull number of results from parameters (search-results) if (numResults) { queryParams.num_results = numResults; } - // Pull customer id's from parameters + // Pull customer id's from parameters (search-results) if (customerIds && Array.isArray(customerIds)) { queryParams.customer_ids = customerIds.join(','); } } + queryParams = utils.cleanParams(queryParams); + const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/behavior?${queryString}`; @@ -76,33 +76,26 @@ export function tracker(options) { const { apiKey, version, serviceUrl, sessionId, clientId } = options; let queryParams = { c: version }; - // Validate product name is provided + // Validate term is provided if (!term || typeof term !== 'string') { throw new Error('term is a required parameter of type string'); } - // Validate parameters are supplied and valid - if (!parameters || typeof parameters !== 'object') { - throw new Error('parameters is a required object'); - } - - // Original query, result id and section are required - if (!parameters.originalQuery || !parameters.resultId) { - throw new Error('parameters is a required object, as are parameters.originalQuery and parameters.resultId'); - } - - // Section is required for select - if (action === 'select' && !parameters.section) { - throw new Error('parameters is a required object, as is parameters.section'); - } - queryParams.key = apiKey; queryParams.i = clientId; queryParams.s = sessionId; queryParams._dt = Date.now(); if (parameters) { - const { originalQuery, resultId, section, tr, groupId, displayName } = parameters; + const { + originalQuery, + resultId, + section, + original_section, // eslint-disable-line camelcase + tr, + groupId, + displayName, + } = parameters; // Pull original query from parameters if (originalQuery) { @@ -115,8 +108,9 @@ export function tracker(options) { } // Pull section from parameters - if (section) { - queryParams.autocomplete_section = section; + // - Ideally, original_section should be deprecated and replaced with section + if (section || original_section) { + queryParams.autocomplete_section = section || original_section; } // Pull trigger (tr) from parameters @@ -125,10 +119,10 @@ export function tracker(options) { } // Pull group id and display name from parameters - if (groupId && displayName) { + if (groupId) { queryParams.group = { group_id: groupId, - display_name: displayName, + display_name: displayName || '', }; } } @@ -140,58 +134,6 @@ export function tracker(options) { return `${serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/${action}?${queryString}`; }; - // Create autocomplete URL from supplied parameters using query in directive - const createAutocompleteUrlByQuery = (action, query, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId } = options; - const queryParams = { c: version }; - const validActions = [ - 'click_through', - ]; - - // Ensure supplied action is valid - if (!action || validActions.indexOf(action) === -1) { - throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); - } - - // Validate query (term) is provided - if (!query || typeof query !== 'string') { - throw new Error('query is a required parameter of type string'); - } - - // Validate parameters are supplied and valid - if (!parameters || typeof parameters !== 'object') { - throw new Error('parameters is a required object'); - } - - // Name and customer id are required for 'click_through' actions - if (!parameters.name || !parameters.customerId) { - throw new Error('parameters is a required object, as are parameters.name and parameters.customerId'); - } - - queryParams.key = apiKey; - queryParams.i = clientId; - queryParams.s = sessionId; - queryParams._dt = Date.now(); - - if (parameters) { - const { name, customerId } = parameters; - - // Pull name from parameters (click_through) - if (name) { - queryParams.name = name; - } - - // Pull customer id from parameters (click_through) - if (customerId) { - queryParams.customer_id = customerId; - } - } - - const queryString = qs.stringify(queryParams, { indices: false }); - - return `${serviceUrl}/autocomplete/${encodeURIComponent(query)}/${action}?${queryString}`; - }; - return { /** * Send session start event to API @@ -283,14 +225,14 @@ export function tracker(options) { * Send search results event to API * * @function sendSearchResults + * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.query - The search query (term) * @param {number} parameters.numResults - Number of search results in total * @param {array} [parameters.customerIds] - List of customer item id's returned from search * @returns {Promise} */ - sendSearchResults: (parameters) => { - const requestUrl = createBehaviorUrl('search', parameters); + sendSearchResults: (term, parameters) => { + const requestUrl = createBehaviorUrl('search-results', term, parameters); return fetch(requestUrl).then((response) => { if (response.ok) { @@ -301,26 +243,8 @@ export function tracker(options) { }); }, - /** - * Send click through event to API - * - * @function sendSearchResultClick - * @param {string} query - Current search query (term) - * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.name - The name of the item that was clicked - * @param {string} parameters.customerId - The customer id of the item that was clicked - * @returns {Promise} - */ - sendSearchResultClick: (query, parameters) => { - const requestUrl = createAutocompleteUrlByQuery('click_through', query, parameters); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } + sendSearchResultClick: () => { - throw new Error(response.statusText); - }); }, sendConversion: () => { From e5b9bfbaafd26c9265bb87684a1c0c20132e08a5 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Fri, 27 Sep 2019 14:23:23 -0600 Subject: [PATCH 12/56] Define sendSearchResultClick behavior. --- src/modules/tracker.js | 76 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 0b8d361c..9cc2f68c 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -71,10 +71,20 @@ export function tracker(options) { return `${serviceUrl}/behavior?${queryString}`; }; - // Create autocomplete select URL from supplied parameters using term in directive + // Create autocomplete URL from supplied parameters using term in directive const createAutocompleteUrl = (action, term, parameters) => { const { apiKey, version, serviceUrl, sessionId, clientId } = options; let queryParams = { c: version }; + const validActions = [ + 'select', + 'search', + 'click_through', + ]; + + // Ensure supplied action is valid + if (!action || validActions.indexOf(action) === -1) { + throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); + } // Validate term is provided if (!term || typeof term !== 'string') { @@ -95,36 +105,66 @@ export function tracker(options) { tr, groupId, displayName, + itemId, + item, + name, + itemName, + customerId, } = parameters; - // Pull original query from parameters + // Pull original query from parameters (select, search) if (originalQuery) { queryParams.original_query = originalQuery; } - // Pull result id from parameters + // Pull result id from parameters (select, search, click_through) if (resultId) { queryParams.result_id = resultId; } - // Pull section from parameters + // Pull section from parameters (select) // - Ideally, original_section should be deprecated and replaced with section if (section || original_section) { queryParams.autocomplete_section = section || original_section; } - // Pull trigger (tr) from parameters + // Pull trigger (tr) from parameters (select) if (tr) { queryParams.tr = tr; } - // Pull group id and display name from parameters + // Pull group id and display name from parameters (select, search) if (groupId) { queryParams.group = { group_id: groupId, display_name: displayName || '', }; } + + // Pull item id from parameters (click_through) + if (itemId) { + queryParams.item_id = itemId; + } + + // Pull item from parameters (click_through) + if (item) { + queryParams.item = item; + } + + // Pull name from parameters (click_through) + if (name) { + queryParams.name = name; + } + + // Pull item name from parameters (click_through) + if (itemName) { + queryParams.item_name = itemName; + } + + // Pull customer id from parameters (click_through) + if (customerId) { + queryParams.customer_id = customerId; + } } queryParams = utils.cleanParams(queryParams); @@ -243,8 +283,30 @@ export function tracker(options) { }); }, - sendSearchResultClick: () => { + /** + * Send click through event to API + * + * @function sendSearchResults + * @param {string} term - Search results query term + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Identifier (only send itemId, item, name or itemName) + * @param {string} parameters.item - Identifier (only send itemId, item, name or itemName) + * @param {string} parameters.name - Identifier (only send itemId, item, name or itemName) + * @param {string} parameters.itemName - Identifier (only send itemId, item, name or itemName) + * @param {string} parameters.customerId - Customer id + * @param {string} parameters.resultId - Result id + * @returns {Promise} + */ + sendSearchResultClick: (term, parameters) => { + const requestUrl = createAutocompleteUrl('click_through', term, parameters); + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + + throw new Error(response.statusText); + }); }, sendConversion: () => { From cab2b7f95679dc3e7ba6d7efd4368399d2bf5aa0 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Fri, 27 Sep 2019 14:29:02 -0600 Subject: [PATCH 13/56] Add tests for sendSearchResultClick. --- spec/src/modules/tracker.js | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index d3040b93..e5d4f17c 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -260,4 +260,60 @@ describe.only('ConstructorIO - Tracker', () => { .notify(done); }); }); + + describe('sendSearchResultClick', () => { + const term = 'Where The Wild Things Are'; + const validParameters = { item: '123-456-789' }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when term and parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendSearchResultClick(term, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should respond with a valid response when term and parameters including result id and customer id are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendSearchResultClick(term, { + ...validParameters, + customerId: 'customer-id', + resultId: 'result-id', + }).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResultClick([], validParameters)).to.throw('term is a required parameter of type string'); + }); + + it('Should throw an error when no term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendSearchResultClick(null, validParameters)).to.throw('term is a required parameter of type string'); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendSearchResultClick(term, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); }); From e591d7999c9f30080217637e0fa3ae65d911e178 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Fri, 27 Sep 2019 15:17:05 -0600 Subject: [PATCH 14/56] Defnine sendConversion + tests. --- spec/src/modules/tracker.js | 56 ++++++++++++++++++++++++++++++++++++ src/modules/tracker.js | 57 ++++++++++++++++++++++++++++--------- 2 files changed, 100 insertions(+), 13 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index e5d4f17c..262726e7 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -316,4 +316,60 @@ describe.only('ConstructorIO - Tracker', () => { .notify(done); }); }); + + describe('sendConversion', () => { + const term = 'Where The Wild Things Are'; + const validParameters = { item: '123-456-789' }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when term and parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendConversion(term, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should respond with a valid response when term and parameters including result id and customer id are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendConversion(term, { + ...validParameters, + customerId: 'customer-id', + resultId: 'result-id', + }).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendConversion([], validParameters)).to.throw('term is a required parameter of type string'); + }); + + it('Should throw an error when no term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendConversion(null, validParameters)).to.throw('term is a required parameter of type string'); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendConversion(term, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 9cc2f68c..7ae257e1 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -79,6 +79,7 @@ export function tracker(options) { 'select', 'search', 'click_through', + 'conversion', ]; // Ensure supplied action is valid @@ -110,6 +111,7 @@ export function tracker(options) { name, itemName, customerId, + revenue, } = parameters; // Pull original query from parameters (select, search) @@ -117,7 +119,7 @@ export function tracker(options) { queryParams.original_query = originalQuery; } - // Pull result id from parameters (select, search, click_through) + // Pull result id from parameters (select, search, click_through, conversion) if (resultId) { queryParams.result_id = resultId; } @@ -128,7 +130,7 @@ export function tracker(options) { queryParams.autocomplete_section = section || original_section; } - // Pull trigger (tr) from parameters (select) + // Pull trigger from parameters (select) if (tr) { queryParams.tr = tr; } @@ -141,30 +143,35 @@ export function tracker(options) { }; } - // Pull item id from parameters (click_through) + // Pull item id from parameters (click_through, conversion) if (itemId) { queryParams.item_id = itemId; } - // Pull item from parameters (click_through) + // Pull item from parameters (click_through, conversion) if (item) { queryParams.item = item; } - // Pull name from parameters (click_through) + // Pull name from parameters (click_through, conversion) if (name) { queryParams.name = name; } - // Pull item name from parameters (click_through) + // Pull item name from parameters (click_through, conversion) if (itemName) { queryParams.item_name = itemName; } - // Pull customer id from parameters (click_through) + // Pull customer id from parameters (click_through, conversion) if (customerId) { queryParams.customer_id = customerId; } + + // Pull revenue from parameters (conversion) + if (revenue) { + queryParams.revenue = revenue; + } } queryParams = utils.cleanParams(queryParams); @@ -286,13 +293,13 @@ export function tracker(options) { /** * Send click through event to API * - * @function sendSearchResults + * @function sendSearchResultClick * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.itemId - Identifier (only send itemId, item, name or itemName) - * @param {string} parameters.item - Identifier (only send itemId, item, name or itemName) - * @param {string} parameters.name - Identifier (only send itemId, item, name or itemName) - * @param {string} parameters.itemName - Identifier (only send itemId, item, name or itemName) + * @param {string} parameters.itemId - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.item - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.name - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.itemName - Identifier (send either itemId, item, name or itemName) * @param {string} parameters.customerId - Customer id * @param {string} parameters.resultId - Result id * @returns {Promise} @@ -309,8 +316,32 @@ export function tracker(options) { }); }, - sendConversion: () => { + /** + * Send conversion event to API + * + * @function sendConversion + * @param {string} term - Search results query term + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.itemId - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.item - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.name - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.itemName - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.customerId - Customer id + * @param {string} parameters.resultId - Result id + * @param {string} parameters.revenue - Revenue + * @param {string} parameters.section - Autocomplete section + * @returns {Promise} + */ + sendConversion: (term, parameters) => { + const requestUrl = createAutocompleteUrl('conversion', term, parameters); + + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + throw new Error(response.statusText); + }); }, sendPurchase: () => { From 63e45755ada625b99ed6d26a2062aaaf1d9f9bf9 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Fri, 27 Sep 2019 15:30:10 -0600 Subject: [PATCH 15/56] Implement sendPurchase + tests. --- spec/src/modules/tracker.js | 56 +++++++++++++++++++++++++++++++++++++ src/modules/tracker.js | 31 ++++++++++++++++++-- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 262726e7..ddd87ee6 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -372,4 +372,60 @@ describe.only('ConstructorIO - Tracker', () => { .notify(done); }); }); + + describe('sendPurchase', () => { + const term = 'Where The Wild Things Are'; + const validParameters = { revenue: 123 }; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + }); + + it('Should respond with a valid response when term and parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendPurchase(term, validParameters).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should respond with a valid response when term and parameters including customer ids, and section are provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + tracker.sendPurchase(term, { + ...validParameters, + customerIds: ['foo', 'bar'], + section: 'books', + }).then((res) => { + expect(res).to.equal(true); + done(); + }); + }); + + it('Should throw an error when invalid term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendPurchase([], validParameters)).to.throw('term is a required parameter of type string'); + }); + + it('Should throw an error when no term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(() => tracker.sendPurchase(null, validParameters)).to.throw('term is a required parameter of type string'); + }); + + it('Should throw an error when invalid apiKey is provided', (done) => { + const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); + + return expect(tracker.sendPurchase(term, validParameters)) + .to.eventually.be.rejectedWith('BAD REQUEST') + .and.be.an.instanceOf(Error) + .notify(done); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 7ae257e1..1322e417 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -80,6 +80,7 @@ export function tracker(options) { 'search', 'click_through', 'conversion', + 'purchase', ]; // Ensure supplied action is valid @@ -112,6 +113,7 @@ export function tracker(options) { itemName, customerId, revenue, + customerIds, } = parameters; // Pull original query from parameters (select, search) @@ -124,7 +126,7 @@ export function tracker(options) { queryParams.result_id = resultId; } - // Pull section from parameters (select) + // Pull section from parameters (select, conversion, purchase) // - Ideally, original_section should be deprecated and replaced with section if (section || original_section) { queryParams.autocomplete_section = section || original_section; @@ -168,10 +170,15 @@ export function tracker(options) { queryParams.customer_id = customerId; } - // Pull revenue from parameters (conversion) + // Pull revenue from parameters (conversion, purchase) if (revenue) { queryParams.revenue = revenue; } + + // Pull customer id's from parameters (purchase) + if (customerIds && Array.isArray(customerIds)) { + queryParams.customer_ids = customerIds.join(','); + } } queryParams = utils.cleanParams(queryParams); @@ -344,8 +351,26 @@ export function tracker(options) { }); }, - sendPurchase: () => { + /** + * Send purchase event to API + * + * @function sendPurchase + * @param {object} parameters - Additional parameters to be sent with request + * @param {array} parameters.customerIds - List of customer item id's + * @param {string} parameters.revenue - Revenue + * @param {string} parameters.section - Autocomplete section + * @returns {Promise} + */ + sendPurchase: (parameters) => { + const requestUrl = createAutocompleteUrl('purchase', 'TERM_UNKNOWN', parameters); + + return fetch(requestUrl).then((response) => { + if (response.ok) { + return true; + } + throw new Error(response.statusText); + }); }, }; } From 93ac7cde0040d38f8308b0b0f8a9cafe412e1a2f Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Fri, 27 Sep 2019 15:34:28 -0600 Subject: [PATCH 16/56] Corrections to sendPurchase tests. --- spec/src/modules/tracker.js | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index ddd87ee6..7e3ea5bb 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -374,7 +374,6 @@ describe.only('ConstructorIO - Tracker', () => { }); describe('sendPurchase', () => { - const term = 'Where The Wild Things Are'; const validParameters = { revenue: 123 }; beforeEach(() => { @@ -385,44 +384,32 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term and parameters are provided', (done) => { + it('Should respond with a valid response when parameters are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendPurchase(term, validParameters).then((res) => { + tracker.sendPurchase(validParameters).then((res) => { expect(res).to.equal(true); done(); }); }); - it('Should respond with a valid response when term and parameters including customer ids, and section are provided', (done) => { + it('Should respond with a valid response when parameters including customer ids, and section are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendPurchase(term, { + tracker.sendPurchase({ ...validParameters, customerIds: ['foo', 'bar'], - section: 'books', + section: 'Products', }).then((res) => { expect(res).to.equal(true); done(); }); }); - it('Should throw an error when invalid term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(() => tracker.sendPurchase([], validParameters)).to.throw('term is a required parameter of type string'); - }); - - it('Should throw an error when no term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(() => tracker.sendPurchase(null, validParameters)).to.throw('term is a required parameter of type string'); - }); - it('Should throw an error when invalid apiKey is provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - return expect(tracker.sendPurchase(term, validParameters)) + return expect(tracker.sendPurchase(validParameters)) .to.eventually.be.rejectedWith('BAD REQUEST') .and.be.an.instanceOf(Error) .notify(done); From 607c5cc1684f59fa0bd849b27b70f7c91871b8e9 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Fri, 27 Sep 2019 17:20:21 -0600 Subject: [PATCH 17/56] No longer return promises from tracking calls. Simplify test cases. --- package.json | 4 +- spec/src/modules/tracker.js | 260 ++++-------------------------------- src/modules/tracker.js | 145 ++++++-------------- 3 files changed, 71 insertions(+), 338 deletions(-) diff --git a/package.json b/package.json index 21eec29f..99ac606a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "eslint-plugin-import": "^2.18.2", "http-server": "^0.11.1", "jsdoc": "^3.6.3", - "lodash.clonedeep": "^4.5.0", "minami": "^1.2.3", "mocha": "^6.2.0", "mocha-jsdom": "^2.0.0", @@ -49,6 +48,7 @@ "@constructor-io/constructorio-id": "^2.1.0", "es6-promise": "^4.2.8", "fetch-ponyfill": "^6.1.0", - "qs": "^6.8.0" + "qs": "^6.8.0", + "store2": "^2.9.0" } } diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 7e3ea5bb..890bd311 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -2,7 +2,6 @@ import jsdom from 'mocha-jsdom'; import dotenv from 'dotenv'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import cloneDeep from 'lodash.clonedeep'; import ConstructorIO from '../../../src/constructorio'; chai.use(chaiAsPromised); @@ -10,7 +9,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe.only('ConstructorIO - Tracker', () => { +describe('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { @@ -22,22 +21,10 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response', (done) => { + it('Should respond with a valid response', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendSessionStart().then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendSessionStart()) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendSessionStart()).to.equal(true); }); }); @@ -50,32 +37,15 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response', (done) => { + it('Should respond with a valid response', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendInputFocus().then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendInputFocus()) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendInputFocus()).to.equal(true); }); }); describe('sendAutocompleteSelect', () => { const term = 'Where The Wild Things Are'; - const validParameters = { - section: 'Products', - resultId: '123-456-789', - originalQuery: 'books', - }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -85,68 +55,27 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term and parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - tracker.sendAutocompleteSelect(term, validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should respond with a valid response when term and parameters including tr are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - tracker.sendAutocompleteSelect(term, { - ...validParameters, - tr: 'click', - }).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should respond with a valid response when term and parameters including group information are provided', (done) => { + it('Should respond with a valid response when term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendAutocompleteSelect(term, { - ...validParameters, - groupId: 'group-id', - displayName: 'display-name', - }).then((res) => { - expect(res).to.equal(true); - done(); - }); + expect(tracker.sendAutocompleteSelect(term)).to.equal(true); }); it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect([], validParameters)).to.throw('term is a required parameter of type string'); + expect(tracker.sendAutocompleteSelect([])).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSelect(null, validParameters)).to.throw('term is a required parameter of type string'); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendAutocompleteSelect(term, validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendAutocompleteSelect()).to.be.an('error'); }); }); describe('sendAutocompleteSearch', () => { const term = 'Where The Wild Things Are'; - const validParameters = { - resultId: '123-456-789', - originalQuery: 'books', - }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -156,53 +85,27 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term and parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - tracker.sendAutocompleteSearch(term, validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should respond with a valid response when term and parameters including group information are provided', (done) => { + it('Should respond with a valid response when term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendAutocompleteSearch(term, { - ...validParameters, - groupId: 'group-id', - displayName: 'display-name', - }).then((res) => { - expect(res).to.equal(true); - done(); - }); + expect(tracker.sendAutocompleteSearch(term)).to.equal(true); }); it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch([], validParameters)).to.throw('term is a required parameter of type string'); + expect(tracker.sendAutocompleteSearch([])).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendAutocompleteSearch(null, validParameters)).to.throw('term is a required parameter of type string'); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendAutocompleteSearch(term, validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendAutocompleteSearch()).to.be.an('error'); }); }); describe('sendSearchResults', () => { const term = 'Cat in the Hat'; - const validParameters = { numResults: 1234 }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -212,58 +115,27 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - tracker.sendSearchResults(term, validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should respond with a valid response when parameters including customerIds are provided', (done) => { + it('Should respond with a valid response when term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendSearchResults(term, { - ...validParameters, - customerIds: ['foo', 'bar', 'baz'], - }).then((res) => { - expect(res).to.equal(true); - done(); - }); + expect(tracker.sendSearchResults(term)).to.equal(true); }); it('Should throw an error when invalid term parameter is provided', () => { - const parameters = cloneDeep(validParameters); const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.query; - - expect(() => tracker.sendSearchResults([], parameters)).to.throw('term is a required parameter of type string'); + expect(tracker.sendSearchResults([])).to.be.an('error'); }); it('Should throw an error when no term parameter is provided', () => { - const parameters = cloneDeep(validParameters); const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - delete parameters.query; - - expect(() => tracker.sendSearchResults(null, parameters)).to.throw('term is a required parameter of type string'); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendSearchResults(term, validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendSearchResults()).to.be.an('error'); }); }); describe('sendSearchResultClick', () => { const term = 'Where The Wild Things Are'; - const validParameters = { item: '123-456-789' }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -273,53 +145,27 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term and parameters are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - tracker.sendSearchResultClick(term, validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should respond with a valid response when term and parameters including result id and customer id are provided', (done) => { + it('Should respond with a valid response when term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendSearchResultClick(term, { - ...validParameters, - customerId: 'customer-id', - resultId: 'result-id', - }).then((res) => { - expect(res).to.equal(true); - done(); - }); + expect(tracker.sendSearchResultClick(term)).to.equal(true); }); it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResultClick([], validParameters)).to.throw('term is a required parameter of type string'); + expect(tracker.sendSearchResultClick([])).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendSearchResultClick(null, validParameters)).to.throw('term is a required parameter of type string'); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendSearchResultClick(term, validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendSearchResultClick()).to.be.an('error'); }); }); describe('sendConversion', () => { const term = 'Where The Wild Things Are'; - const validParameters = { item: '123-456-789' }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -329,53 +175,26 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term and parameters are provided', (done) => { + it('Should respond with a valid response when term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendConversion(term, validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should respond with a valid response when term and parameters including result id and customer id are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - tracker.sendConversion(term, { - ...validParameters, - customerId: 'customer-id', - resultId: 'result-id', - }).then((res) => { - expect(res).to.equal(true); - done(); - }); + expect(tracker.sendConversion(term)).to.equal(true); }); it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendConversion([], validParameters)).to.throw('term is a required parameter of type string'); + expect(tracker.sendConversion([])).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(() => tracker.sendConversion(null, validParameters)).to.throw('term is a required parameter of type string'); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendConversion(term, validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendConversion()).to.be.an('error'); }); }); describe('sendPurchase', () => { - const validParameters = { revenue: 123 }; - beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; }); @@ -384,35 +203,10 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when parameters are provided', (done) => { + it('Should respond with a valid response', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - tracker.sendPurchase(validParameters).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should respond with a valid response when parameters including customer ids, and section are provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - tracker.sendPurchase({ - ...validParameters, - customerIds: ['foo', 'bar'], - section: 'Products', - }).then((res) => { - expect(res).to.equal(true); - done(); - }); - }); - - it('Should throw an error when invalid apiKey is provided', (done) => { - const { tracker } = new ConstructorIO({ apiKey: 'fyzs7tfF8L161VoAXQ8u' }); - - return expect(tracker.sendPurchase(validParameters)) - .to.eventually.be.rejectedWith('BAD REQUEST') - .and.be.an.instanceOf(Error) - .notify(done); + expect(tracker.sendPurchase()).to.equal(true); }); }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 1322e417..71f46ed7 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -5,11 +5,21 @@ camelcase */ import qs from 'qs'; -import fetchPonyfill from 'fetch-ponyfill'; -import Promise from 'es6-promise'; +import store from 'store2'; import utils from '../utils'; -const { fetch } = fetchPonyfill({ Promise }); +// Options related to local or session storage +const storageOptions = { + keys: { + searchTerm: { scope: 'session', key: '_constructorio_search_term' }, + autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, + autocompleteEvents: { scope: 'local', key: '_constructorio_autocomplete' }, + recentSearches: { scope: 'local', key: '_constructorio_recent_searches' }, + }, + recentSearchesMaxCount: 100, + integrationTestCookieName: '_constructorio_integration_test', + isHumanCookieName: '_constructorio_is_human', +}; /** * Interface to tracking related API calls. @@ -31,12 +41,12 @@ export function tracker(options) { // Ensure supplied action is valid if (!action || validActions.indexOf(action) === -1) { - throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); + return new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); } // Term is required for 'search' actions if (action === 'search-results' && typeof term !== 'string') { - throw new Error('term is a required parameter of type string'); + return new Error('term is a required parameter of type string'); } queryParams.key = apiKey; @@ -85,12 +95,12 @@ export function tracker(options) { // Ensure supplied action is valid if (!action || validActions.indexOf(action) === -1) { - throw new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); + return new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); } // Validate term is provided if (!term || typeof term !== 'string') { - throw new Error('term is a required parameter of type string'); + return new Error('term is a required parameter of type string'); } queryParams.key = apiKey; @@ -188,42 +198,31 @@ export function tracker(options) { return `${serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/${action}?${queryString}`; }; + // Handle response from URL generation + const handleResponse = (urlResponse) => { + if (urlResponse instanceof Error) { + return urlResponse; + } + + return true; + }; + return { /** * Send session start event to API * * @function sendSessionStart - * @returns {Promise} + * @returns {(true|Error)} */ - sendSessionStart: () => { - const requestUrl = createBehaviorUrl('session_start'); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendSessionStart: () => handleResponse(createBehaviorUrl('session_start')), /** * Send input focus event to API * * @function sendInputFocus - * @returns {Promise} + * @returns {(true|Error)} */ - sendInputFocus: () => { - const requestUrl = createBehaviorUrl('focus'); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendInputFocus: () => handleResponse(createBehaviorUrl('session_start')), /** * Send autocomplete select event to API @@ -237,19 +236,9 @@ export function tracker(options) { * @param {string} [parameters.tr] - Trigger used to select the item (click, etc.) * @param {string} [parameters.groupId] - Group identifier of selected item * @param {string} [parameters.displayName] - Display name of group of selected item - * @returns {Promise} + * @returns {(true|Error)} */ - sendAutocompleteSelect: (term, parameters) => { - const requestUrl = createAutocompleteUrl('select', term, parameters); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendAutocompleteSelect: (term, parameters) => handleResponse(createAutocompleteUrl('select', term, parameters)), /** * Send autocomplete search event to API @@ -261,19 +250,9 @@ export function tracker(options) { * @param {string} parameters.resultId - Customer ID of the selected autocomplete item * @param {string} [parameters.groupId] - Group identifier of selected item * @param {string} [parameters.displayName] - Display name of group of selected item - * @returns {Promise} + * @returns {(true|Error)} */ - sendAutocompleteSearch: (term, parameters) => { - const requestUrl = createAutocompleteUrl('search', term, parameters); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendAutocompleteSearch: (term, parameters) => handleResponse(createAutocompleteUrl('search', term, parameters)), /** * Send search results event to API @@ -283,19 +262,9 @@ export function tracker(options) { * @param {object} parameters - Additional parameters to be sent with request * @param {number} parameters.numResults - Number of search results in total * @param {array} [parameters.customerIds] - List of customer item id's returned from search - * @returns {Promise} + * @returns {(true|Error)} */ - sendSearchResults: (term, parameters) => { - const requestUrl = createBehaviorUrl('search-results', term, parameters); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendSearchResults: (term, parameters) => handleResponse(createBehaviorUrl('search-results', term, parameters)), /** * Send click through event to API @@ -309,19 +278,9 @@ export function tracker(options) { * @param {string} parameters.itemName - Identifier (send either itemId, item, name or itemName) * @param {string} parameters.customerId - Customer id * @param {string} parameters.resultId - Result id - * @returns {Promise} + * @returns {(true|Error)} */ - sendSearchResultClick: (term, parameters) => { - const requestUrl = createAutocompleteUrl('click_through', term, parameters); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendSearchResultClick: (term, parameters) => handleResponse(createAutocompleteUrl('click_through', term, parameters)), /** * Send conversion event to API @@ -337,19 +296,9 @@ export function tracker(options) { * @param {string} parameters.resultId - Result id * @param {string} parameters.revenue - Revenue * @param {string} parameters.section - Autocomplete section - * @returns {Promise} + * @returns {(true|Error)} */ - sendConversion: (term, parameters) => { - const requestUrl = createAutocompleteUrl('conversion', term, parameters); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendConversion: (term, parameters) => handleResponse(createAutocompleteUrl('conversion', term, parameters)), /** * Send purchase event to API @@ -359,18 +308,8 @@ export function tracker(options) { * @param {array} parameters.customerIds - List of customer item id's * @param {string} parameters.revenue - Revenue * @param {string} parameters.section - Autocomplete section - * @returns {Promise} + * @returns {(true|Error)} */ - sendPurchase: (parameters) => { - const requestUrl = createAutocompleteUrl('purchase', 'TERM_UNKNOWN', parameters); - - return fetch(requestUrl).then((response) => { - if (response.ok) { - return true; - } - - throw new Error(response.statusText); - }); - }, + sendPurchase: (parameters) => handleResponse(createAutocompleteUrl('purchase', 'TERM_UNKNOWN', parameters)), }; } From ec32308152ec0b8c637b45d5db13681a1f4f6059 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Mon, 30 Sep 2019 15:28:33 -0600 Subject: [PATCH 18/56] Set term and last used autocomplete item in browser storage. --- src/modules/tracker.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 71f46ed7..cc3b5da1 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -10,13 +10,8 @@ import utils from '../utils'; // Options related to local or session storage const storageOptions = { - keys: { - searchTerm: { scope: 'session', key: '_constructorio_search_term' }, - autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, - autocompleteEvents: { scope: 'local', key: '_constructorio_autocomplete' }, - recentSearches: { scope: 'local', key: '_constructorio_recent_searches' }, - }, - recentSearchesMaxCount: 100, + searchTerm: { scope: 'session', key: '_constructorio_search_term' }, + autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, integrationTestCookieName: '_constructorio_integration_test', isHumanCookieName: '_constructorio_is_human', }; @@ -238,7 +233,16 @@ export function tracker(options) { * @param {string} [parameters.displayName] - Display name of group of selected item * @returns {(true|Error)} */ - sendAutocompleteSelect: (term, parameters) => handleResponse(createAutocompleteUrl('select', term, parameters)), + sendAutocompleteSelect: (term, parameters) => { + const storageOption = storageOptions.autocompleteItem; + + store[storageOption.scope].set(storageOption.key, JSON.stringify({ + item: term, + section: parameters && (parameters.section || parameters.original_section), + })); + + return handleResponse(createAutocompleteUrl('select', term, parameters)); + }, /** * Send autocomplete search event to API @@ -252,7 +256,13 @@ export function tracker(options) { * @param {string} [parameters.displayName] - Display name of group of selected item * @returns {(true|Error)} */ - sendAutocompleteSearch: (term, parameters) => handleResponse(createAutocompleteUrl('search', term, parameters)), + sendAutocompleteSearch: (term, parameters) => { + const storageOption = storageOptions.searchTerm; + + store[storageOption.scope].set(storageOption.key, term); + + return handleResponse(createAutocompleteUrl('search', term, parameters)); + }, /** * Send search results event to API From 974f5c100c6b64cdbccc85cf2df239b834853801 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Mon, 30 Sep 2019 15:52:29 -0600 Subject: [PATCH 19/56] Add tests to ensure storage setting is being performed correctly. --- spec/src/modules/tracker.js | 13 ++++++++++--- src/constructorio.js | 8 ++++++++ src/modules/tracker.js | 12 ++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 890bd311..47af853c 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -2,6 +2,7 @@ import jsdom from 'mocha-jsdom'; import dotenv from 'dotenv'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import store from 'store2'; import ConstructorIO from '../../../src/constructorio'; chai.use(chaiAsPromised); @@ -9,7 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe('ConstructorIO - Tracker', () => { +describe.only('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { @@ -56,9 +57,13 @@ describe('ConstructorIO - Tracker', () => { }); it('Should respond with a valid response when term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker, options } = new ConstructorIO({ apiKey: testApiKey }); + const storageOption = options.storage.autocompleteItem; expect(tracker.sendAutocompleteSelect(term)).to.equal(true); + expect(JSON.parse(store[storageOption.scope].get(storageOption.key))).to.deep.equal({ + item: term, + }); }); it('Should throw an error when invalid term is provided', () => { @@ -86,9 +91,11 @@ describe('ConstructorIO - Tracker', () => { }); it('Should respond with a valid response when term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker, options } = new ConstructorIO({ apiKey: testApiKey }); + const storageOption = options.storage.searchTerm; expect(tracker.sendAutocompleteSearch(term)).to.equal(true); + expect(store[storageOption.scope].get(storageOption.key)).to.deep.equal(term); }); it('Should throw an error when invalid term is provided', () => { diff --git a/src/constructorio.js b/src/constructorio.js index 8bc925fe..4b79511c 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -50,6 +50,14 @@ class ConstructorIO { clientId: clientId || client_id, segments, testCells, + storage: { + searchTerm: { scope: 'session', key: '_constructorio_search_term' }, + autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, + cookies: { + integrationTest: '_constructorio_integration_test', + isHuman: '_constructorio_is_human', + }, + }, }; // Expose modules diff --git a/src/modules/tracker.js b/src/modules/tracker.js index cc3b5da1..433d0252 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -8,14 +8,6 @@ import qs from 'qs'; import store from 'store2'; import utils from '../utils'; -// Options related to local or session storage -const storageOptions = { - searchTerm: { scope: 'session', key: '_constructorio_search_term' }, - autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, - integrationTestCookieName: '_constructorio_integration_test', - isHumanCookieName: '_constructorio_is_human', -}; - /** * Interface to tracking related API calls. * @@ -234,7 +226,7 @@ export function tracker(options) { * @returns {(true|Error)} */ sendAutocompleteSelect: (term, parameters) => { - const storageOption = storageOptions.autocompleteItem; + const storageOption = options.storage.autocompleteItem; store[storageOption.scope].set(storageOption.key, JSON.stringify({ item: term, @@ -257,7 +249,7 @@ export function tracker(options) { * @returns {(true|Error)} */ sendAutocompleteSearch: (term, parameters) => { - const storageOption = storageOptions.searchTerm; + const storageOption = options.storage.searchTerm; store[storageOption.scope].set(storageOption.key, term); From 790551d4a4359a229c75eaab10607aa228c7c2cb Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Mon, 30 Sep 2019 17:07:46 -0600 Subject: [PATCH 20/56] Tack on userId as supplied parameter to constructor, begin to refactor tracking module to match browser-tracking. --- spec/src/constructorio.js | 1 + spec/src/modules/tracker.js | 2 +- src/botlist.js | 361 +++++++++++++++++++++++++++++++++ src/constructorio.js | 4 + src/modules/autocomplete.js | 7 +- src/modules/recommendations.js | 7 +- src/modules/search.js | 7 +- src/modules/tracker.js | 223 ++++---------------- src/utils.js | 9 + 9 files changed, 438 insertions(+), 183 deletions(-) create mode 100644 src/botlist.js diff --git a/spec/src/constructorio.js b/spec/src/constructorio.js index 211165dd..5b051527 100644 --- a/spec/src/constructorio.js +++ b/spec/src/constructorio.js @@ -26,6 +26,7 @@ describe('ConstructorIO', () => { expect(instance.options).to.have.property('serviceUrl'); expect(instance.options).to.have.property('clientId'); expect(instance.options).to.have.property('sessionId'); + expect(instance.options).to.have.property('storage'); }); it('Should return an instance with custom options when valid API key is provided', () => { diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 47af853c..12b3b878 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -10,7 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe.only('ConstructorIO - Tracker', () => { +describe('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { diff --git a/src/botlist.js b/src/botlist.js new file mode 100644 index 00000000..2b03dfc5 --- /dev/null +++ b/src/botlist.js @@ -0,0 +1,361 @@ +/* eslint-disable no-useless-escape */ + +const botList = [ + 'Googlebot\/', + 'Googlebot-Mobile', + 'Googlebot-Image', + 'Googlebot-News', + 'Googlebot-Video', + 'AdsBot-Google([^-]|$)', + 'AdsBot-Google-Mobile', + 'Feedfetcher-Google', + 'Mediapartners-Google', + 'Mediapartners \(Googlebot\)', + 'APIs-Google', + 'bingbot', + 'Slurp', + '[wW]get', + 'curl', + 'LinkedInBot', + 'Python-urllib', + 'python-requests', + 'libwww', + 'httpunit', + 'nutch', + 'Go-http-client', + 'phpcrawl', + 'msnbot', + 'jyxobot', + 'FAST-WebCrawler', + 'FAST Enterprise Crawler', + 'BIGLOTRON', + 'Teoma', + 'convera', + 'seekbot', + 'Gigabot', + 'Gigablast', + 'exabot', + 'ia_archiver', + 'GingerCrawler', + 'webmon ', + 'HTTrack', + 'grub.org', + 'UsineNouvelleCrawler', + 'antibot', + 'netresearchserver', + 'speedy', + 'fluffy', + 'bibnum.bnf', + 'findlink', + 'msrbot', + 'panscient', + 'yacybot', + 'AISearchBot', + 'ips-agent', + 'tagoobot', + 'MJ12bot', + 'woriobot', + 'yanga', + 'buzzbot', + 'mlbot', + 'YandexBot', + 'yandex.com\/bots', + 'purebot', + 'Linguee Bot', + 'CyberPatrol', + 'voilabot', + 'Baiduspider', + 'citeseerxbot', + 'spbot', + 'twengabot', + 'postrank', + 'turnitinbot', + 'scribdbot', + 'page2rss', + 'sitebot', + 'linkdex', + 'Adidxbot', + 'blekkobot', + 'ezooms', + 'dotbot', + 'Mail.RU_Bot', + 'discobot', + 'heritrix', + 'findthatfile', + 'europarchive.org', + 'NerdByNature.Bot', + 'sistrix crawler', + 'Ahrefs(Bot|SiteAudit)', + 'fuelbot', + 'CrunchBot', + 'centurybot9', + 'IndeedBot', + 'mappydata', + 'woobot', + 'ZoominfoBot', + 'PrivacyAwareBot', + 'Multiviewbot', + 'SWIMGBot', + 'Grobbot', + 'eright', + 'Apercite', + 'semanticbot', + 'Aboundex', + 'domaincrawler', + 'wbsearchbot', + 'summify', + 'CCBot', + 'edisterbot', + 'seznambot', + 'ec2linkfinder', + 'gslfbot', + 'aiHitBot', + 'intelium_bot', + 'facebookexternalhit', + 'Yeti', + 'RetrevoPageAnalyzer', + 'lb-spider', + 'Sogou', + 'lssbot', + 'careerbot', + 'wotbox', + 'wocbot', + 'ichiro', + 'DuckDuckBot', + 'lssrocketcrawler', + 'drupact', + 'webcompanycrawler', + 'acoonbot', + 'openindexspider', + 'gnam gnam spider', + 'web-archive-net.com.bot', + 'backlinkcrawler', + 'coccoc', + 'integromedb', + 'content crawler spider', + 'toplistbot', + 'it2media-domain-crawler', + 'ip-web-crawler.com', + 'siteexplorer.info', + 'elisabot', + 'proximic', + 'changedetection', + 'arabot', + 'WeSEE:Search', + 'niki-bot', + 'CrystalSemanticsBot', + 'rogerbot', + '360Spider', + 'psbot', + 'InterfaxScanBot', + 'CC Metadata Scaper', + 'g00g1e.net', + 'GrapeshotCrawler', + 'urlappendbot', + 'brainobot', + 'fr-crawler', + 'binlar', + 'SimpleCrawler', + 'Twitterbot', + 'cXensebot', + 'smtbot', + 'bnf.fr_bot', + 'A6-Indexer', + 'ADmantX', + 'Facebot', + 'OrangeBot\/', + 'memorybot', + 'AdvBot', + 'MegaIndex', + 'SemanticScholarBot', + 'ltx71', + 'nerdybot', + 'xovibot', + 'BUbiNG', + 'Qwantify', + 'archive.org_bot', + 'Applebot', + 'TweetmemeBot', + 'crawler4j', + 'findxbot', + 'S[eE][mM]rushBot', + 'yoozBot', + 'lipperhey', + 'Y!J', + 'Domain Re-Animator Bot', + 'AddThis', + 'Screaming Frog SEO Spider', + 'MetaURI', + 'Scrapy', + 'Livelap[bB]ot', + 'OpenHoseBot', + 'CapsuleChecker', + 'collection@infegy.com', + 'IstellaBot', + 'DeuSu\/', + 'betaBot', + 'Cliqzbot\/', + 'MojeekBot\/', + 'netEstate NE Crawler', + 'SafeSearch microdata crawler', + 'Gluten Free Crawler\/', + 'Sonic', + 'Sysomos', + 'Trove', + 'deadlinkchecker', + 'Slack-ImgProxy', + 'Embedly', + 'RankActiveLinkBot', + 'iskanie', + 'SafeDNSBot', + 'SkypeUriPreview', + 'Veoozbot', + 'Slackbot', + 'redditbot', + 'datagnionbot', + 'Google-Adwords-Instant', + 'adbeat_bot', + 'WhatsApp', + 'contxbot', + 'pinterest', + 'electricmonk', + 'GarlikCrawler', + 'BingPreview\/', + 'vebidoobot', + 'FemtosearchBot', + 'Yahoo Link Preview', + 'MetaJobBot', + 'DomainStatsBot', + 'mindUpBot', + 'Daum\/', + 'Jugendschutzprogramm-Crawler', + 'Xenu Link Sleuth', + 'Pcore-HTTP', + 'moatbot', + 'KosmioBot', + 'pingdom', + 'PhantomJS', + 'Gowikibot', + 'PiplBot', + 'Discordbot', + 'TelegramBot', + 'Jetslide', + 'newsharecounts', + 'James BOT', + 'Barkrowler', + 'TinEye', + 'SocialRankIOBot', + 'trendictionbot', + 'Ocarinabot', + 'epicbot', + 'Primalbot', + 'DuckDuckGo-Favicons-Bot', + 'GnowitNewsbot', + 'Leikibot', + 'LinkArchiver', + 'YaK\/', + 'PaperLiBot', + 'Digg Deeper', + 'dcrawl', + 'Snacktory', + 'AndersPinkBot', + 'Fyrebot', + 'EveryoneSocialBot', + 'Mediatoolkitbot', + 'Luminator-robots', + 'ExtLinksBot', + 'SurveyBot', + 'NING\/', + 'okhttp', + 'Nuzzel', + 'omgili', + 'PocketParser', + 'YisouSpider', + 'um-LN', + 'ToutiaoSpider', + 'MuckRack', + "Jamie's Spider", + 'AHC\/', + 'NetcraftSurveyAgent', + 'Laserlikebot', + 'Apache-HttpClient', + 'AppEngine-Google', + 'Jetty', + 'Upflow', + 'Thinklab', + 'Traackr.com', + 'Twurly', + 'Mastodon', + 'http_get', + 'DnyzBot', + 'botify', + '007ac9 Crawler', + 'BehloolBot', + 'BrandVerity', + 'check_http', + 'BDCbot', + 'ZumBot', + 'EZID', + 'ICC-Crawler', + 'ArchiveBot', + '^LCC ', + 'filterdb.iss.net\/crawler', + 'BLP_bbot', + 'BomboraBot', + 'Buck\/', + 'Companybook-Crawler', + 'Genieo', + 'magpie-crawler', + 'MeltwaterNews', + 'Moreover', + 'newspaper\/', + 'ScoutJet', + '(^| )sentry\/', + 'StorygizeBot', + 'UptimeRobot', + 'OutclicksBot', + 'seoscanners', + 'Hatena', + 'Google Web Preview', + 'MauiBot', + 'AlphaBot', + 'SBL-BOT', + 'IAS crawler', + 'adscanner', + 'Netvibes', + 'acapbot', + 'Baidu-YunGuanCe', + 'bitlybot', + 'blogmuraBot', + 'Bot.AraTurka.com', + 'bot-pge.chlooe.com', + 'BoxcarBot', + 'BTWebClient', + 'ContextAd Bot', + 'Digincore bot', + 'Disqus', + 'Feedly', + 'Fetch\/', + 'Fever', + 'Flamingo_SearchEngine', + 'FlipboardProxy', + 'g2reader-bot', + 'imrbot', + 'K7MLWCBot', + 'Kemvibot', + 'Landau-Media-Spider', + 'linkapediabot', + 'vkShare', + 'Siteimprove.com', + 'BLEXBot\/', + 'DareBoost', + 'ZuperlistBot\/', + 'Miniflux\/', + 'Feedspotbot\/', + 'Diffbot\/', + 'SEOkicks', + 'tracemyfile', + 'Nimbostratus-Bot', +]; + +module.exports = botList; diff --git a/src/constructorio.js b/src/constructorio.js index 4b79511c..865fa33c 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -19,6 +19,7 @@ class ConstructorIO { * @param {object} [testCells] - User test cells * @param {string} [clientId] - Client ID, defaults to value supplied by 'constructorio-id' * @param {string} [sessionId] - Session id, defaults to value supplied by 'constructorio-id' + * @param {string} [userId] - User id * @property {object} [search] - Interface to {@link module:search} * @property {object} [autocomplete] - Interface to {@link module:autocomplete} * @property {object} [recommendations] - Interface to {@link module:recommendations} @@ -33,6 +34,7 @@ class ConstructorIO { testCells, clientId, sessionId, + userId, } = options; if (!apiKey || typeof apiKey !== 'string') { @@ -48,11 +50,13 @@ class ConstructorIO { serviceUrl: serviceUrl || 'https://ac.cnstrc.com', sessionId: sessionId || session_id, clientId: clientId || client_id, + userId: userId, segments, testCells, storage: { searchTerm: { scope: 'session', key: '_constructorio_search_term' }, autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, + events: { scope: 'local', key: '_constructorio_events'}, cookies: { integrationTest: '_constructorio_integration_test', isHuman: '_constructorio_is_human', diff --git a/src/modules/autocomplete.js b/src/modules/autocomplete.js index ec49e7cd..1965d1d5 100644 --- a/src/modules/autocomplete.js +++ b/src/modules/autocomplete.js @@ -15,7 +15,7 @@ const { fetch } = fetchPonyfill({ Promise }); export function autocomplete(options) { // Create URL from supplied query (term) and parameters const createAutocompleteUrl = (query, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId, segments, testCells } = options; + const { apiKey, version, serviceUrl, sessionId, clientId, userId, segments, testCells } = options; const queryParams = { c: version }; queryParams.key = apiKey; @@ -39,6 +39,11 @@ export function autocomplete(options) { queryParams.us = segments; } + // Pull user id from options + if (userId) { + queryParams.ui = userId; + } + if (parameters) { const { results, resultsPerSection, filters } = parameters; diff --git a/src/modules/recommendations.js b/src/modules/recommendations.js index 44a09113..c258d138 100644 --- a/src/modules/recommendations.js +++ b/src/modules/recommendations.js @@ -15,7 +15,7 @@ const { fetch } = fetchPonyfill({ Promise }); export function recommendations(options) { // Create URL from supplied parameters const createRecommendationsUrl = (parameters, endpoint) => { - const { apiKey, version, serviceUrl, sessionId, clientId, segments } = options; + const { apiKey, version, serviceUrl, sessionId, userId, clientId, segments } = options; const queryParams = { c: version }; const validEndpoints = [ 'alternative_items', @@ -38,6 +38,11 @@ export function recommendations(options) { queryParams.us = segments; } + // Pull user id from options + if (userId) { + queryParams.ui = userId; + } + if (parameters) { const { results, itemIds } = parameters; diff --git a/src/modules/search.js b/src/modules/search.js index aadc3079..306aae6f 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -15,7 +15,7 @@ const { fetch } = fetchPonyfill({ Promise }); export function search(options) { // Create URL from supplied query (term) and parameters const createSearchUrl = (query, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId, segments, testCells } = options; + const { apiKey, version, serviceUrl, sessionId, clientId, userId, segments, testCells } = options; const queryParams = { c: version }; queryParams.key = apiKey; @@ -39,6 +39,11 @@ export function search(options) { queryParams.us = segments; } + // Pull user id from options + if (userId) { + queryParams.ui = userId; + } + if (parameters) { const { page, resultsPerPage, filters, sortBy, sortOrder, section } = parameters; diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 433d0252..a508cc80 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -16,183 +16,46 @@ import utils from '../utils'; * @returns {object} */ export function tracker(options) { - // Create behavior URL from supplied parameters - const createBehaviorUrl = (action, term, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId } = options; - let queryParams = { c: version }; - const validActions = [ - 'session_start', - 'focus', - 'search-results', - ]; - - // Ensure supplied action is valid - if (!action || validActions.indexOf(action) === -1) { - return new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); - } - - // Term is required for 'search' actions - if (action === 'search-results' && typeof term !== 'string') { - return new Error('term is a required parameter of type string'); + // Add request to queue to be dispatched + const queueRequest = (request) => { + if (!utils.isBot()) { + //this.requestQueue.push(request); } + } - queryParams.key = apiKey; - queryParams.i = clientId; - queryParams.s = sessionId; - queryParams.action = action; - queryParams._dt = Date.now(); + // Append common parameters to supplied parameters object + const createQueryString = (queryParamsObj) => { + const { apiKey, version, serviceUrl, sessionId, clientId, userId, segments, testCells } = options; + const paramsObj = Object.assign(queryParamsObj); - // Append term to query params (search-results) - if (term) { - queryParams.term = term; + if (version) { + paramsObj.c = version; } - if (parameters) { - const { numResults, customerIds } = parameters; - - // Pull number of results from parameters (search-results) - if (numResults) { - queryParams.num_results = numResults; - } - - // Pull customer id's from parameters (search-results) - if (customerIds && Array.isArray(customerIds)) { - queryParams.customer_ids = customerIds.join(','); - } + if (clientId) { + paramsObj.i = clientId; } - queryParams = utils.cleanParams(queryParams); - - const queryString = qs.stringify(queryParams, { indices: false }); - - return `${serviceUrl}/behavior?${queryString}`; - }; - - // Create autocomplete URL from supplied parameters using term in directive - const createAutocompleteUrl = (action, term, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId } = options; - let queryParams = { c: version }; - const validActions = [ - 'select', - 'search', - 'click_through', - 'conversion', - 'purchase', - ]; - - // Ensure supplied action is valid - if (!action || validActions.indexOf(action) === -1) { - return new Error(`action is a required parameter and must be one of the following strings: ${validActions.join(', ')}`); + if (sessionId) { + paramsObj.s = sessionId; } - // Validate term is provided - if (!term || typeof term !== 'string') { - return new Error('term is a required parameter of type string'); + if (userId) { + paramsObj.ui = userId; } - queryParams.key = apiKey; - queryParams.i = clientId; - queryParams.s = sessionId; - queryParams._dt = Date.now(); - - if (parameters) { - const { - originalQuery, - resultId, - section, - original_section, // eslint-disable-line camelcase - tr, - groupId, - displayName, - itemId, - item, - name, - itemName, - customerId, - revenue, - customerIds, - } = parameters; - - // Pull original query from parameters (select, search) - if (originalQuery) { - queryParams.original_query = originalQuery; - } - - // Pull result id from parameters (select, search, click_through, conversion) - if (resultId) { - queryParams.result_id = resultId; - } - - // Pull section from parameters (select, conversion, purchase) - // - Ideally, original_section should be deprecated and replaced with section - if (section || original_section) { - queryParams.autocomplete_section = section || original_section; - } - - // Pull trigger from parameters (select) - if (tr) { - queryParams.tr = tr; - } - - // Pull group id and display name from parameters (select, search) - if (groupId) { - queryParams.group = { - group_id: groupId, - display_name: displayName || '', - }; - } - - // Pull item id from parameters (click_through, conversion) - if (itemId) { - queryParams.item_id = itemId; - } - - // Pull item from parameters (click_through, conversion) - if (item) { - queryParams.item = item; - } - - // Pull name from parameters (click_through, conversion) - if (name) { - queryParams.name = name; - } - - // Pull item name from parameters (click_through, conversion) - if (itemName) { - queryParams.item_name = itemName; - } - - // Pull customer id from parameters (click_through, conversion) - if (customerId) { - queryParams.customer_id = customerId; - } - - // Pull revenue from parameters (conversion, purchase) - if (revenue) { - queryParams.revenue = revenue; - } - - // Pull customer id's from parameters (purchase) - if (customerIds && Array.isArray(customerIds)) { - queryParams.customer_ids = customerIds.join(','); - } + if (segments && segments.length) { + paramsObj.us = segments; } - queryParams = utils.cleanParams(queryParams); - - const queryString = qs.stringify(queryParams, { indices: false }); - - return `${serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/${action}?${queryString}`; - }; - - // Handle response from URL generation - const handleResponse = (urlResponse) => { - if (urlResponse instanceof Error) { - return urlResponse; + if (apiKey) { + paramsObj.key = apiKey; } - return true; - }; + paramsObj._dt = Date.now(); + + return qs.stringify(paramsObj, { indices: false }); + } return { /** @@ -201,7 +64,14 @@ export function tracker(options) { * @function sendSessionStart * @returns {(true|Error)} */ - sendSessionStart: () => handleResponse(createBehaviorUrl('session_start')), + sendSessionStart: () => { + const url = `${options.serviceUrl}/behavior?`; + const queryParamsObj = { action: 'session_start' }; + const queryString = createQueryString(queryParamsObj); + + queueRequest(`${url}${queryString}`); + //this.sendRequests(); + }, /** * Send input focus event to API @@ -209,7 +79,9 @@ export function tracker(options) { * @function sendInputFocus * @returns {(true|Error)} */ - sendInputFocus: () => handleResponse(createBehaviorUrl('session_start')), + sendInputFocus: () => { + + }, /** * Send autocomplete select event to API @@ -226,14 +98,7 @@ export function tracker(options) { * @returns {(true|Error)} */ sendAutocompleteSelect: (term, parameters) => { - const storageOption = options.storage.autocompleteItem; - store[storageOption.scope].set(storageOption.key, JSON.stringify({ - item: term, - section: parameters && (parameters.section || parameters.original_section), - })); - - return handleResponse(createAutocompleteUrl('select', term, parameters)); }, /** @@ -249,11 +114,7 @@ export function tracker(options) { * @returns {(true|Error)} */ sendAutocompleteSearch: (term, parameters) => { - const storageOption = options.storage.searchTerm; - - store[storageOption.scope].set(storageOption.key, term); - return handleResponse(createAutocompleteUrl('search', term, parameters)); }, /** @@ -266,7 +127,8 @@ export function tracker(options) { * @param {array} [parameters.customerIds] - List of customer item id's returned from search * @returns {(true|Error)} */ - sendSearchResults: (term, parameters) => handleResponse(createBehaviorUrl('search-results', term, parameters)), + sendSearchResults: (term, parameters) => { + }, /** * Send click through event to API @@ -282,7 +144,8 @@ export function tracker(options) { * @param {string} parameters.resultId - Result id * @returns {(true|Error)} */ - sendSearchResultClick: (term, parameters) => handleResponse(createAutocompleteUrl('click_through', term, parameters)), + sendSearchResultClick: (term, parameters) => { + }, /** * Send conversion event to API @@ -300,7 +163,8 @@ export function tracker(options) { * @param {string} parameters.section - Autocomplete section * @returns {(true|Error)} */ - sendConversion: (term, parameters) => handleResponse(createAutocompleteUrl('conversion', term, parameters)), + sendConversion: (term, parameters) => { + }, /** * Send purchase event to API @@ -312,6 +176,7 @@ export function tracker(options) { * @param {string} parameters.section - Autocomplete section * @returns {(true|Error)} */ - sendPurchase: (parameters) => handleResponse(createAutocompleteUrl('purchase', 'TERM_UNKNOWN', parameters)), + sendPurchase: (parameters) => { + }, }; } diff --git a/src/utils.js b/src/utils.js index f3550e4d..6f615f10 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,9 +1,11 @@ import qs from 'qs'; +import botList from './botlist'; const utils = { ourEncodeURIComponent: (str) => { if (str) { const parsedStrObj = qs.parse(`s=${str.replace(/&/g, '%26')}`); + parsedStrObj.s = parsedStrObj.s.replace(/\s/g, ' '); return qs.stringify(parsedStrObj).split('=')[1]; @@ -29,6 +31,13 @@ const utils = { return cleanedParams; }, + + isBot: () => { + const { userAgent, webdriver } = window && window.navigator; + const botRegex = new RegExp(`(${botList.join('|')})`); + + return Boolean(userAgent.match(botRegex)) || Boolean(webdriver); + } }; module.exports = utils; From 3fee69a39db6f01e0f5d7cee2394390f000d263b Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 1 Oct 2019 10:13:28 -0600 Subject: [PATCH 21/56] Implement tracking request functionality inspired by `browser-tracking` repo. --- spec/src/modules/tracker.js | 2 +- src/constructorio.js | 4 +- src/modules/autocomplete.js | 11 +++++- src/modules/search.js | 11 +++++- src/modules/tracker.js | 76 ++++++++++++++++++++++++++++++++++--- src/utils.js | 2 +- 6 files changed, 94 insertions(+), 12 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 12b3b878..47af853c 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -10,7 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe('ConstructorIO - Tracker', () => { +describe.only('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { diff --git a/src/constructorio.js b/src/constructorio.js index 865fa33c..6e80704c 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -50,13 +50,13 @@ class ConstructorIO { serviceUrl: serviceUrl || 'https://ac.cnstrc.com', sessionId: sessionId || session_id, clientId: clientId || client_id, - userId: userId, + userId, segments, testCells, storage: { searchTerm: { scope: 'session', key: '_constructorio_search_term' }, autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, - events: { scope: 'local', key: '_constructorio_events'}, + requests: { scope: 'local', key: '_constructorio_requests' }, cookies: { integrationTest: '_constructorio_integration_test', isHuman: '_constructorio_is_human', diff --git a/src/modules/autocomplete.js b/src/modules/autocomplete.js index 1965d1d5..717f0b9d 100644 --- a/src/modules/autocomplete.js +++ b/src/modules/autocomplete.js @@ -15,7 +15,16 @@ const { fetch } = fetchPonyfill({ Promise }); export function autocomplete(options) { // Create URL from supplied query (term) and parameters const createAutocompleteUrl = (query, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId, userId, segments, testCells } = options; + const { + apiKey, + version, + serviceUrl, + sessionId, + clientId, + userId, + segments, + testCells, + } = options; const queryParams = { c: version }; queryParams.key = apiKey; diff --git a/src/modules/search.js b/src/modules/search.js index 306aae6f..3a7e1c2f 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -15,7 +15,16 @@ const { fetch } = fetchPonyfill({ Promise }); export function search(options) { // Create URL from supplied query (term) and parameters const createSearchUrl = (query, parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId, userId, segments, testCells } = options; + const { + apiKey, + version, + serviceUrl, + sessionId, + clientId, + userId, + segments, + testCells, + } = options; const queryParams = { c: version }; queryParams.key = apiKey; diff --git a/src/modules/tracker.js b/src/modules/tracker.js index a508cc80..bb75648a 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -6,8 +6,23 @@ */ import qs from 'qs'; import store from 'store2'; +import fetchPonyfill from 'fetch-ponyfill'; +import Promise from 'es6-promise'; import utils from '../utils'; +const { fetch } = fetchPonyfill({ Promise }); +const humanEvents = [ + 'scroll', + 'resize', + 'touchmove', + 'mouseover', + 'mousemove', + 'keydown', + 'keypress', + 'keyup', + 'focus', +]; + /** * Interface to tracking related API calls. * @@ -16,16 +31,63 @@ import utils from '../utils'; * @returns {object} */ export function tracker(options) { + const requestsStorage = options.storage.requests; + let requestPending = false; + let flushScheduled = false; + let isHuman = false; + const requestQueue = store[requestsStorage.scope].get(requestsStorage.key) || []; + + // Bind event handlers for humanity detection and unload (invoked on instantiation) + (() => { + // Humanity proved, remove handlers to prove humanity + const remove = () => { + isHuman = true; + + humanEvents.forEach((eventType) => { + window.removeEventListener(eventType, remove, true); + }); + }; + + // Add handlers to prove humanity + humanEvents.forEach((eventType) => { + window.addEventListener(eventType, remove, true); + }); + + // Flush requests to storage on unload + window.addEventListener('beforeunload', () => { + flushScheduled = true; + + store[requestsStorage.scope].set(requestsStorage.key, requestQueue); + }); + })(); + // Add request to queue to be dispatched const queueRequest = (request) => { if (!utils.isBot()) { - //this.requestQueue.push(request); + requestQueue.push(request); } - } + }; + + // Read from queue and send requests to server + const sendRequests = () => { + if (isHuman && requestQueue.length && !requestPending && flushScheduled) { + const nextInQueue = requestQueue.shift(); + const request = fetch(nextInQueue); + + if (request) { + requestPending = true; + + request.finally(() => { + requestPending = false; + sendRequests(); + }); + } + } + }; // Append common parameters to supplied parameters object const createQueryString = (queryParamsObj) => { - const { apiKey, version, serviceUrl, sessionId, clientId, userId, segments, testCells } = options; + const { apiKey, version, sessionId, clientId, userId, segments } = options; const paramsObj = Object.assign(queryParamsObj); if (version) { @@ -55,14 +117,14 @@ export function tracker(options) { paramsObj._dt = Date.now(); return qs.stringify(paramsObj, { indices: false }); - } + }; return { /** * Send session start event to API * * @function sendSessionStart - * @returns {(true|Error)} + * @returns {true} */ sendSessionStart: () => { const url = `${options.serviceUrl}/behavior?`; @@ -70,7 +132,9 @@ export function tracker(options) { const queryString = createQueryString(queryParamsObj); queueRequest(`${url}${queryString}`); - //this.sendRequests(); + sendRequests(); + + return true; }, /** diff --git a/src/utils.js b/src/utils.js index 6f615f10..e2cb373e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -37,7 +37,7 @@ const utils = { const botRegex = new RegExp(`(${botList.join('|')})`); return Boolean(userAgent.match(botRegex)) || Boolean(webdriver); - } + }, }; module.exports = utils; From 65bafb3242dbdf796d0b5d38500f88107f107abb Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 1 Oct 2019 10:39:40 -0600 Subject: [PATCH 22/56] Split requests and humanity into separate modules. --- package.json | 3 +- spec/src/modules/tracker.js | 4 ++ src/modules/tracker-humanity.js | 34 +++++++++++++++ src/modules/tracker-requests.js | 48 +++++++++++++++++++++ src/modules/tracker.js | 74 ++------------------------------- 5 files changed, 92 insertions(+), 71 deletions(-) create mode 100644 src/modules/tracker-humanity.js create mode 100644 src/modules/tracker-requests.js diff --git a/package.json b/package.json index 99ac606a..33e3dff2 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "mocha": "^6.2.0", "mocha-jsdom": "^2.0.0", "nyc": "^14.1.1", - "pre-push": "^0.1.1" + "pre-push": "^0.1.1", + "simulant": "^0.2.2" }, "dependencies": { "@constructor-io/constructorio-id": "^2.1.0", diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 47af853c..0cf27be6 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -13,6 +13,10 @@ const testApiKey = process.env.TEST_API_KEY; describe.only('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); + describe('initialization', () => { + + }); + describe('sendSessionStart', () => { beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; diff --git a/src/modules/tracker-humanity.js b/src/modules/tracker-humanity.js new file mode 100644 index 00000000..6d9f4e24 --- /dev/null +++ b/src/modules/tracker-humanity.js @@ -0,0 +1,34 @@ +const humanEvents = [ + 'scroll', + 'resize', + 'touchmove', + 'mouseover', + 'mousemove', + 'keydown', + 'keypress', + 'keyup', + 'focus', +]; + +export default function trackerHumanity() { + let isHumanBoolean = false; + + // Humanity proved, remove handlers to prove humanity + const remove = () => { + isHumanBoolean = true; + + humanEvents.forEach((eventType) => { + window.removeEventListener(eventType, remove, true); + }); + }; + + // Add handlers to prove humanity + humanEvents.forEach((eventType) => { + window.addEventListener(eventType, remove, true); + }); + + return { + // Return boolean indicating if is human + isHuman: () => isHumanBoolean, + }; +} diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js new file mode 100644 index 00000000..a3f35528 --- /dev/null +++ b/src/modules/tracker-requests.js @@ -0,0 +1,48 @@ +import store from 'store2'; +import fetchPonyfill from 'fetch-ponyfill'; +import Promise from 'es6-promise'; +import utils from '../utils'; +import trackerHumanity from './tracker-humanity'; + +const { fetch } = fetchPonyfill({ Promise }); + +export default function trackerRequests(options) { + const humanity = trackerHumanity(options); + const requestsStorage = options.storage.requests; + let requestPending = false; + let flushScheduled = false; + const requestQueue = store[requestsStorage.scope].get(requestsStorage.key) || []; + + // Flush requests to storage on unload + window.addEventListener('beforeunload', () => { + flushScheduled = true; + + store[requestsStorage.scope].set(requestsStorage.key, requestQueue); + }); + + return { + // Add request to queue to be dispatched + queue: (request) => { + if (!utils.isBot()) { + requestQueue.push(request); + } + }, + + // Read from queue and send requests to server + send: () => { + if (humanity.isHuman() && requestQueue.length && !requestPending && flushScheduled) { + const nextInQueue = requestQueue.shift(); + const request = fetch(nextInQueue); + + if (request) { + requestPending = true; + + request.finally(() => { + requestPending = false; + trackerRequests.send(); + }); + } + } + }, + }; +} diff --git a/src/modules/tracker.js b/src/modules/tracker.js index bb75648a..a216ee27 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -6,22 +6,8 @@ */ import qs from 'qs'; import store from 'store2'; -import fetchPonyfill from 'fetch-ponyfill'; -import Promise from 'es6-promise'; import utils from '../utils'; - -const { fetch } = fetchPonyfill({ Promise }); -const humanEvents = [ - 'scroll', - 'resize', - 'touchmove', - 'mouseover', - 'mousemove', - 'keydown', - 'keypress', - 'keyup', - 'focus', -]; +import trackerRequests from './tracker-requests'; /** * Interface to tracking related API calls. @@ -31,59 +17,7 @@ const humanEvents = [ * @returns {object} */ export function tracker(options) { - const requestsStorage = options.storage.requests; - let requestPending = false; - let flushScheduled = false; - let isHuman = false; - const requestQueue = store[requestsStorage.scope].get(requestsStorage.key) || []; - - // Bind event handlers for humanity detection and unload (invoked on instantiation) - (() => { - // Humanity proved, remove handlers to prove humanity - const remove = () => { - isHuman = true; - - humanEvents.forEach((eventType) => { - window.removeEventListener(eventType, remove, true); - }); - }; - - // Add handlers to prove humanity - humanEvents.forEach((eventType) => { - window.addEventListener(eventType, remove, true); - }); - - // Flush requests to storage on unload - window.addEventListener('beforeunload', () => { - flushScheduled = true; - - store[requestsStorage.scope].set(requestsStorage.key, requestQueue); - }); - })(); - - // Add request to queue to be dispatched - const queueRequest = (request) => { - if (!utils.isBot()) { - requestQueue.push(request); - } - }; - - // Read from queue and send requests to server - const sendRequests = () => { - if (isHuman && requestQueue.length && !requestPending && flushScheduled) { - const nextInQueue = requestQueue.shift(); - const request = fetch(nextInQueue); - - if (request) { - requestPending = true; - - request.finally(() => { - requestPending = false; - sendRequests(); - }); - } - } - }; + const requests = trackerRequests(options); // Append common parameters to supplied parameters object const createQueryString = (queryParamsObj) => { @@ -131,8 +65,8 @@ export function tracker(options) { const queryParamsObj = { action: 'session_start' }; const queryString = createQueryString(queryParamsObj); - queueRequest(`${url}${queryString}`); - sendRequests(); + requests.queue(`${url}${queryString}`); + requests.send(); return true; }, From 334eda7417c147a105c6350af3b71ca25958e253 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 1 Oct 2019 11:35:25 -0600 Subject: [PATCH 23/56] Define behavior for sendAutocompleteSelect + tests. --- spec/src/modules/tracker.js | 4 --- src/modules/tracker-requests.js | 14 +++++---- src/modules/tracker.js | 55 ++++++++++++++++++++++++++++++++- src/utils.js | 2 +- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 0cf27be6..47af853c 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -13,10 +13,6 @@ const testApiKey = process.env.TEST_API_KEY; describe.only('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); - describe('initialization', () => { - - }); - describe('sendSessionStart', () => { beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js index a3f35528..23c1feab 100644 --- a/src/modules/tracker-requests.js +++ b/src/modules/tracker-requests.js @@ -8,16 +8,16 @@ const { fetch } = fetchPonyfill({ Promise }); export default function trackerRequests(options) { const humanity = trackerHumanity(options); - const requestsStorage = options.storage.requests; + const storageOption = options.storage.requests; let requestPending = false; let flushScheduled = false; - const requestQueue = store[requestsStorage.scope].get(requestsStorage.key) || []; + const requestQueue = store[storageOption.scope].get(storageOption.key) || []; // Flush requests to storage on unload window.addEventListener('beforeunload', () => { flushScheduled = true; - store[requestsStorage.scope].set(requestsStorage.key, requestQueue); + store[storageOption.scope].set(storageOption.key, requestQueue); }); return { @@ -29,8 +29,9 @@ export default function trackerRequests(options) { }, // Read from queue and send requests to server - send: () => { - if (humanity.isHuman() && requestQueue.length && !requestPending && flushScheduled) { + // - Note: Must not be fat-arrow function to keep context + send: function send() { + if (humanity.isHuman() && requestQueue.length && !requestPending && !flushScheduled) { const nextInQueue = requestQueue.shift(); const request = fetch(nextInQueue); @@ -39,7 +40,8 @@ export default function trackerRequests(options) { request.finally(() => { requestPending = false; - trackerRequests.send(); + + this.send(); }); } } diff --git a/src/modules/tracker.js b/src/modules/tracker.js index a216ee27..879a264d 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -75,10 +75,17 @@ export function tracker(options) { * Send input focus event to API * * @function sendInputFocus - * @returns {(true|Error)} + * @returns {true} */ sendInputFocus: () => { + const url = `${options.serviceUrl}/behavior?`; + const queryParamsObj = { action: 'focus' }; + const queryString = createQueryString(queryParamsObj); + requests.queue(`${url}${queryString}`); + requests.send(); + + return true; }, /** @@ -96,7 +103,53 @@ export function tracker(options) { * @returns {(true|Error)} */ sendAutocompleteSelect: (term, parameters) => { + if (term && typeof term === 'string') { + const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/select?`; + const queryParamsObj = {}; + const storageOption = options.storage.autocompleteItem; + + if (parameters) { + const { originalQuery, resultId, section, original_section, tr, groupId, displayName } = parameters; + + if (originalQuery) { + queryParamsObj.original_query = originalQuery; + } + + if (tr) { + queryParamsObj.tr = tr; + } + + if (section || original_section) { + queryParamsObj.autocomplete_section = section || original_section; + } + + if (groupId) { + queryParamsObj.group = { + group_id: groupId, + display_name: displayName || '', + }; + } + + if (resultId) { + queryParamsObj.result_id = resultId; + } + } + + const queryString = createQueryString(queryParamsObj); + + requests.queue(`${url}${queryString}`); + requests.send(); + + // Store last term in browser storage + store[storageOption.scope].set(storageOption.key, JSON.stringify({ + item: term, + section: parameters && (parameters.section || parameters.original_section), + })); + + return true; + } + return new Error('term is a required parameter of type string'); }, /** diff --git a/src/utils.js b/src/utils.js index e2cb373e..5caa3253 100644 --- a/src/utils.js +++ b/src/utils.js @@ -40,4 +40,4 @@ const utils = { }, }; -module.exports = utils; +export default utils; From f1e15d85befb7cd451f5d02c358f496a9b176cab Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 1 Oct 2019 11:43:06 -0600 Subject: [PATCH 24/56] Define sendAutocompleteSearch + tests. --- src/modules/tracker.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 879a264d..72b7905e 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -140,7 +140,7 @@ export function tracker(options) { requests.queue(`${url}${queryString}`); requests.send(); - // Store last term in browser storage + // Store term and section in browser storage store[storageOption.scope].set(storageOption.key, JSON.stringify({ item: term, section: parameters && (parameters.section || parameters.original_section), @@ -165,7 +165,42 @@ export function tracker(options) { * @returns {(true|Error)} */ sendAutocompleteSearch: (term, parameters) => { + if (term && typeof term === 'string') { + const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/search?`; + const queryParamsObj = {}; + const storageOption = options.storage.searchTerm; + + if (parameters) { + const { originalQuery, resultId, groupId, displayName } = parameters; + + if (originalQuery) { + queryParamsObj.original_query = originalQuery; + } + + if (groupId) { + queryParamsObj.group = { + group_id: groupId, + display_name: displayName, + }; + } + if (resultId) { + queryParamsObj.result_id = resultId; + } + } + + const queryString = createQueryString(queryParamsObj); + + requests.queue(`${url}${queryString}`); + requests.send(); + + // Store term in browser storage + store[storageOption.scope].set(storageOption.key, term); + + return true + } + + return new Error('term is a required parameter of type string'); }, /** From 83477fff1baa6eab9370c34b82e72cc90c3205b4 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 1 Oct 2019 13:37:40 -0600 Subject: [PATCH 25/56] Update tests and define remaining methods. --- spec/src/modules/tracker.js | 36 --------- src/modules/tracker.js | 151 +++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 40 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 47af853c..eb8a8317 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -127,18 +127,6 @@ describe.only('ConstructorIO - Tracker', () => { expect(tracker.sendSearchResults(term)).to.equal(true); }); - - it('Should throw an error when invalid term parameter is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.sendSearchResults([])).to.be.an('error'); - }); - - it('Should throw an error when no term parameter is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.sendSearchResults()).to.be.an('error'); - }); }); describe('sendSearchResultClick', () => { @@ -157,18 +145,6 @@ describe.only('ConstructorIO - Tracker', () => { expect(tracker.sendSearchResultClick(term)).to.equal(true); }); - - it('Should throw an error when invalid term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.sendSearchResultClick([])).to.be.an('error'); - }); - - it('Should throw an error when no term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.sendSearchResultClick()).to.be.an('error'); - }); }); describe('sendConversion', () => { @@ -187,18 +163,6 @@ describe.only('ConstructorIO - Tracker', () => { expect(tracker.sendConversion(term)).to.equal(true); }); - - it('Should throw an error when invalid term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.sendConversion([])).to.be.an('error'); - }); - - it('Should throw an error when no term is provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - - expect(tracker.sendConversion()).to.be.an('error'); - }); }); describe('sendPurchase', () => { diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 72b7905e..06ee34aa 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -58,7 +58,7 @@ export function tracker(options) { * Send session start event to API * * @function sendSessionStart - * @returns {true} + * @returns {(true|Error)} */ sendSessionStart: () => { const url = `${options.serviceUrl}/behavior?`; @@ -75,7 +75,7 @@ export function tracker(options) { * Send input focus event to API * * @function sendInputFocus - * @returns {true} + * @returns {(true|Error)} */ sendInputFocus: () => { const url = `${options.serviceUrl}/behavior?`; @@ -109,7 +109,15 @@ export function tracker(options) { const storageOption = options.storage.autocompleteItem; if (parameters) { - const { originalQuery, resultId, section, original_section, tr, groupId, displayName } = parameters; + const { + originalQuery, + resultId, + section, + original_section, + tr, + groupId, + displayName, + } = parameters; if (originalQuery) { queryParamsObj.original_query = originalQuery; @@ -197,7 +205,7 @@ export function tracker(options) { // Store term in browser storage store[storageOption.scope].set(storageOption.key, term); - return true + return true; } return new Error('term is a required parameter of type string'); @@ -214,6 +222,27 @@ export function tracker(options) { * @returns {(true|Error)} */ sendSearchResults: (term, parameters) => { + const url = `${options.serviceUrl}/behavior?`; + const queryParamsObj = { action: 'search-results', term }; + + if (parameters) { + const { numResults, customerIds } = parameters; + + if (numResults) { + queryParamsObj.num_results = numResults; + } + + if (customerIds && Array.isArray(customerIds)) { + queryParamsObj.customer_ids = customerIds.join(','); + } + } + + const queryString = createQueryString(queryParamsObj); + + requests.queue(`${url}${queryString}`); + requests.send(); + + return true; }, /** @@ -231,6 +260,44 @@ export function tracker(options) { * @returns {(true|Error)} */ sendSearchResultClick: (term, parameters) => { + const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/click_through?`; + const queryParamsObj = {}; + + if (parameters && Object.keys(parameters).length > 0) { + const { itemId, item, name, itemName, customerId, resultId } = parameters; + + if (itemId) { + queryParamsObj.item_id = itemId; + } + + if (item) { + queryParamsObj.item = item; + } + + if (name) { + queryParamsObj.name = name; + } + + if (itemName) { + queryParamsObj.item_name = itemName; + } + + if (customerId) { + queryParamsObj.customer_id = customerId; + } + + if (resultId) { + queryParamsObj.result_id = resultId; + } + + const queryString = createQueryString(queryParamsObj); + + requests.queue(`${url}${queryString}`); + } + + requests.send(); + + return true; }, /** @@ -250,6 +317,60 @@ export function tracker(options) { * @returns {(true|Error)} */ sendConversion: (term, parameters) => { + // eslint-disable-next-line + const lastSearchTerm = store[options.storage.searchTerm.scope] + .get(options.storage.searchTerm.key); + // eslint-disable-next-line + const lastSelectedItemData = JSON.parse(store[options.storage.autocompleteItem.scope] + .get(options.storage.autocompleteItem.key)); + + const searchTerm = utils.ourEncodeURIComponent(term) || 'TERM_UNKNOWN'; + const url = `${options.serviceUrl}/autocomplete/${searchTerm}/conversion?`; + const queryParamsObj = {}; + + if (parameters && Object.keys(parameters).length > 0) { + const { itemId, item, name, itemName, customerId, resultId, revenue, section } = parameters; + + if (itemId) { + queryParamsObj.item_id = itemId; + } + + if (item) { + queryParamsObj.item = item; + } + + if (name) { + queryParamsObj.name = name; + } + + if (itemName) { + queryParamsObj.item_name = itemName; + } + + if (customerId) { + queryParamsObj.customer_id = customerId; + } + + if (resultId) { + queryParamsObj.result_id = resultId; + } + + if (revenue) { + queryParamsObj.revenue = revenue; + } + + if (section) { + queryParamsObj.autocomplete_section = section; + } + + const queryString = createQueryString(queryParamsObj); + + requests.queue(`${url}${queryString}`); + } + + requests.send(); + + return true; }, /** @@ -263,6 +384,28 @@ export function tracker(options) { * @returns {(true|Error)} */ sendPurchase: (parameters) => { + const url = `${options.serviceUrl}/autocomplete/TERM_UNKNOWN/purchase?`; + const queryParamsObj = {}; + + if (parameters && Object.keys(parameters).length > 0) { + const { customerIds, section } = parameters; + + if (customerIds) { + queryParamsObj.customer_ids = customerIds; + } + + if (section) { + queryParamsObj.autocomplete_section = section; + } + + const queryString = createQueryString(queryParamsObj); + + requests.queue(`${url}${queryString}`); + } + + requests.send(); + + return true; }, }; } From aafcae6822c8c49f99724679638c3fee8506d41e Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 1 Oct 2019 15:28:44 -0600 Subject: [PATCH 26/56] Add tests for tracker-humanity. --- spec/mocha.helpers.js | 32 ++++++++++++++++++++++ spec/src/modules/tracker-humanity.js | 40 ++++++++++++++++++++++++++++ spec/src/modules/tracker.js | 2 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 spec/mocha.helpers.js create mode 100644 spec/src/modules/tracker-humanity.js diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js new file mode 100644 index 00000000..5fe2107e --- /dev/null +++ b/spec/mocha.helpers.js @@ -0,0 +1,32 @@ +const { JSDOM } = require('jsdom'); + +const setupDOM = () => { + const { window } = new JSDOM(''); + + global.window = window; + global.document = window.document; +}; + +const teardownDOM = () => { + delete global.window; + delete global.document; +} + +const triggerResize = () => { + const resizeEvent = document.createEvent('Event'); + resizeEvent.initEvent('resize', true, true); + + global.window.resizeTo = (width, height) => { + global.window.innerWidth = width || global.window.innerWidth; + global.window.innerHeight = width || global.window.innerHeight; + global.window.dispatchEvent(resizeEvent); + }; + + window.resizeTo(1024, 768); +}; + +module.exports = { + setupDOM, + teardownDOM, + triggerResize, +}; diff --git a/spec/src/modules/tracker-humanity.js b/spec/src/modules/tracker-humanity.js new file mode 100644 index 00000000..f34133f2 --- /dev/null +++ b/spec/src/modules/tracker-humanity.js @@ -0,0 +1,40 @@ +import jsdom from 'mocha-jsdom'; +import dotenv from 'dotenv'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +const { setupDOM, teardownDOM, triggerResize } = require('../../mocha.helpers.js'); +import trackerHumanity from '../../../src/modules/tracker-humanity'; + +chai.use(chaiAsPromised); +dotenv.config(); + +const testApiKey = process.env.TEST_API_KEY; + +describe.only('ConstructorIO - Tracker - Humanity', () => { + describe('isHuman', () => { + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + setupDOM(); + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + teardownDOM(); + }); + + it('Should not have isHuman flag set on initial instantiation', () => { + const humanity = trackerHumanity(); + + expect(humanity.isHuman()).to.equal(false); + }); + + it('Should have isHuman flag set if human-like actions are detected', () => { + const humanity = trackerHumanity(); + const input = document.querySelector('input'); + + triggerResize(); + + expect(humanity.isHuman()).to.equal(true); + }); + }); +}); diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index eb8a8317..ce539fcd 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -10,7 +10,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe.only('ConstructorIO - Tracker', () => { +describe('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { From cf45e221da5bb51176932d1aede064c6b35856f5 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 2 Oct 2019 11:04:27 -0600 Subject: [PATCH 27/56] Define test file for tracker-requests. Lint fixes. --- package.json | 4 ++-- spec/mocha.helpers.js | 7 ++++--- spec/src/modules/tracker-humanity.js | 9 +++------ spec/src/modules/tracker-requests.js | 9 +++++++++ src/modules/tracker-requests.js | 3 +++ 5 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 spec/src/modules/tracker-requests.js diff --git a/package.json b/package.json index 33e3dff2..6f82208d 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,12 @@ "eslint-plugin-import": "^2.18.2", "http-server": "^0.11.1", "jsdoc": "^3.6.3", + "jsdom": "^15.1.1", "minami": "^1.2.3", "mocha": "^6.2.0", "mocha-jsdom": "^2.0.0", "nyc": "^14.1.1", - "pre-push": "^0.1.1", - "simulant": "^0.2.2" + "pre-push": "^0.1.1" }, "dependencies": { "@constructor-io/constructorio-id": "^2.1.0", diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js index 5fe2107e..ea61c508 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -1,7 +1,7 @@ const { JSDOM } = require('jsdom'); const setupDOM = () => { - const { window } = new JSDOM(''); + const { window } = new JSDOM(); global.window = window; global.document = window.document; @@ -10,15 +10,16 @@ const setupDOM = () => { const teardownDOM = () => { delete global.window; delete global.document; -} +}; const triggerResize = () => { const resizeEvent = document.createEvent('Event'); + resizeEvent.initEvent('resize', true, true); global.window.resizeTo = (width, height) => { global.window.innerWidth = width || global.window.innerWidth; - global.window.innerHeight = width || global.window.innerHeight; + global.window.innerHeight = height || global.window.innerHeight; global.window.dispatchEvent(resizeEvent); }; diff --git a/spec/src/modules/tracker-humanity.js b/spec/src/modules/tracker-humanity.js index f34133f2..b0809176 100644 --- a/spec/src/modules/tracker-humanity.js +++ b/spec/src/modules/tracker-humanity.js @@ -1,16 +1,14 @@ -import jsdom from 'mocha-jsdom'; import dotenv from 'dotenv'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -const { setupDOM, teardownDOM, triggerResize } = require('../../mocha.helpers.js'); import trackerHumanity from '../../../src/modules/tracker-humanity'; +const { setupDOM, teardownDOM, triggerResize } = require('../../mocha.helpers.js'); + chai.use(chaiAsPromised); dotenv.config(); -const testApiKey = process.env.TEST_API_KEY; - -describe.only('ConstructorIO - Tracker - Humanity', () => { +describe('ConstructorIO - Tracker - Humanity', () => { describe('isHuman', () => { beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -30,7 +28,6 @@ describe.only('ConstructorIO - Tracker - Humanity', () => { it('Should have isHuman flag set if human-like actions are detected', () => { const humanity = trackerHumanity(); - const input = document.querySelector('input'); triggerResize(); diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js new file mode 100644 index 00000000..cdc0e1ce --- /dev/null +++ b/spec/src/modules/tracker-requests.js @@ -0,0 +1,9 @@ +import dotenv from 'dotenv'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +chai.use(chaiAsPromised); +dotenv.config(); + +describe('ConstructorIO - Tracker - Requests', () => { +}); diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js index 23c1feab..64661286 100644 --- a/src/modules/tracker-requests.js +++ b/src/modules/tracker-requests.js @@ -46,5 +46,8 @@ export default function trackerRequests(options) { } } }, + + // Return current queue + get: () => requestQueue, }; } From 2f615f834a887677c44d86c924d4a09f41eae46a Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 2 Oct 2019 11:42:07 -0600 Subject: [PATCH 28/56] Remove storage options block and all downstream use cases. --- spec/src/constructorio.js | 1 - spec/src/modules/tracker.js | 11 ++--------- src/constructorio.js | 9 --------- src/modules/tracker-requests.js | 10 +++++----- src/modules/tracker.js | 19 ------------------- 5 files changed, 7 insertions(+), 43 deletions(-) diff --git a/spec/src/constructorio.js b/spec/src/constructorio.js index 5b051527..211165dd 100644 --- a/spec/src/constructorio.js +++ b/spec/src/constructorio.js @@ -26,7 +26,6 @@ describe('ConstructorIO', () => { expect(instance.options).to.have.property('serviceUrl'); expect(instance.options).to.have.property('clientId'); expect(instance.options).to.have.property('sessionId'); - expect(instance.options).to.have.property('storage'); }); it('Should return an instance with custom options when valid API key is provided', () => { diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index ce539fcd..d596874d 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -2,7 +2,6 @@ import jsdom from 'mocha-jsdom'; import dotenv from 'dotenv'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import store from 'store2'; import ConstructorIO from '../../../src/constructorio'; chai.use(chaiAsPromised); @@ -57,13 +56,9 @@ describe('ConstructorIO - Tracker', () => { }); it('Should respond with a valid response when term is provided', () => { - const { tracker, options } = new ConstructorIO({ apiKey: testApiKey }); - const storageOption = options.storage.autocompleteItem; + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); expect(tracker.sendAutocompleteSelect(term)).to.equal(true); - expect(JSON.parse(store[storageOption.scope].get(storageOption.key))).to.deep.equal({ - item: term, - }); }); it('Should throw an error when invalid term is provided', () => { @@ -91,11 +86,9 @@ describe('ConstructorIO - Tracker', () => { }); it('Should respond with a valid response when term is provided', () => { - const { tracker, options } = new ConstructorIO({ apiKey: testApiKey }); - const storageOption = options.storage.searchTerm; + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); expect(tracker.sendAutocompleteSearch(term)).to.equal(true); - expect(store[storageOption.scope].get(storageOption.key)).to.deep.equal(term); }); it('Should throw an error when invalid term is provided', () => { diff --git a/src/constructorio.js b/src/constructorio.js index 6e80704c..e0cb3984 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -53,15 +53,6 @@ class ConstructorIO { userId, segments, testCells, - storage: { - searchTerm: { scope: 'session', key: '_constructorio_search_term' }, - autocompleteItem: { scope: 'session', key: '_constructorio_selected_item' }, - requests: { scope: 'local', key: '_constructorio_requests' }, - cookies: { - integrationTest: '_constructorio_integration_test', - isHuman: '_constructorio_is_human', - }, - }, }; // Expose modules diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js index 64661286..8187ccd0 100644 --- a/src/modules/tracker-requests.js +++ b/src/modules/tracker-requests.js @@ -6,18 +6,18 @@ import trackerHumanity from './tracker-humanity'; const { fetch } = fetchPonyfill({ Promise }); -export default function trackerRequests(options) { - const humanity = trackerHumanity(options); - const storageOption = options.storage.requests; +export default function trackerRequests() { + const humanity = trackerHumanity(); + const storageKey = '_constructorio_requests'; let requestPending = false; let flushScheduled = false; - const requestQueue = store[storageOption.scope].get(storageOption.key) || []; + const requestQueue = store.local.get(storageKey) || []; // Flush requests to storage on unload window.addEventListener('beforeunload', () => { flushScheduled = true; - store[storageOption.scope].set(storageOption.key, requestQueue); + store.local.set(storageKey, requestQueue); }); return { diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 06ee34aa..623c6fac 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -5,7 +5,6 @@ camelcase */ import qs from 'qs'; -import store from 'store2'; import utils from '../utils'; import trackerRequests from './tracker-requests'; @@ -106,7 +105,6 @@ export function tracker(options) { if (term && typeof term === 'string') { const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/select?`; const queryParamsObj = {}; - const storageOption = options.storage.autocompleteItem; if (parameters) { const { @@ -148,12 +146,6 @@ export function tracker(options) { requests.queue(`${url}${queryString}`); requests.send(); - // Store term and section in browser storage - store[storageOption.scope].set(storageOption.key, JSON.stringify({ - item: term, - section: parameters && (parameters.section || parameters.original_section), - })); - return true; } @@ -176,7 +168,6 @@ export function tracker(options) { if (term && typeof term === 'string') { const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/search?`; const queryParamsObj = {}; - const storageOption = options.storage.searchTerm; if (parameters) { const { originalQuery, resultId, groupId, displayName } = parameters; @@ -202,9 +193,6 @@ export function tracker(options) { requests.queue(`${url}${queryString}`); requests.send(); - // Store term in browser storage - store[storageOption.scope].set(storageOption.key, term); - return true; } @@ -317,13 +305,6 @@ export function tracker(options) { * @returns {(true|Error)} */ sendConversion: (term, parameters) => { - // eslint-disable-next-line - const lastSearchTerm = store[options.storage.searchTerm.scope] - .get(options.storage.searchTerm.key); - // eslint-disable-next-line - const lastSelectedItemData = JSON.parse(store[options.storage.autocompleteItem.scope] - .get(options.storage.autocompleteItem.key)); - const searchTerm = utils.ourEncodeURIComponent(term) || 'TERM_UNKNOWN'; const url = `${options.serviceUrl}/autocomplete/${searchTerm}/conversion?`; const queryParamsObj = {}; From 40bede6f857ee97956d47ecf4cdb9bdb21ef4bbb Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 2 Oct 2019 17:39:23 -0600 Subject: [PATCH 29/56] Track isHuman in session storage + tests. --- spec/mocha.helpers.js | 12 ++++++++++++ spec/src/modules/tracker-humanity.js | 26 ++++++++++++++++++++------ src/modules/tracker-humanity.js | 17 ++++++++++++----- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js index ea61c508..91a20525 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -1,5 +1,8 @@ +import store from 'store2'; + const { JSDOM } = require('jsdom'); +// Setup mock DOM environment const setupDOM = () => { const { window } = new JSDOM(); @@ -7,11 +10,13 @@ const setupDOM = () => { global.document = window.document; }; +// Tear down mock DOM environment const teardownDOM = () => { delete global.window; delete global.document; }; +// Trigger browser resize event const triggerResize = () => { const resizeEvent = document.createEvent('Event'); @@ -26,8 +31,15 @@ const triggerResize = () => { window.resizeTo(1024, 768); }; +// Clear local and session storage +const clearStorage = () => { + store.local.clearAll(); + store.session.clearAll(); +}; + module.exports = { setupDOM, teardownDOM, triggerResize, + clearStorage, }; diff --git a/spec/src/modules/tracker-humanity.js b/spec/src/modules/tracker-humanity.js index b0809176..ab7673ca 100644 --- a/spec/src/modules/tracker-humanity.js +++ b/spec/src/modules/tracker-humanity.js @@ -1,37 +1,51 @@ import dotenv from 'dotenv'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import store from 'store2'; import trackerHumanity from '../../../src/modules/tracker-humanity'; - -const { setupDOM, teardownDOM, triggerResize } = require('../../mocha.helpers.js'); +import helpers from '../../mocha.helpers'; chai.use(chaiAsPromised); dotenv.config(); -describe('ConstructorIO - Tracker - Humanity', () => { +describe.only('ConstructorIO - Tracker - Humanity', () => { describe('isHuman', () => { + const storageKey = '_constructorio_is_human'; + beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; - setupDOM(); + helpers.setupDOM(); }); afterEach(() => { delete global.CLIENT_VERSION; - teardownDOM(); + helpers.teardownDOM(); + helpers.clearStorage(); }); it('Should not have isHuman flag set on initial instantiation', () => { const humanity = trackerHumanity(); expect(humanity.isHuman()).to.equal(false); + expect(store.session.get(storageKey)).to.equal(null); }); it('Should have isHuman flag set if human-like actions are detected', () => { const humanity = trackerHumanity(); - triggerResize(); + expect(humanity.isHuman()).to.equal(false); + helpers.triggerResize(); + expect(humanity.isHuman()).to.equal(true); + expect(store.session.get(storageKey)).to.equal(true); + }); + + it('Should have isHuman flag set if session variable is set', () => { + const humanity = trackerHumanity(); + expect(humanity.isHuman()).to.equal(false); + store.session.set(storageKey, true); expect(humanity.isHuman()).to.equal(true); + expect(store.session.get(storageKey)).to.equal(true); }); }); }); diff --git a/src/modules/tracker-humanity.js b/src/modules/tracker-humanity.js index 6d9f4e24..0f291e36 100644 --- a/src/modules/tracker-humanity.js +++ b/src/modules/tracker-humanity.js @@ -1,3 +1,5 @@ +import store from 'store2'; + const humanEvents = [ 'scroll', 'resize', @@ -11,24 +13,29 @@ const humanEvents = [ ]; export default function trackerHumanity() { - let isHumanBoolean = false; + const storageKey = '_constructorio_is_human'; + const isHumanStorage = !!store.session.get(storageKey); + let isHumanBoolean = isHumanStorage || false; // Humanity proved, remove handlers to prove humanity const remove = () => { isHumanBoolean = true; + store.session.set(storageKey, true); humanEvents.forEach((eventType) => { window.removeEventListener(eventType, remove, true); }); }; // Add handlers to prove humanity - humanEvents.forEach((eventType) => { - window.addEventListener(eventType, remove, true); - }); + if (!isHumanBoolean) { + humanEvents.forEach((eventType) => { + window.addEventListener(eventType, remove, true); + }); + } return { // Return boolean indicating if is human - isHuman: () => isHumanBoolean, + isHuman: () => isHumanBoolean || !!store.session.get(storageKey), }; } From 135cc860a5fe44245848fa431e9a27db3c615a28 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Wed, 2 Oct 2019 18:11:35 -0600 Subject: [PATCH 30/56] Add tests for tracker-requests. --- spec/mocha.helpers.js | 14 ++++++ spec/src/modules/tracker-humanity.js | 4 +- spec/src/modules/tracker-requests.js | 65 ++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js index 91a20525..8ae5d86d 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -31,6 +31,19 @@ const triggerResize = () => { window.resizeTo(1024, 768); }; +// Trigger browser unload event +const triggerUnload = () => { + const unloadEvent = document.createEvent('Event'); + + unloadEvent.initEvent('beforeunload', true, true); + + global.window.unload = () => { + global.window.dispatchEvent(unloadEvent); + }; + + window.unload(); +}; + // Clear local and session storage const clearStorage = () => { store.local.clearAll(); @@ -41,5 +54,6 @@ module.exports = { setupDOM, teardownDOM, triggerResize, + triggerUnload, clearStorage, }; diff --git a/spec/src/modules/tracker-humanity.js b/spec/src/modules/tracker-humanity.js index ab7673ca..b15c4191 100644 --- a/spec/src/modules/tracker-humanity.js +++ b/spec/src/modules/tracker-humanity.js @@ -8,17 +8,19 @@ import helpers from '../../mocha.helpers'; chai.use(chaiAsPromised); dotenv.config(); -describe.only('ConstructorIO - Tracker - Humanity', () => { +describe('ConstructorIO - Tracker - Humanity', () => { describe('isHuman', () => { const storageKey = '_constructorio_is_human'; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; + helpers.setupDOM(); }); afterEach(() => { delete global.CLIENT_VERSION; + helpers.teardownDOM(); helpers.clearStorage(); }); diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js index cdc0e1ce..1614b6bf 100644 --- a/spec/src/modules/tracker-requests.js +++ b/spec/src/modules/tracker-requests.js @@ -1,9 +1,74 @@ +/* eslint-disable no-restricted-properties, no-underscore-dangle */ import dotenv from 'dotenv'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import store from 'store2'; +import trackerRequests from '../../../src/modules/tracker-requests'; +import helpers from '../../mocha.helpers'; chai.use(chaiAsPromised); dotenv.config(); describe('ConstructorIO - Tracker - Requests', () => { + describe('queue', () => { + const storageKey = '_constructorio_requests'; + let defaultAgent; + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + helpers.setupDOM(); + + defaultAgent = window.navigator.userAgent; + }); + + afterEach(() => { + window.navigator.__defineGetter__('userAgent', () => defaultAgent); + window.navigator.__defineGetter__('webdriver', () => undefined); + + delete global.CLIENT_VERSION; + + helpers.teardownDOM(); + helpers.clearStorage(); + }); + + it('Should add requests to the queue and persist on unload event', () => { + const requests = trackerRequests(); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start'); + requests.queue('https://ac.cnstrc.com/behavior?action=focus'); + requests.queue('https://ac.cnstrc.com/behavior?action=magic_number_three'); + + expect(requests.get()).to.be.an('array').length(3); + helpers.triggerUnload(); + expect(store.local.get(storageKey)).to.be.an('array').length(3); + }); + + it('Should not add requests to the queue if the user has a bot-like useragent', () => { + const requests = trackerRequests(); + + window.navigator.__defineGetter__('userAgent', () => 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Safari/537.36'); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start'); + requests.queue('https://ac.cnstrc.com/behavior?action=focus'); + requests.queue('https://ac.cnstrc.com/behavior?action=magic_number_three'); + + expect(requests.get()).to.be.an('array').length(0); + helpers.triggerUnload(); + expect(store.local.get(storageKey)).to.be.an('array').length(0); + }); + + it('Should not add requests to the queue if the user is webdriver', () => { + const requests = trackerRequests(); + + window.navigator.__defineGetter__('webdriver', () => true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start'); + requests.queue('https://ac.cnstrc.com/behavior?action=focus'); + requests.queue('https://ac.cnstrc.com/behavior?action=magic_number_three'); + + expect(requests.get()).to.be.an('array').length(0); + helpers.triggerUnload(); + expect(store.local.get(storageKey)).to.be.an('array').length(0); + }); + }); }); From c66e0c648e04aba4ef5f8a4f8dc8d4eab455fa93 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Thu, 3 Oct 2019 10:13:07 -0600 Subject: [PATCH 31/56] Corrections to sendAutocompleteSelect + tests. --- spec/src/modules/tracker.js | 28 ++++++++++++++--- src/modules/tracker.js | 60 +++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index d596874d..8243faae 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -46,6 +46,14 @@ describe('ConstructorIO - Tracker', () => { describe('sendAutocompleteSelect', () => { const term = 'Where The Wild Things Are'; + const parameters = { + original_query: 'query', + result_id: 'result-id', + section: 'Search Suggestions', + tr: 'click', + group_id: 'group-id', + display_name: 'display-name', + }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -55,22 +63,34 @@ describe('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term is provided', () => { + it('Should respond with a valid response when term and parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSelect(term)).to.equal(true); + expect(tracker.sendAutocompleteSelect(term, parameters)).to.equal(true); }); it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSelect([])).to.be.an('error'); + expect(tracker.sendAutocompleteSelect([], parameters)).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSelect()).to.be.an('error'); + expect(tracker.sendAutocompleteSelect(null, parameters)).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendAutocompleteSelect(term, [])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendAutocompleteSelect(term)).to.be.an('error'); }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 623c6fac..3ef30bbb 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -21,7 +21,7 @@ export function tracker(options) { // Append common parameters to supplied parameters object const createQueryString = (queryParamsObj) => { const { apiKey, version, sessionId, clientId, userId, segments } = options; - const paramsObj = Object.assign(queryParamsObj); + let paramsObj = Object.assign(queryParamsObj); if (version) { paramsObj.c = version; @@ -48,6 +48,7 @@ export function tracker(options) { } paramsObj._dt = Date.now(); + paramsObj = utils.cleanParams(paramsObj); return qs.stringify(paramsObj, { indices: false }); }; @@ -93,62 +94,69 @@ export function tracker(options) { * @function sendAutocompleteSelect * @param {string} term - Term of selected autocomplete item * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.originalQuery - The current autocomplete search query - * @param {string} parameters.resultId - Customer id of the selected autocomplete item + * @param {string} parameters.original_query - The current autocomplete search query + * @param {string} parameters.result_id - Customer id of the selected autocomplete item * @param {string} parameters.section - Section the selected item resides within * @param {string} [parameters.tr] - Trigger used to select the item (click, etc.) - * @param {string} [parameters.groupId] - Group identifier of selected item - * @param {string} [parameters.displayName] - Display name of group of selected item + * @param {string} [parameters.group_id] - Group identifier of selected item + * @param {string} [parameters.display_name] - Display name of group of selected item * @returns {(true|Error)} */ sendAutocompleteSelect: (term, parameters) => { + // Ensure term is provided (required) if (term && typeof term === 'string') { - const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/select?`; - const queryParamsObj = {}; - - if (parameters) { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/select?`; + const queryParamsObj = {}; const { - originalQuery, - resultId, + original_query, + result_id, section, original_section, tr, - groupId, - displayName, + group_id, + display_name, } = parameters; - if (originalQuery) { - queryParamsObj.original_query = originalQuery; + if (original_query) { + queryParamsObj.original_query = original_query; } if (tr) { queryParamsObj.tr = tr; } - if (section || original_section) { - queryParamsObj.autocomplete_section = section || original_section; + if (original_section || section) { + queryParamsObj.section = original_section || section; } - if (groupId) { + if (group_id) { queryParamsObj.group = { - group_id: groupId, - display_name: displayName || '', + group_id, + display_name, }; } - if (resultId) { - queryParamsObj.result_id = resultId; + if (result_id) { + queryParamsObj.result_id = result_id; } - } - const queryString = createQueryString(queryParamsObj); + const queryString = createQueryString(queryParamsObj); + + requests.queue(`${url}${queryString}`); + requests.send(); + + return true; + } - requests.queue(`${url}${queryString}`); requests.send(); - return true; + return new Error('parameters are required of type object'); } + requests.send(); + return new Error('term is a required parameter of type string'); }, From 61ae38e88181e17568b8b2f1fc1a47b6eb59e344 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Thu, 3 Oct 2019 10:22:17 -0600 Subject: [PATCH 32/56] Corrections to sendAutocompleteSearch + tests. --- spec/src/modules/tracker.js | 30 +++++++++++++---- src/modules/tracker.js | 64 ++++++++++++++++++++----------------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 8243faae..4d37d448 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -9,7 +9,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe('ConstructorIO - Tracker', () => { +describe.only('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { @@ -47,7 +47,7 @@ describe('ConstructorIO - Tracker', () => { describe('sendAutocompleteSelect', () => { const term = 'Where The Wild Things Are'; const parameters = { - original_query: 'query', + original_query: 'original-query', result_id: 'result-id', section: 'Search Suggestions', tr: 'click', @@ -96,6 +96,12 @@ describe('ConstructorIO - Tracker', () => { describe('sendAutocompleteSearch', () => { const term = 'Where The Wild Things Are'; + const parameters = { + original_query: 'original-query', + result_id: 'result-id', + group_id: 'group-id', + display_name: 'display-name', + }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -105,22 +111,34 @@ describe('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term is provided', () => { + it('Should respond with a valid response when term and parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSearch(term)).to.equal(true); + expect(tracker.sendAutocompleteSearch(term, parameters)).to.equal(true); }); it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSearch([])).to.be.an('error'); + expect(tracker.sendAutocompleteSearch([], parameters)).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSearch()).to.be.an('error'); + expect(tracker.sendAutocompleteSearch(null, parameters)).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendAutocompleteSearch(term, [])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendAutocompleteSearch(term)).to.be.an('error'); }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 3ef30bbb..4ab90b92 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -63,9 +63,8 @@ export function tracker(options) { sendSessionStart: () => { const url = `${options.serviceUrl}/behavior?`; const queryParamsObj = { action: 'session_start' }; - const queryString = createQueryString(queryParamsObj); - requests.queue(`${url}${queryString}`); + requests.queue(`${url}${createQueryString(queryParamsObj)}`); requests.send(); return true; @@ -80,9 +79,8 @@ export function tracker(options) { sendInputFocus: () => { const url = `${options.serviceUrl}/behavior?`; const queryParamsObj = { action: 'focus' }; - const queryString = createQueryString(queryParamsObj); - requests.queue(`${url}${queryString}`); + requests.queue(`${url}${createQueryString(queryParamsObj)}`); requests.send(); return true; @@ -142,9 +140,7 @@ export function tracker(options) { queryParamsObj.result_id = result_id; } - const queryString = createQueryString(queryParamsObj); - - requests.queue(`${url}${queryString}`); + requests.queue(`${url}${createQueryString(queryParamsObj)}`); requests.send(); return true; @@ -166,44 +162,52 @@ export function tracker(options) { * @function sendAutocompleteSearch * @param {string} term - Term of submitted autocomplete event * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.originalQuery - The current autocomplete search query - * @param {string} parameters.resultId - Customer ID of the selected autocomplete item - * @param {string} [parameters.groupId] - Group identifier of selected item - * @param {string} [parameters.displayName] - Display name of group of selected item + * @param {string} parameters.original_query - The current autocomplete search query + * @param {string} parameters.result_id - Customer ID of the selected autocomplete item + * @param {string} [parameters.group_id] - Group identifier of selected item + * @param {string} [parameters.display_name] - Display name of group of selected item * @returns {(true|Error)} */ sendAutocompleteSearch: (term, parameters) => { + // Ensure term is provided (required) if (term && typeof term === 'string') { - const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/search?`; - const queryParamsObj = {}; + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/search?`; + const queryParamsObj = {}; - if (parameters) { - const { originalQuery, resultId, groupId, displayName } = parameters; + if (parameters) { + const { original_query, result_id, group_id, display_name } = parameters; - if (originalQuery) { - queryParamsObj.original_query = originalQuery; - } + if (original_query) { + queryParamsObj.original_query = original_query; + } - if (groupId) { - queryParamsObj.group = { - group_id: groupId, - display_name: displayName, - }; - } + if (group_id) { + queryParamsObj.group = { + group_id, + display_name, + }; + } - if (resultId) { - queryParamsObj.result_id = resultId; + if (result_id) { + queryParamsObj.result_id = result_id; + } } - } - const queryString = createQueryString(queryParamsObj); + requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.send(); + + return true; + } - requests.queue(`${url}${queryString}`); requests.send(); - return true; + return new Error('parameters are required of type object'); } + requests.send(); + return new Error('term is a required parameter of type string'); }, From 6ff46925c39b01658ac14a9d3c0c0b858271d1ef Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Thu, 3 Oct 2019 10:26:15 -0600 Subject: [PATCH 33/56] Corrections for sendSearchResults + tests. --- spec/src/modules/tracker.js | 32 ++++++++++++++++++++++++++-- src/modules/tracker.js | 42 ++++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 4d37d448..0cd9f48b 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -144,6 +144,10 @@ describe.only('ConstructorIO - Tracker', () => { describe('sendSearchResults', () => { const term = 'Cat in the Hat'; + const parameters = { + num_results: 1337, + customer_ids: [1, 2, 3], + }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -153,10 +157,34 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term is provided', () => { + it('Should respond with a valid response when term and parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResults(term, parameters)).to.equal(true); + }); + + it('Should throw an error when invalid term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResults([], parameters)).to.be.an('error'); + }); + + it('Should throw an error when no term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResults(null, parameters)).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResults(term, [])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResults(term)).to.equal(true); + expect(tracker.sendSearchResults(term)).to.be.an('error'); }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 4ab90b92..45457b9c 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -217,32 +217,44 @@ export function tracker(options) { * @function sendSearchResults * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request - * @param {number} parameters.numResults - Number of search results in total - * @param {array} [parameters.customerIds] - List of customer item id's returned from search + * @param {number} parameters.num_results - Number of search results in total + * @param {array} [parameters.customer_ids] - List of customer item id's returned from search * @returns {(true|Error)} */ sendSearchResults: (term, parameters) => { - const url = `${options.serviceUrl}/behavior?`; - const queryParamsObj = { action: 'search-results', term }; + // Ensure term is provided (required) + if (term && typeof term === 'string') { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const url = `${options.serviceUrl}/behavior?`; + const queryParamsObj = { action: 'search-results', term }; - if (parameters) { - const { numResults, customerIds } = parameters; + if (parameters) { + const { num_results, customer_ids } = parameters; - if (numResults) { - queryParamsObj.num_results = numResults; - } + if (num_results) { + queryParamsObj.num_results = num_results; + } + + if (customer_ids && Array.isArray(customer_ids)) { + queryParamsObj.customer_ids = customer_ids.join(','); + } + } - if (customerIds && Array.isArray(customerIds)) { - queryParamsObj.customer_ids = customerIds.join(','); + requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.send(); + + return true; } - } - const queryString = createQueryString(queryParamsObj); + requests.send(); + + return new Error('parameters are required of type object'); + } - requests.queue(`${url}${queryString}`); requests.send(); - return true; + return new Error('term is a required parameter of type string'); }, /** From b606c2617a0f9fbb444061ab241bc03ae2d77dac Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Thu, 3 Oct 2019 10:31:11 -0600 Subject: [PATCH 34/56] Corrections to sendSearchResultClick + tests. --- spec/src/modules/tracker.js | 33 ++++++++++++- src/modules/tracker.js | 97 ++++++++++++++++--------------------- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 0cd9f48b..7f7d14d2 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -190,6 +190,11 @@ describe.only('ConstructorIO - Tracker', () => { describe('sendSearchResultClick', () => { const term = 'Where The Wild Things Are'; + const parameters = { + name: 'name', + customer_id: 'customer-id', + result_id: 'result-id', + }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -199,10 +204,34 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term is provided', () => { + it('Should respond with a valid response when term and parmeters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResultClick(term, parameters)).to.equal(true); + }); + + it('Should throw an error when invalid term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResultClick([], parameters)).to.be.an('error'); + }); + + it('Should throw an error when no term is provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResultClick(null, parameters)).to.be.an('error'); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResultClick(term, [])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResultClick(term)).to.equal(true); + expect(tracker.sendSearchResultClick(term)).to.be.an('error'); }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 45457b9c..da5a3ed8 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -175,24 +175,21 @@ export function tracker(options) { if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/search?`; const queryParamsObj = {}; + const { original_query, result_id, group_id, display_name } = parameters; - if (parameters) { - const { original_query, result_id, group_id, display_name } = parameters; - - if (original_query) { - queryParamsObj.original_query = original_query; - } + if (original_query) { + queryParamsObj.original_query = original_query; + } - if (group_id) { - queryParamsObj.group = { - group_id, - display_name, - }; - } + if (group_id) { + queryParamsObj.group = { + group_id, + display_name, + }; + } - if (result_id) { - queryParamsObj.result_id = result_id; - } + if (result_id) { + queryParamsObj.result_id = result_id; } requests.queue(`${url}${createQueryString(queryParamsObj)}`); @@ -228,17 +225,14 @@ export function tracker(options) { if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/behavior?`; const queryParamsObj = { action: 'search-results', term }; + const { num_results, customer_ids } = parameters; - if (parameters) { - const { num_results, customer_ids } = parameters; - - if (num_results) { - queryParamsObj.num_results = num_results; - } + if (num_results) { + queryParamsObj.num_results = num_results; + } - if (customer_ids && Array.isArray(customer_ids)) { - queryParamsObj.customer_ids = customer_ids.join(','); - } + if (customer_ids && Array.isArray(customer_ids)) { + queryParamsObj.customer_ids = customer_ids.join(','); } requests.queue(`${url}${createQueryString(queryParamsObj)}`); @@ -263,53 +257,46 @@ export function tracker(options) { * @function sendSearchResultClick * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.itemId - Identifier (send either itemId, item, name or itemName) - * @param {string} parameters.item - Identifier (send either itemId, item, name or itemName) * @param {string} parameters.name - Identifier (send either itemId, item, name or itemName) - * @param {string} parameters.itemName - Identifier (send either itemId, item, name or itemName) - * @param {string} parameters.customerId - Customer id - * @param {string} parameters.resultId - Result id + * @param {string} parameters.customer_id - Customer id + * @param {string} parameters.result_id - Result id * @returns {(true|Error)} */ sendSearchResultClick: (term, parameters) => { - const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/click_through?`; - const queryParamsObj = {}; - - if (parameters && Object.keys(parameters).length > 0) { - const { itemId, item, name, itemName, customerId, resultId } = parameters; - - if (itemId) { - queryParamsObj.item_id = itemId; - } + // Ensure term is provided (required) + if (term && typeof term === 'string') { + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/click_through?`; + const queryParamsObj = {}; + const { name, customer_id, result_id } = parameters; - if (item) { - queryParamsObj.item = item; - } + if (name) { + queryParamsObj.name = name; + } - if (name) { - queryParamsObj.name = name; - } + if (customer_id) { + queryParamsObj.customer_id = customer_id; + } - if (itemName) { - queryParamsObj.item_name = itemName; - } + if (result_id) { + queryParamsObj.result_id = result_id; + } - if (customerId) { - queryParamsObj.customer_id = customerId; - } + requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.send(); - if (resultId) { - queryParamsObj.result_id = resultId; + return true; } - const queryString = createQueryString(queryParamsObj); + requests.send(); - requests.queue(`${url}${queryString}`); + return new Error('parameters are required of type object'); } requests.send(); - return true; + return new Error('term is a required parameter of type string'); }, /** From 1773ff3ccae0eccc7953c5f3fb7c7bd5e4125ef6 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Thu, 3 Oct 2019 10:42:17 -0600 Subject: [PATCH 35/56] Corrections to sendConversion + tests. --- spec/src/modules/tracker.js | 29 ++++++++++++++++++-- src/modules/tracker.js | 54 +++++++++++++++---------------------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 7f7d14d2..fb630ccf 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -237,6 +237,13 @@ describe.only('ConstructorIO - Tracker', () => { describe('sendConversion', () => { const term = 'Where The Wild Things Are'; + const parameters = { + name: 'name', + customer_id: 'customer-id', + result_id: 'result-id', + revenue: 123, + section: 'Products', + }; beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -246,10 +253,28 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response when term is provided', () => { + it('Should respond with a valid response when term and parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendConversion(term, parameters)).to.equal(true); + }); + + it('Should respond with a valid response no term is provided, but parameters are', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendConversion(null, parameters)).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendConversion(term)).to.equal(true); + expect(tracker.sendSearchResultClick(term, [])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendSearchResultClick(term)).to.be.an('error'); }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index da5a3ed8..02a93d5a 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -257,7 +257,7 @@ export function tracker(options) { * @function sendSearchResultClick * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.name - Identifier (send either itemId, item, name or itemName) + * @param {string} parameters.name - Identifier * @param {string} parameters.customer_id - Customer id * @param {string} parameters.result_id - Result id * @returns {(true|Error)} @@ -305,46 +305,31 @@ export function tracker(options) { * @function sendConversion * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request - * @param {string} parameters.itemId - Identifier (send either itemId, item, name or itemName) - * @param {string} parameters.item - Identifier (send either itemId, item, name or itemName) - * @param {string} parameters.name - Identifier (send either itemId, item, name or itemName) - * @param {string} parameters.itemName - Identifier (send either itemId, item, name or itemName) - * @param {string} parameters.customerId - Customer id - * @param {string} parameters.resultId - Result id + * @param {string} parameters.name - Identifier + * @param {string} parameters.customer_id - Customer id + * @param {string} parameters.result_id - Result id * @param {string} parameters.revenue - Revenue * @param {string} parameters.section - Autocomplete section * @returns {(true|Error)} */ sendConversion: (term, parameters) => { - const searchTerm = utils.ourEncodeURIComponent(term) || 'TERM_UNKNOWN'; - const url = `${options.serviceUrl}/autocomplete/${searchTerm}/conversion?`; - const queryParamsObj = {}; - - if (parameters && Object.keys(parameters).length > 0) { - const { itemId, item, name, itemName, customerId, resultId, revenue, section } = parameters; - - if (itemId) { - queryParamsObj.item_id = itemId; - } - - if (item) { - queryParamsObj.item = item; - } + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const searchTerm = utils.ourEncodeURIComponent(term) || 'TERM_UNKNOWN'; + const url = `${options.serviceUrl}/autocomplete/${searchTerm}/conversion?`; + const queryParamsObj = {}; + const { name, customer_id, result_id, revenue, section } = parameters; if (name) { queryParamsObj.name = name; } - if (itemName) { - queryParamsObj.item_name = itemName; - } - - if (customerId) { - queryParamsObj.customer_id = customerId; + if (customer_id) { + queryParamsObj.customer_id = customer_id; } - if (resultId) { - queryParamsObj.result_id = resultId; + if (result_id) { + queryParamsObj.result_id = result_id; } if (revenue) { @@ -352,17 +337,20 @@ export function tracker(options) { } if (section) { - queryParamsObj.autocomplete_section = section; + queryParamsObj.section = section; + } else { + queryParamsObj.section = 'Products'; } - const queryString = createQueryString(queryParamsObj); + requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.send(); - requests.queue(`${url}${queryString}`); + return true; } requests.send(); - return true; + return new Error('parameters are required of type object'); }, /** From fed68a7b39be0c125be00f28defc540a5beb795b Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Thu, 3 Oct 2019 11:08:01 -0600 Subject: [PATCH 36/56] Corrections to sendPurchase + tests. --- spec/src/modules/tracker.js | 26 ++++++++++++++++++++++---- src/modules/tracker.js | 30 +++++++++++++++++++----------- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index fb630ccf..9fd80ba1 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -9,7 +9,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; -describe.only('ConstructorIO - Tracker', () => { +describe('ConstructorIO - Tracker', () => { jsdom({ url: 'http://localhost' }); describe('sendSessionStart', () => { @@ -259,7 +259,7 @@ describe.only('ConstructorIO - Tracker', () => { expect(tracker.sendConversion(term, parameters)).to.equal(true); }); - it('Should respond with a valid response no term is provided, but parameters are', () => { + it('Should respond with a valid response when no term is provided, but parameters are', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); expect(tracker.sendConversion(null, parameters)).to.equal(true); @@ -279,6 +279,12 @@ describe.only('ConstructorIO - Tracker', () => { }); describe('sendPurchase', () => { + const parameters = { + customer_ids: 'customer-id', + revenue: 123, + section: 'Products', + }; + beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; }); @@ -287,10 +293,22 @@ describe.only('ConstructorIO - Tracker', () => { delete global.CLIENT_VERSION; }); - it('Should respond with a valid response', () => { + it('Should respond with a valid response when parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendPurchase(parameters)).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.sendPurchase([])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendPurchase()).to.equal(true); + expect(tracker.sendPurchase()).to.be.an('error'); }); }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 02a93d5a..f0e273ec 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -358,34 +358,42 @@ export function tracker(options) { * * @function sendPurchase * @param {object} parameters - Additional parameters to be sent with request - * @param {array} parameters.customerIds - List of customer item id's + * @param {array} parameters.customer_ids - List of customer item id's * @param {string} parameters.revenue - Revenue * @param {string} parameters.section - Autocomplete section * @returns {(true|Error)} */ sendPurchase: (parameters) => { - const url = `${options.serviceUrl}/autocomplete/TERM_UNKNOWN/purchase?`; - const queryParamsObj = {}; + // Ensure parameters are provided (required) + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const url = `${options.serviceUrl}/autocomplete/TERM_UNKNOWN/purchase?`; + const queryParamsObj = {}; + + const { customer_ids, revenue, section } = parameters; - if (parameters && Object.keys(parameters).length > 0) { - const { customerIds, section } = parameters; + if (customer_ids) { + queryParamsObj.customer_ids = customer_ids; + } - if (customerIds) { - queryParamsObj.customer_ids = customerIds; + if (revenue) { + queryParamsObj.revenue = revenue; } if (section) { - queryParamsObj.autocomplete_section = section; + queryParamsObj.section = section; + } else { + queryParamsObj.section = 'Products'; } - const queryString = createQueryString(queryParamsObj); + requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.send(); - requests.queue(`${url}${queryString}`); + return true; } requests.send(); - return true; + return new Error('parameters are required of type object'); }, }; } From 81b798808c9c203239989b40a8ada72a9f6ba907 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Fri, 4 Oct 2019 16:36:58 -0600 Subject: [PATCH 37/56] Use require in favor of import. --- spec/mocha.helpers.js | 3 +-- spec/src/modules/tracker-humanity.js | 12 ++++++------ spec/src/modules/tracker-requests.js | 12 ++++++------ spec/src/modules/tracker.js | 10 +++++----- src/constructorio.js | 8 ++++---- src/modules/autocomplete.js | 4 +--- src/modules/recommendations.js | 4 +--- src/modules/search.js | 6 ++---- src/modules/tracker-humanity.js | 8 +++++--- src/modules/tracker-requests.js | 16 +++++++++------- src/modules/tracker.js | 19 ++++++++----------- src/utils.js | 6 +++--- 12 files changed, 51 insertions(+), 57 deletions(-) diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js index 8ae5d86d..b26497cf 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -1,5 +1,4 @@ -import store from 'store2'; - +const store = require('store2'); const { JSDOM } = require('jsdom'); // Setup mock DOM environment diff --git a/spec/src/modules/tracker-humanity.js b/spec/src/modules/tracker-humanity.js index b15c4191..17f32566 100644 --- a/spec/src/modules/tracker-humanity.js +++ b/spec/src/modules/tracker-humanity.js @@ -1,9 +1,9 @@ -import dotenv from 'dotenv'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import store from 'store2'; -import trackerHumanity from '../../../src/modules/tracker-humanity'; -import helpers from '../../mocha.helpers'; +const dotenv = require('dotenv'); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const store = require('store2'); +const trackerHumanity = require('../../../src/modules/tracker-humanity'); +const helpers = require('../../mocha.helpers'); chai.use(chaiAsPromised); dotenv.config(); diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js index 1614b6bf..c8ec6a04 100644 --- a/spec/src/modules/tracker-requests.js +++ b/spec/src/modules/tracker-requests.js @@ -1,10 +1,10 @@ /* eslint-disable no-restricted-properties, no-underscore-dangle */ -import dotenv from 'dotenv'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import store from 'store2'; -import trackerRequests from '../../../src/modules/tracker-requests'; -import helpers from '../../mocha.helpers'; +const dotenv = require('dotenv'); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const store = require('store2'); +const trackerRequests = require('../../../src/modules/tracker-requests'); +const helpers = require('../../mocha.helpers'); chai.use(chaiAsPromised); dotenv.config(); diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 9fd80ba1..48a9dc22 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -1,8 +1,8 @@ -import jsdom from 'mocha-jsdom'; -import dotenv from 'dotenv'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import ConstructorIO from '../../../src/constructorio'; +const jsdom = require('mocha-jsdom'); +const dotenv = require('dotenv'); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const ConstructorIO = require('../../../src/constructorio'); chai.use(chaiAsPromised); dotenv.config(); diff --git a/src/constructorio.js b/src/constructorio.js index e0cb3984..52246461 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -2,10 +2,10 @@ const ConstructorioID = require('@constructor-io/constructorio-id'); // Modules -const { search } = require('./modules/search'); -const { autocomplete } = require('./modules/autocomplete'); -const { recommendations } = require('./modules/recommendations'); -const { tracker } = require('./modules/tracker'); +const search = require('./modules/search'); +const autocomplete = require('./modules/autocomplete'); +const recommendations = require('./modules/recommendations'); +const tracker = require('./modules/tracker'); const { version } = require('../package.json'); /** diff --git a/src/modules/autocomplete.js b/src/modules/autocomplete.js index 979db973..08e1724e 100644 --- a/src/modules/autocomplete.js +++ b/src/modules/autocomplete.js @@ -135,6 +135,4 @@ const autocomplete = (options) => { }; }; -module.exports = { - autocomplete, -}; +module.exports = autocomplete; diff --git a/src/modules/recommendations.js b/src/modules/recommendations.js index e80d03e9..52a02771 100644 --- a/src/modules/recommendations.js +++ b/src/modules/recommendations.js @@ -182,6 +182,4 @@ const recommendations = (options) => { }; }; -module.exports = { - recommendations, -}; +module.exports = recommendations; diff --git a/src/modules/search.js b/src/modules/search.js index b268026c..8854d297 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -1,4 +1,4 @@ -/* eslint-disable import/prefer-default-export, object-curly-newline */ +/* eslint-disable object-curly-newline */ const qs = require('qs'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); @@ -249,6 +249,4 @@ const search = (options) => { }; }; -module.exports = { - search, -}; +module.exports = search; diff --git a/src/modules/tracker-humanity.js b/src/modules/tracker-humanity.js index 0f291e36..6b965527 100644 --- a/src/modules/tracker-humanity.js +++ b/src/modules/tracker-humanity.js @@ -1,4 +1,4 @@ -import store from 'store2'; +const store = require('store2'); const humanEvents = [ 'scroll', @@ -12,7 +12,7 @@ const humanEvents = [ 'focus', ]; -export default function trackerHumanity() { +const trackerHumanity = () => { const storageKey = '_constructorio_is_human'; const isHumanStorage = !!store.session.get(storageKey); let isHumanBoolean = isHumanStorage || false; @@ -38,4 +38,6 @@ export default function trackerHumanity() { // Return boolean indicating if is human isHuman: () => isHumanBoolean || !!store.session.get(storageKey), }; -} +}; + +module.exports = trackerHumanity; diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js index 8187ccd0..753a2967 100644 --- a/src/modules/tracker-requests.js +++ b/src/modules/tracker-requests.js @@ -1,12 +1,12 @@ -import store from 'store2'; -import fetchPonyfill from 'fetch-ponyfill'; -import Promise from 'es6-promise'; -import utils from '../utils'; -import trackerHumanity from './tracker-humanity'; +const store = require('store2'); +const fetchPonyfill = require('fetch-ponyfill'); +const Promise = require('es6-promise'); +const utils = require('../utils'); +const trackerHumanity = require('./tracker-humanity'); const { fetch } = fetchPonyfill({ Promise }); -export default function trackerRequests() { +const trackerRequests = () => { const humanity = trackerHumanity(); const storageKey = '_constructorio_requests'; let requestPending = false; @@ -50,4 +50,6 @@ export default function trackerRequests() { // Return current queue get: () => requestQueue, }; -} +}; + +module.exports = trackerRequests; diff --git a/src/modules/tracker.js b/src/modules/tracker.js index f0e273ec..a3e0904b 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1,12 +1,7 @@ -/* eslint-disable - import/prefer-default-export, - object-curly-newline, - no-underscore-dangle, - camelcase -*/ -import qs from 'qs'; -import utils from '../utils'; -import trackerRequests from './tracker-requests'; +/* eslint-disable object-curly-newline, no-underscore-dangle, camelcase */ +const qs = require('qs'); +const utils = require('../utils'); +const trackerRequests = require('./tracker-requests'); /** * Interface to tracking related API calls. @@ -15,7 +10,7 @@ import trackerRequests from './tracker-requests'; * @inner * @returns {object} */ -export function tracker(options) { +const tracker = (options) => { const requests = trackerRequests(options); // Append common parameters to supplied parameters object @@ -396,4 +391,6 @@ export function tracker(options) { return new Error('parameters are required of type object'); }, }; -} +}; + +module.exports = tracker; diff --git a/src/utils.js b/src/utils.js index 19d491dc..9a522b4b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ -import qs from 'qs'; -import botList from './botlist'; +const qs = require('qs'); +const botList = require('./botlist'); const utils = { ourEncodeURIComponent: (str) => { @@ -56,4 +56,4 @@ const utils = { }), }; -export default utils; +module.exports = utils; From 32e3640ba7cfe33046ba5fa43ad493df7a698b77 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Mon, 7 Oct 2019 17:35:10 -0600 Subject: [PATCH 38/56] Add tests for tracker-requests send method. --- spec/src/modules/tracker-requests.js | 126 ++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js index c8ec6a04..4e426292 100644 --- a/spec/src/modules/tracker-requests.js +++ b/spec/src/modules/tracker-requests.js @@ -10,8 +10,9 @@ chai.use(chaiAsPromised); dotenv.config(); describe('ConstructorIO - Tracker - Requests', () => { + const storageKey = '_constructorio_requests'; + describe('queue', () => { - const storageKey = '_constructorio_requests'; let defaultAgent; beforeEach(() => { @@ -71,4 +72,127 @@ describe('ConstructorIO - Tracker - Requests', () => { expect(store.local.get(storageKey)).to.be.an('array').length(0); }); }); + + describe('send', () => { + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + + helpers.setupDOM(); + }); + + afterEach(() => { + delete global.CLIENT_VERSION; + + helpers.teardownDOM(); + helpers.clearStorage(); + }); + + it('Should send all tracking requests if queue is populated and user is human', (done) => { + const requests = trackerRequests(); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start'); + requests.queue('https://ac.cnstrc.com/behavior?action=focus'); + requests.queue('https://ac.cnstrc.com/behavior?action=magic_number_three'); + + expect(requests.get()).to.be.an('array').length(3); + helpers.triggerResize(); + requests.send(); + + setTimeout(() => { + expect(requests.get()).to.be.an('array').length(0); + done(); + }, 1000); + }); + + it('Should not send tracking requests if queue is populated and user is not human', (done) => { + const requests = trackerRequests(); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start'); + requests.queue('https://ac.cnstrc.com/behavior?action=focus'); + requests.queue('https://ac.cnstrc.com/behavior?action=magic_number_three'); + + expect(requests.get()).to.be.an('array').length(3); + requests.send(); + + setTimeout(() => { + expect(requests.get()).to.be.an('array').length(3); + done(); + }, 1000); + }); + + it('Should not send tracking requests if queue is populated and user is human and page is unloading', (done) => { + const requests = trackerRequests(); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start'); + requests.queue('https://ac.cnstrc.com/behavior?action=focus'); + requests.queue('https://ac.cnstrc.com/behavior?action=magic_number_three'); + + expect(requests.get()).to.be.an('array').length(3); + helpers.triggerResize(); + helpers.triggerUnload(); + requests.send(); + + setTimeout(() => { + expect(requests.get()).to.be.an('array').length(3); + done(); + }, 1000); + }); + + it('Should send all tracking requests if requests exist in storage and user is human', (done) => { + store.local.set(storageKey, [ + 'https://ac.cnstrc.com/behavior?action=session_start', + 'https://ac.cnstrc.com/behavior?action=focus', + 'https://ac.cnstrc.com/behavior?action=magic_number_three', + ]); + + const requests = trackerRequests(); + + expect(requests.get()).to.be.an('array').length(3); + helpers.triggerResize(); + requests.send(); + + setTimeout(() => { + expect(requests.get()).to.be.an('array').length(0); + done(); + }, 1000); + }); + + it('Should not send tracking requests if requests exist in storage and user is not human', (done) => { + store.local.set(storageKey, [ + 'https://ac.cnstrc.com/behavior?action=session_start', + 'https://ac.cnstrc.com/behavior?action=focus', + 'https://ac.cnstrc.com/behavior?action=magic_number_three', + ]); + + const requests = trackerRequests(); + + expect(requests.get()).to.be.an('array').length(3); + requests.send(); + + setTimeout(() => { + expect(requests.get()).to.be.an('array').length(3); + done(); + }, 1000); + }); + + it('Should not send tracking requests if requests exist in storage and user is human and page is unloading', (done) => { + store.local.set(storageKey, [ + 'https://ac.cnstrc.com/behavior?action=session_start', + 'https://ac.cnstrc.com/behavior?action=focus', + 'https://ac.cnstrc.com/behavior?action=magic_number_three', + ]); + + const requests = trackerRequests(); + + expect(requests.get()).to.be.an('array').length(3); + helpers.triggerResize(); + helpers.triggerUnload(); + requests.send(); + + setTimeout(() => { + expect(requests.get()).to.be.an('array').length(3); + done(); + }, 1000); + }); + }); }); From 8ae20c1b6eb49714d31d2c683ade23fb8f574b1b Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Wed, 9 Oct 2019 18:35:20 -0400 Subject: [PATCH 39/56] Overflow on store2 --- spec/mocha.helpers.js | 2 +- spec/src/modules/tracker-humanity.js | 2 +- spec/src/modules/tracker-requests.js | 2 +- src/modules/tracker-humanity.js | 2 +- src/modules/tracker-requests.js | 2 +- src/store.js | 8 +++ src/store.overflow.js | 90 ++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/store.js create mode 100644 src/store.overflow.js diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js index b26497cf..6a0d29f4 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -1,4 +1,4 @@ -const store = require('store2'); +const store = require('../src/store'); const { JSDOM } = require('jsdom'); // Setup mock DOM environment diff --git a/spec/src/modules/tracker-humanity.js b/spec/src/modules/tracker-humanity.js index 17f32566..0c69963a 100644 --- a/spec/src/modules/tracker-humanity.js +++ b/spec/src/modules/tracker-humanity.js @@ -1,7 +1,7 @@ const dotenv = require('dotenv'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const store = require('store2'); +const store = require('../../../src/store'); const trackerHumanity = require('../../../src/modules/tracker-humanity'); const helpers = require('../../mocha.helpers'); diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js index 4e426292..738321a0 100644 --- a/spec/src/modules/tracker-requests.js +++ b/spec/src/modules/tracker-requests.js @@ -2,7 +2,7 @@ const dotenv = require('dotenv'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const store = require('store2'); +const store = require('../../../src/store'); const trackerRequests = require('../../../src/modules/tracker-requests'); const helpers = require('../../mocha.helpers'); diff --git a/src/modules/tracker-humanity.js b/src/modules/tracker-humanity.js index 6b965527..44ec3f2d 100644 --- a/src/modules/tracker-humanity.js +++ b/src/modules/tracker-humanity.js @@ -1,4 +1,4 @@ -const store = require('store2'); +const store = require('../store'); const humanEvents = [ 'scroll', diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js index 753a2967..1928f790 100644 --- a/src/modules/tracker-requests.js +++ b/src/modules/tracker-requests.js @@ -1,6 +1,6 @@ -const store = require('store2'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); +const store = require('../store'); const utils = require('../utils'); const trackerHumanity = require('./tracker-humanity'); diff --git a/src/store.js b/src/store.js new file mode 100644 index 00000000..6f534c0f --- /dev/null +++ b/src/store.js @@ -0,0 +1,8 @@ +const store = require('store2'); +const overflow = require('./store.overflow'); + +// Inject overflow into store +// https://raw.githubusercontent.com/nbubna/store/master/src/store.overflow.js +overflow(store, store._); + +module.exports = store; diff --git a/src/store.overflow.js b/src/store.overflow.js new file mode 100644 index 00000000..ba62e875 --- /dev/null +++ b/src/store.overflow.js @@ -0,0 +1,90 @@ +/* eslint-disable */ +/** + * Copyright (c) 2013 ESHA Research + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * When quota is reached on a storage area, this shifts incoming values to + * fake storage, so they last only as long as the page does. This is useful + * because it is more burdensome for localStorage to recover from quota errors + * than incomplete caches. In other words, it is wiser to rely on store.js + * never complaining than never missing data. You should already be checking + * the integrity of cached data on every page load. + * + * Status: BETA + */ +module.exports = function (store, _) { + var _set = _.set, + _get = _.get, + _remove = _.remove, + _key = _.key, + _length = _.length, + _clear = _.clear; + + _.overflow = function (area, create) { + var name = area === _.areas.local ? '+local+' : + area === _.areas.session ? '+session+' : false; + if (name) { + var overflow = _.areas[name]; + if (create && !overflow) { + overflow = store.area(name)._area; // area() copies to _.areas + } else if (create === false) { + delete _.areas[name]; + delete store[name]; + } + return overflow; + } + }; + _.set = function (area, key, string) { + try { + _set.apply(this, arguments); + } catch (e) { + if (e.name === 'QUOTA_EXCEEDED_ERR' || + e.name === 'NS_ERROR_DOM_QUOTA_REACHED' || + e.toString().indexOf("QUOTA_EXCEEDED_ERR") !== -1 || + e.toString().indexOf("QuotaExceededError") !== -1) { + // the e.toString is needed for IE9 / IE10, cos name is empty there + return _.set(_.overflow(area, true), key, string); + } + throw e; + } + }; + _.get = function (area, key) { + var overflow = _.overflow(area); + return (overflow && _get.call(this, overflow, key)) || + _get.apply(this, arguments); + }; + _.remove = function (area, key) { + var overflow = _.overflow(area); + if (overflow) { + _remove.call(this, overflow, key); + } + _remove.apply(this, arguments); + }; + _.key = function (area, i) { + var overflow = _.overflow(area); + if (overflow) { + var l = _length.call(this, area); + if (i >= l) { + i = i - l; // make i overflow-relative + for (var j = 0, m = _length.call(this, overflow); j < m; j++) { + if (j === i) { // j is overflow index + return _key.call(this, overflow, j); + } + } + } + } + return _key.apply(this, arguments); + }; + _.length = function (area) { + var length = _length(area), + overflow = _.overflow(area); + return overflow ? length + _length(overflow) : length; + }; + _.clear = function (area) { + _.overflow(area, false); + _clear.apply(this, arguments); + }; + +}; \ No newline at end of file From 14395646c7388bc18a6a1f52b2ea784a4ed4a37a Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Wed, 9 Oct 2019 18:36:40 -0400 Subject: [PATCH 40/56] Linting error --- spec/mocha.helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js index 6a0d29f4..b729ec5d 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -1,5 +1,5 @@ -const store = require('../src/store'); const { JSDOM } = require('jsdom'); +const store = require('../src/store'); // Setup mock DOM environment const setupDOM = () => { From 4d13d31ead1f17a511e72fe10ff847d6fe7cf5b0 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 12:27:56 -0600 Subject: [PATCH 41/56] Verify parameters on first tracker test. --- spec/src/modules/tracker.js | 104 ++++++++++++-------------------- src/modules/tracker-requests.js | 5 +- 2 files changed, 42 insertions(+), 67 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 48a9dc22..6b09419a 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -1,42 +1,66 @@ +/* eslint-disable no-unused-expressions */ const jsdom = require('mocha-jsdom'); const dotenv = require('dotenv'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); +const fetchPonyfill = require('fetch-ponyfill'); +const Promise = require('es6-promise'); +const store = require('../../../src/store'); const ConstructorIO = require('../../../src/constructorio'); +const helpers = require('../../mocha.helpers'); chai.use(chaiAsPromised); +chai.use(sinonChai); dotenv.config(); const testApiKey = process.env.TEST_API_KEY; +const { fetch } = fetchPonyfill({ Promise }); describe('ConstructorIO - Tracker', () => { + const clientVersion = 'cio-mocha'; + let fetchSpy; + jsdom({ url: 'http://localhost' }); - describe('sendSessionStart', () => { - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); + beforeEach(() => { + store.session.set('_constructorio_is_human', true); - afterEach(() => { - delete global.CLIENT_VERSION; - }); + global.CLIENT_VERSION = clientVersion; + fetchSpy = sinon.spy(fetch); + }); + + afterEach(() => { + helpers.clearStorage(); + delete global.CLIENT_VERSION; + + fetchSpy = null; + }); + + describe('sendSessionStart', () => { it('Should respond with a valid response', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendSessionStart()).to.equal(true); - }); - }); - describe('sendInputFocus', () => { - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - afterEach(() => { - delete global.CLIENT_VERSION; + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('i'); + expect(requestedUrlParams).to.have.property('s'); + expect(requestedUrlParams).to.have.property('action').to.equal('session_start'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + expect(requestedUrlParams).to.have.property('_dt'); }); + }); + describe('sendInputFocus', () => { it('Should respond with a valid response', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -55,14 +79,6 @@ describe('ConstructorIO - Tracker', () => { display_name: 'display-name', }; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); - - afterEach(() => { - delete global.CLIENT_VERSION; - }); - it('Should respond with a valid response when term and parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -103,14 +119,6 @@ describe('ConstructorIO - Tracker', () => { display_name: 'display-name', }; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); - - afterEach(() => { - delete global.CLIENT_VERSION; - }); - it('Should respond with a valid response when term and parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -149,14 +157,6 @@ describe('ConstructorIO - Tracker', () => { customer_ids: [1, 2, 3], }; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); - - afterEach(() => { - delete global.CLIENT_VERSION; - }); - it('Should respond with a valid response when term and parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -196,14 +196,6 @@ describe('ConstructorIO - Tracker', () => { result_id: 'result-id', }; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); - - afterEach(() => { - delete global.CLIENT_VERSION; - }); - it('Should respond with a valid response when term and parmeters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -245,14 +237,6 @@ describe('ConstructorIO - Tracker', () => { section: 'Products', }; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); - - afterEach(() => { - delete global.CLIENT_VERSION; - }); - it('Should respond with a valid response when term and parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -285,14 +269,6 @@ describe('ConstructorIO - Tracker', () => { section: 'Products', }; - beforeEach(() => { - global.CLIENT_VERSION = 'cio-mocha'; - }); - - afterEach(() => { - delete global.CLIENT_VERSION; - }); - it('Should respond with a valid response when parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js index 1928f790..2bcc0d0c 100644 --- a/src/modules/tracker-requests.js +++ b/src/modules/tracker-requests.js @@ -4,9 +4,8 @@ const store = require('../store'); const utils = require('../utils'); const trackerHumanity = require('./tracker-humanity'); -const { fetch } = fetchPonyfill({ Promise }); - -const trackerRequests = () => { +const trackerRequests = (options) => { + const fetch = (options && options.fetch) || fetchPonyfill({ Promise }).fetch; const humanity = trackerHumanity(); const storageKey = '_constructorio_requests'; let requestPending = false; From 51fefcb4d0326b6dd469e69c427135c9d8d84c10 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 12:53:15 -0600 Subject: [PATCH 42/56] Use cleanParams and append datetime parameter to search requests. --- spec/src/modules/search.js | 2 ++ src/modules/search.js | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spec/src/modules/search.js b/spec/src/modules/search.js index 99b71b3f..6ba4f3b3 100644 --- a/spec/src/modules/search.js +++ b/spec/src/modules/search.js @@ -59,6 +59,7 @@ describe('ConstructorIO - Search', () => { expect(requestedUrlParams).to.have.property('s'); expect(requestedUrlParams).to.have.property('section').to.equal(section); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + expect(requestedUrlParams).to.have.property('_dt'); done(); }); }); @@ -338,6 +339,7 @@ describe('ConstructorIO - Search', () => { expect(requestedUrlParams).to.have.property('section').to.equal(section); expect(requestedUrlParams).to.have.property('filters'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + expect(requestedUrlParams).to.have.property('_dt'); done(); }); }); diff --git a/src/modules/search.js b/src/modules/search.js index 4b7530cb..a0b200cb 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -1,8 +1,8 @@ -/* eslint-disable object-curly-newline */ +/* eslint-disable object-curly-newline, no-underscore-dangle */ const qs = require('qs'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const { throwHttpErrorFromResponse } = require('../utils'); +const { throwHttpErrorFromResponse, cleanParams } = require('../utils'); /** * Interface to search related API calls. @@ -26,7 +26,7 @@ const search = (options) => { segments, testCells, } = options; - const queryParams = { c: version }; + let queryParams = { c: version }; queryParams.key = apiKey; queryParams.i = clientId; @@ -88,6 +88,9 @@ const search = (options) => { } } + queryParams._dt = Date.now(); + queryParams = cleanParams(queryParams); + const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/search/${encodeURIComponent(query)}?${queryString}`; @@ -96,7 +99,7 @@ const search = (options) => { // Create URL from supplied group ID and parameters const createBrowseUrl = (parameters) => { const { apiKey, version, serviceUrl, sessionId, clientId, segments, testCells } = options; - const queryParams = { c: version }; + let queryParams = { c: version }; queryParams.key = apiKey; queryParams.i = clientId; @@ -147,6 +150,9 @@ const search = (options) => { } } + queryParams._dt = Date.now(); + queryParams = cleanParams(queryParams); + const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/search/?${queryString}`; From 2fc3a6a833b04b75f1341c8d8e32a022051891e5 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 14:40:09 -0600 Subject: [PATCH 43/56] Apply cleanParams to autocomplete and recommendations. --- spec/src/modules/autocomplete.js | 1 + src/modules/autocomplete.js | 9 ++++++--- src/modules/recommendations.js | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/src/modules/autocomplete.js b/spec/src/modules/autocomplete.js index 3421f612..0cc69413 100644 --- a/spec/src/modules/autocomplete.js +++ b/spec/src/modules/autocomplete.js @@ -55,6 +55,7 @@ describe('ConstructorIO - Autocomplete', () => { 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'); done(); }); }); diff --git a/src/modules/autocomplete.js b/src/modules/autocomplete.js index 79621c32..be52ce9a 100644 --- a/src/modules/autocomplete.js +++ b/src/modules/autocomplete.js @@ -1,8 +1,8 @@ -/* eslint-disable object-curly-newline */ +/* eslint-disable object-curly-newline, no-underscore-dangle */ const qs = require('qs'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const { throwHttpErrorFromResponse } = require('../utils'); +const { throwHttpErrorFromResponse, cleanParams } = require('../utils'); /** * Interface to autocomplete related API calls. @@ -26,7 +26,7 @@ const autocomplete = (options) => { segments, testCells, } = options; - const queryParams = { c: version }; + let queryParams = { c: version }; queryParams.key = apiKey; queryParams.i = clientId; @@ -75,6 +75,9 @@ const autocomplete = (options) => { } } + queryParams._dt = Date.now(); + queryParams = cleanParams(queryParams); + const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/autocomplete/${encodeURIComponent(query)}?${queryString}`; diff --git a/src/modules/recommendations.js b/src/modules/recommendations.js index 975a5b05..acfdaa1f 100644 --- a/src/modules/recommendations.js +++ b/src/modules/recommendations.js @@ -2,7 +2,7 @@ const qs = require('qs'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const { throwHttpErrorFromResponse } = require('../utils'); +const { throwHttpErrorFromResponse, cleanParams } = require('../utils'); /** * Interface to recommendations related API calls. @@ -17,7 +17,7 @@ const recommendations = (options) => { // Create URL from supplied parameters const createRecommendationsUrl = (parameters, endpoint) => { const { apiKey, version, serviceUrl, sessionId, userId, clientId, segments } = options; - const queryParams = { c: version }; + let queryParams = { c: version }; const validEndpoints = [ 'alternative_items', 'complementary_items', @@ -58,6 +58,8 @@ const recommendations = (options) => { } } + queryParams = cleanParams(queryParams); + const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/recommendations/${endpoint}/?${queryString}`; From dbae36085a8c1a95e2cc3a0eae9dec81df532fa5 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 14:55:23 -0600 Subject: [PATCH 44/56] Verify outgoing query parameters via spied fetch for tracking tests. --- spec/src/modules/tracker.js | 127 +++++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 8 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 6b09419a..12cb1f96 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -18,7 +18,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; const { fetch } = fetchPonyfill({ Promise }); -describe('ConstructorIO - Tracker', () => { +describe.only('ConstructorIO - Tracker', () => { const clientVersion = 'cio-mocha'; let fetchSpy; @@ -62,9 +62,22 @@ describe('ConstructorIO - Tracker', () => { describe('sendInputFocus', () => { it('Should respond with a valid response', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendInputFocus()).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('i'); + expect(requestedUrlParams).to.have.property('s'); + expect(requestedUrlParams).to.have.property('action').to.equal('focus'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + expect(requestedUrlParams).to.have.property('_dt'); }); }); @@ -80,9 +93,28 @@ describe('ConstructorIO - Tracker', () => { }; it('Should respond with a valid response when term and parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendAutocompleteSelect(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + 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'); + expect(requestedUrlParams).to.have.property('original_query').to.equal(parameters.original_query); + expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); + expect(requestedUrlParams).to.have.property('result_id').to.equal(parameters.result_id); + expect(requestedUrlParams).to.have.property('group').to.deep.equal({ + group_id: parameters.group_id, + display_name: parameters.display_name, + }); }); it('Should throw an error when invalid term is provided', () => { @@ -120,9 +152,27 @@ describe('ConstructorIO - Tracker', () => { }; it('Should respond with a valid response when term and parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendAutocompleteSearch(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + 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'); + expect(requestedUrlParams).to.have.property('original_query').to.equal(parameters.original_query); + expect(requestedUrlParams).to.have.property('result_id').to.equal(parameters.result_id); + expect(requestedUrlParams).to.have.property('group').to.deep.equal({ + group_id: parameters.group_id, + display_name: parameters.display_name, + }); }); it('Should throw an error when invalid term is provided', () => { @@ -158,9 +208,23 @@ describe('ConstructorIO - Tracker', () => { }; it('Should respond with a valid response when term and parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendSearchResults(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + 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'); + expect(requestedUrlParams).to.have.property('num_results').to.equal(parameters.num_results.toString()); + expect(requestedUrlParams).to.have.property('customer_ids').to.equal(parameters.customer_ids.join(',')); }); it('Should throw an error when invalid term is provided', () => { @@ -197,9 +261,24 @@ describe('ConstructorIO - Tracker', () => { }; it('Should respond with a valid response when term and parmeters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendSearchResultClick(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + 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'); + expect(requestedUrlParams).to.have.property('name').to.equal(parameters.name); + expect(requestedUrlParams).to.have.property('customer_id').to.equal(parameters.customer_id); + expect(requestedUrlParams).to.have.property('result_id').to.equal(parameters.result_id); }); it('Should throw an error when invalid term is provided', () => { @@ -238,9 +317,26 @@ describe('ConstructorIO - Tracker', () => { }; it('Should respond with a valid response when term and parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendConversion(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + 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'); + expect(requestedUrlParams).to.have.property('name').to.equal(parameters.name); + expect(requestedUrlParams).to.have.property('customer_id').to.equal(parameters.customer_id); + expect(requestedUrlParams).to.have.property('result_id').to.equal(parameters.result_id); + expect(requestedUrlParams).to.have.property('revenue').to.equal(parameters.revenue.toString()); + expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); }); it('Should respond with a valid response when no term is provided, but parameters are', () => { @@ -270,9 +366,24 @@ describe('ConstructorIO - Tracker', () => { }; it('Should respond with a valid response when parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); expect(tracker.sendPurchase(parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + 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'); + expect(requestedUrlParams).to.have.property('customer_ids').to.equal(parameters.customer_ids); + expect(requestedUrlParams).to.have.property('revenue').to.equal(parameters.revenue.toString()); + expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); }); it('Should throw an error when invalid parameters are provided', () => { From 5aea236cdd82b7145a8f00c48cce4dc67660a2be Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 14:59:50 -0600 Subject: [PATCH 45/56] Naming consistency tweaks. --- spec/src/modules/tracker.js | 2 +- src/modules/tracker.js | 100 ++++++++++++++++++------------------ 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 12cb1f96..8f0714c7 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -18,7 +18,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; const { fetch } = fetchPonyfill({ Promise }); -describe.only('ConstructorIO - Tracker', () => { +describe('ConstructorIO - Tracker', () => { const clientVersion = 'cio-mocha'; let fetchSpy; diff --git a/src/modules/tracker.js b/src/modules/tracker.js index a3e0904b..ce992104 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -14,38 +14,38 @@ const tracker = (options) => { const requests = trackerRequests(options); // Append common parameters to supplied parameters object - const createQueryString = (queryParamsObj) => { + const createQueryString = (parameters) => { const { apiKey, version, sessionId, clientId, userId, segments } = options; - let paramsObj = Object.assign(queryParamsObj); + let queryParams = Object.assign(parameters); if (version) { - paramsObj.c = version; + queryParams.c = version; } if (clientId) { - paramsObj.i = clientId; + queryParams.i = clientId; } if (sessionId) { - paramsObj.s = sessionId; + queryParams.s = sessionId; } if (userId) { - paramsObj.ui = userId; + queryParams.ui = userId; } if (segments && segments.length) { - paramsObj.us = segments; + queryParams.us = segments; } if (apiKey) { - paramsObj.key = apiKey; + queryParams.key = apiKey; } - paramsObj._dt = Date.now(); - paramsObj = utils.cleanParams(paramsObj); + queryParams._dt = Date.now(); + queryParams = utils.cleanParams(queryParams); - return qs.stringify(paramsObj, { indices: false }); + return qs.stringify(queryParams, { indices: false }); }; return { @@ -57,9 +57,9 @@ const tracker = (options) => { */ sendSessionStart: () => { const url = `${options.serviceUrl}/behavior?`; - const queryParamsObj = { action: 'session_start' }; + const queryParams = { action: 'session_start' }; - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; @@ -73,9 +73,9 @@ const tracker = (options) => { */ sendInputFocus: () => { const url = `${options.serviceUrl}/behavior?`; - const queryParamsObj = { action: 'focus' }; + const queryParams = { action: 'focus' }; - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; @@ -101,7 +101,7 @@ const tracker = (options) => { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/select?`; - const queryParamsObj = {}; + const queryParams = {}; const { original_query, result_id, @@ -113,29 +113,29 @@ const tracker = (options) => { } = parameters; if (original_query) { - queryParamsObj.original_query = original_query; + queryParams.original_query = original_query; } if (tr) { - queryParamsObj.tr = tr; + queryParams.tr = tr; } if (original_section || section) { - queryParamsObj.section = original_section || section; + queryParams.section = original_section || section; } if (group_id) { - queryParamsObj.group = { + queryParams.group = { group_id, display_name, }; } if (result_id) { - queryParamsObj.result_id = result_id; + queryParams.result_id = result_id; } - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; @@ -169,25 +169,25 @@ const tracker = (options) => { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/search?`; - const queryParamsObj = {}; + const queryParams = {}; const { original_query, result_id, group_id, display_name } = parameters; if (original_query) { - queryParamsObj.original_query = original_query; + queryParams.original_query = original_query; } if (group_id) { - queryParamsObj.group = { + queryParams.group = { group_id, display_name, }; } if (result_id) { - queryParamsObj.result_id = result_id; + queryParams.result_id = result_id; } - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; @@ -219,18 +219,18 @@ const tracker = (options) => { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/behavior?`; - const queryParamsObj = { action: 'search-results', term }; + const queryParams = { action: 'search-results', term }; const { num_results, customer_ids } = parameters; if (num_results) { - queryParamsObj.num_results = num_results; + queryParams.num_results = num_results; } if (customer_ids && Array.isArray(customer_ids)) { - queryParamsObj.customer_ids = customer_ids.join(','); + queryParams.customer_ids = customer_ids.join(','); } - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; @@ -263,22 +263,22 @@ const tracker = (options) => { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/autocomplete/${utils.ourEncodeURIComponent(term)}/click_through?`; - const queryParamsObj = {}; + const queryParams = {}; const { name, customer_id, result_id } = parameters; if (name) { - queryParamsObj.name = name; + queryParams.name = name; } if (customer_id) { - queryParamsObj.customer_id = customer_id; + queryParams.customer_id = customer_id; } if (result_id) { - queryParamsObj.result_id = result_id; + queryParams.result_id = result_id; } - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; @@ -312,32 +312,32 @@ const tracker = (options) => { if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const searchTerm = utils.ourEncodeURIComponent(term) || 'TERM_UNKNOWN'; const url = `${options.serviceUrl}/autocomplete/${searchTerm}/conversion?`; - const queryParamsObj = {}; + const queryParams = {}; const { name, customer_id, result_id, revenue, section } = parameters; if (name) { - queryParamsObj.name = name; + queryParams.name = name; } if (customer_id) { - queryParamsObj.customer_id = customer_id; + queryParams.customer_id = customer_id; } if (result_id) { - queryParamsObj.result_id = result_id; + queryParams.result_id = result_id; } if (revenue) { - queryParamsObj.revenue = revenue; + queryParams.revenue = revenue; } if (section) { - queryParamsObj.section = section; + queryParams.section = section; } else { - queryParamsObj.section = 'Products'; + queryParams.section = 'Products'; } - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; @@ -362,25 +362,25 @@ const tracker = (options) => { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/autocomplete/TERM_UNKNOWN/purchase?`; - const queryParamsObj = {}; + const queryParams = {}; const { customer_ids, revenue, section } = parameters; if (customer_ids) { - queryParamsObj.customer_ids = customer_ids; + queryParams.customer_ids = customer_ids; } if (revenue) { - queryParamsObj.revenue = revenue; + queryParams.revenue = revenue; } if (section) { - queryParamsObj.section = section; + queryParams.section = section; } else { - queryParamsObj.section = 'Products'; + queryParams.section = 'Products'; } - requests.queue(`${url}${createQueryString(queryParamsObj)}`); + requests.queue(`${url}${createQueryString(queryParams)}`); requests.send(); return true; From 2dd281bba2a497c17abfc9357425d54b168e7ebc Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 15:01:53 -0600 Subject: [PATCH 46/56] Reduce wait interval for request send tests. --- spec/src/modules/tracker-requests.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js index 738321a0..3d0ba894 100644 --- a/spec/src/modules/tracker-requests.js +++ b/spec/src/modules/tracker-requests.js @@ -9,8 +9,9 @@ const helpers = require('../../mocha.helpers'); chai.use(chaiAsPromised); dotenv.config(); -describe('ConstructorIO - Tracker - Requests', () => { +describe.only('ConstructorIO - Tracker - Requests', () => { const storageKey = '_constructorio_requests'; + const waitInterval = 500; describe('queue', () => { let defaultAgent; @@ -101,7 +102,7 @@ describe('ConstructorIO - Tracker - Requests', () => { setTimeout(() => { expect(requests.get()).to.be.an('array').length(0); done(); - }, 1000); + }, waitInterval); }); it('Should not send tracking requests if queue is populated and user is not human', (done) => { @@ -117,7 +118,7 @@ describe('ConstructorIO - Tracker - Requests', () => { setTimeout(() => { expect(requests.get()).to.be.an('array').length(3); done(); - }, 1000); + }, waitInterval); }); it('Should not send tracking requests if queue is populated and user is human and page is unloading', (done) => { @@ -135,7 +136,7 @@ describe('ConstructorIO - Tracker - Requests', () => { setTimeout(() => { expect(requests.get()).to.be.an('array').length(3); done(); - }, 1000); + }, waitInterval); }); it('Should send all tracking requests if requests exist in storage and user is human', (done) => { @@ -154,7 +155,7 @@ describe('ConstructorIO - Tracker - Requests', () => { setTimeout(() => { expect(requests.get()).to.be.an('array').length(0); done(); - }, 1000); + }, waitInterval); }); it('Should not send tracking requests if requests exist in storage and user is not human', (done) => { @@ -172,7 +173,7 @@ describe('ConstructorIO - Tracker - Requests', () => { setTimeout(() => { expect(requests.get()).to.be.an('array').length(3); done(); - }, 1000); + }, waitInterval); }); it('Should not send tracking requests if requests exist in storage and user is human and page is unloading', (done) => { @@ -192,7 +193,7 @@ describe('ConstructorIO - Tracker - Requests', () => { setTimeout(() => { expect(requests.get()).to.be.an('array').length(3); done(); - }, 1000); + }, waitInterval); }); }); }); From 5007e2f6bdd1321a6608a47a3aa4c57d845abde5 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 15:15:40 -0600 Subject: [PATCH 47/56] Support sending userId and add tests for search module. --- spec/src/modules/search.js | 41 ++++++++++++++++++++++++++++ spec/src/modules/tracker-requests.js | 2 +- src/modules/search.js | 16 ++++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/spec/src/modules/search.js b/spec/src/modules/search.js index 6ba4f3b3..ace75c19 100644 --- a/spec/src/modules/search.js +++ b/spec/src/modules/search.js @@ -104,6 +104,25 @@ describe('ConstructorIO - Search', () => { }); }); + it('Should return a response with a valid query, section and user id', (done) => { + const userId = 'user-id'; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { section }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + done(); + }); + }); + it('Should return a response with a valid query, section, and page', (done) => { const page = 1; const { search } = new ConstructorIO({ @@ -390,6 +409,28 @@ describe('ConstructorIO - Search', () => { }); }); + it('Should return a response with a valid group_id, section and user id', (done) => { + const userId = 'user-id'; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + search.getBrowseResults({ + section, + filters, + }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + done(); + }); + }); + it('Should return a response with a valid group_id, section, and page', (done) => { const page = 1; const { search } = new ConstructorIO({ diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js index 3d0ba894..629a87f1 100644 --- a/spec/src/modules/tracker-requests.js +++ b/spec/src/modules/tracker-requests.js @@ -9,7 +9,7 @@ const helpers = require('../../mocha.helpers'); chai.use(chaiAsPromised); dotenv.config(); -describe.only('ConstructorIO - Tracker - Requests', () => { +describe('ConstructorIO - Tracker - Requests', () => { const storageKey = '_constructorio_requests'; const waitInterval = 500; diff --git a/src/modules/search.js b/src/modules/search.js index a0b200cb..5031518f 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -98,7 +98,16 @@ const search = (options) => { // Create URL from supplied group ID and parameters const createBrowseUrl = (parameters) => { - const { apiKey, version, serviceUrl, sessionId, clientId, segments, testCells } = options; + const { + apiKey, + version, + serviceUrl, + sessionId, + clientId, + userId, + segments, + testCells, + } = options; let queryParams = { c: version }; queryParams.key = apiKey; @@ -117,6 +126,11 @@ const search = (options) => { queryParams.us = segments; } + // Pull user id from options + if (userId) { + queryParams.ui = userId; + } + if (parameters) { const { page, resultsPerPage, filters, sortBy, sortOrder, section } = parameters; From fb9cc811dc74fc740971c4a94771b5a3776755c8 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 15:20:12 -0600 Subject: [PATCH 48/56] Add tests to validate user id being sent with requests. --- spec/src/modules/autocomplete.js | 19 ++++++++ spec/src/modules/recommendations.js | 76 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/spec/src/modules/autocomplete.js b/spec/src/modules/autocomplete.js index 0cc69413..c5e45bf0 100644 --- a/spec/src/modules/autocomplete.js +++ b/spec/src/modules/autocomplete.js @@ -100,6 +100,25 @@ describe('ConstructorIO - Autocomplete', () => { }); }); + it('Should return a response with a valid query, and user id', (done) => { + const userId = 'user-id'; + const { autocomplete } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + autocomplete.getResults(query).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('sections').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + done(); + }); + }); + it('Should return a response with a valid query, and results', (done) => { const results = 2; const { autocomplete } = new ConstructorIO({ diff --git a/spec/src/modules/recommendations.js b/spec/src/modules/recommendations.js index 95a9cbbc..b3e65ee1 100644 --- a/spec/src/modules/recommendations.js +++ b/spec/src/modules/recommendations.js @@ -100,6 +100,25 @@ describe('ConstructorIO - Recommendations', () => { }); }); + it('Should return a response with valid itemIds, and user id', (done) => { + const userId = 'user-id'; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + recommendations.getAlternativeItems(itemId).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + done(); + }); + }); + it('Should return a response with valid itemIds, and results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ @@ -225,6 +244,25 @@ describe('ConstructorIO - Recommendations', () => { }); }); + it('Should return a response with valid itemIds, and user id', (done) => { + const userId = 'user-id'; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + recommendations.getComplementaryItems(itemId).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + done(); + }); + }); + it('Should return a response with valid itemIds, and results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ @@ -333,6 +371,25 @@ describe('ConstructorIO - Recommendations', () => { }); }); + it('Should return a response with valid user id', (done) => { + const userId = 'user-id'; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + recommendations.getRecentlyViewedItems().then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + done(); + }); + }); + it('Should return a response with valid results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ @@ -429,6 +486,25 @@ describe('ConstructorIO - Recommendations', () => { }); }); + it('Should return a response with valid user id', (done) => { + const userId = 'user-id'; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + recommendations.getUserFeaturedItems().then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + done(); + }); + }); + it('Should return a response with valid results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ From 4303aca7bc930cc63194437cd983f4d948d476a9 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 15:29:11 -0600 Subject: [PATCH 49/56] Add tests to validate user id and segments for tracking and recommendations modules. --- spec/src/modules/tracker.js | 242 +++++++++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 1 deletion(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 8f0714c7..fce35ab2 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -18,7 +18,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; const { fetch } = fetchPonyfill({ Promise }); -describe('ConstructorIO - Tracker', () => { +describe.only('ConstructorIO - Tracker', () => { const clientVersion = 'cio-mocha'; let fetchSpy; @@ -58,6 +58,36 @@ describe('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); expect(requestedUrlParams).to.have.property('_dt'); }); + + it('Should respond with a valid response with segments', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendSessionStart()).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response with user id', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendSessionStart()).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); }); describe('sendInputFocus', () => { @@ -79,6 +109,36 @@ describe('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); expect(requestedUrlParams).to.have.property('_dt'); }); + + it('Should respond with a valid response with segments', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendInputFocus()).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response with user id', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendInputFocus()).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); }); describe('sendAutocompleteSelect', () => { @@ -117,6 +177,36 @@ describe('ConstructorIO - Tracker', () => { }); }); + it('Should respond with a valid response when term, parameters and segments are provided', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendAutocompleteSelect(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response when term, parameters and user id are provided', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendAutocompleteSelect(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); + it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -175,6 +265,36 @@ describe('ConstructorIO - Tracker', () => { }); }); + it('Should respond with a valid response when term, parameters and segments are provided', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendAutocompleteSearch(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response when term, parameters and user id are provided', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendAutocompleteSearch(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); + it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -227,6 +347,36 @@ describe('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('customer_ids').to.equal(parameters.customer_ids.join(',')); }); + it('Should respond with a valid response when term, parameters and segments are provided', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendSearchResults(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response when term, parameters and user id are provided', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendSearchResults(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); + it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -281,6 +431,36 @@ describe('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('result_id').to.equal(parameters.result_id); }); + it('Should respond with a valid response when term, parameters and segments are provided', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendSearchResultClick(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response when term, parameters and user id are provided', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendSearchResultClick(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); + it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -339,6 +519,36 @@ describe('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); }); + it('Should respond with a valid response when term, parameters and segments are provided', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendConversion(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response when term, parameters and user id are provided', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendConversion(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); + it('Should respond with a valid response when no term is provided, but parameters are', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); @@ -386,6 +596,36 @@ describe('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); }); + it('Should respond with a valid response parameters and segments are provided', () => { + const segments = ['foo', 'bar']; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + segments, + fetch: fetchSpy, + }); + + expect(tracker.sendPurchase(parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('us').to.deep.equal(segments); + }); + + it('Should respond with a valid response when parameters and user id are provided', () => { + const userId = 'user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + userId, + fetch: fetchSpy, + }); + + expect(tracker.sendPurchase(parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ui').to.equal(userId); + }); + it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); From 7b5b34663b9864170c015ef60954e0753c904bb0 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sat, 12 Oct 2019 15:36:28 -0600 Subject: [PATCH 50/56] Add tests to validate defaulting of section for tracking requests. --- spec/src/modules/tracker.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index fce35ab2..d3024adc 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -18,7 +18,7 @@ dotenv.config(); const testApiKey = process.env.TEST_API_KEY; const { fetch } = fetchPonyfill({ Promise }); -describe.only('ConstructorIO - Tracker', () => { +describe('ConstructorIO - Tracker', () => { const clientVersion = 'cio-mocha'; let fetchSpy; @@ -519,6 +519,19 @@ describe.only('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); }); + it('Should respond with a valid response and section should be defaulted when term and parameters are provided', () => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + expect(tracker.sendConversion(term, parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('section').to.equal('Products'); + }); + it('Should respond with a valid response when term, parameters and segments are provided', () => { const segments = ['foo', 'bar']; const { tracker } = new ConstructorIO({ @@ -596,6 +609,19 @@ describe.only('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); }); + it('Should respond with a valid response and section should be defaulted when term and parameters are provided', () => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + expect(tracker.sendPurchase(parameters)).to.equal(true); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('section').to.equal('Products'); + }); + it('Should respond with a valid response parameters and segments are provided', () => { const segments = ['foo', 'bar']; const { tracker } = new ConstructorIO({ From 70223119b8078710769e67e7157dd25f0b49a50c Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sun, 13 Oct 2019 11:15:39 -0600 Subject: [PATCH 51/56] Move store into folder, exclude from coverage reports. --- .nycrc | 3 +++ spec/mocha.helpers.js | 2 +- spec/src/modules/tracker-humanity.js | 2 +- spec/src/modules/tracker-requests.js | 2 +- spec/src/modules/tracker.js | 2 +- src/modules/tracker-humanity.js | 2 +- src/modules/tracker-requests.js | 2 +- src/{ => store}/store.js | 0 src/{ => store}/store.overflow.js | 0 9 files changed, 9 insertions(+), 6 deletions(-) rename src/{ => store}/store.js (100%) rename src/{ => store}/store.overflow.js (100%) diff --git a/.nycrc b/.nycrc index bc37942d..3c30c5b8 100644 --- a/.nycrc +++ b/.nycrc @@ -1,4 +1,7 @@ { + "exclude": [ + "src/store/*" + ], "include": [ "src/*" ], diff --git a/spec/mocha.helpers.js b/spec/mocha.helpers.js index 5aab84b0..a249ffaa 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -1,6 +1,6 @@ const qs = require('qs'); const { JSDOM } = require('jsdom'); -const store = require('../src/store'); +const store = require('../src/store/store'); // Setup mock DOM environment const setupDOM = () => { diff --git a/spec/src/modules/tracker-humanity.js b/spec/src/modules/tracker-humanity.js index 0c69963a..a7a0073e 100644 --- a/spec/src/modules/tracker-humanity.js +++ b/spec/src/modules/tracker-humanity.js @@ -1,7 +1,7 @@ const dotenv = require('dotenv'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const store = require('../../../src/store'); +const store = require('../../../src/store/store'); const trackerHumanity = require('../../../src/modules/tracker-humanity'); const helpers = require('../../mocha.helpers'); diff --git a/spec/src/modules/tracker-requests.js b/spec/src/modules/tracker-requests.js index 629a87f1..1dd60f45 100644 --- a/spec/src/modules/tracker-requests.js +++ b/spec/src/modules/tracker-requests.js @@ -2,7 +2,7 @@ const dotenv = require('dotenv'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const store = require('../../../src/store'); +const store = require('../../../src/store/store'); const trackerRequests = require('../../../src/modules/tracker-requests'); const helpers = require('../../mocha.helpers'); diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index d3024adc..2d0eb24d 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -7,7 +7,7 @@ const sinon = require('sinon'); const sinonChai = require('sinon-chai'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const store = require('../../../src/store'); +const store = require('../../../src/store/store'); const ConstructorIO = require('../../../src/constructorio'); const helpers = require('../../mocha.helpers'); diff --git a/src/modules/tracker-humanity.js b/src/modules/tracker-humanity.js index 44ec3f2d..a6f4424f 100644 --- a/src/modules/tracker-humanity.js +++ b/src/modules/tracker-humanity.js @@ -1,4 +1,4 @@ -const store = require('../store'); +const store = require('../store/store'); const humanEvents = [ 'scroll', diff --git a/src/modules/tracker-requests.js b/src/modules/tracker-requests.js index 2bcc0d0c..c93919a2 100644 --- a/src/modules/tracker-requests.js +++ b/src/modules/tracker-requests.js @@ -1,6 +1,6 @@ const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const store = require('../store'); +const store = require('../store/store'); const utils = require('../utils'); const trackerHumanity = require('./tracker-humanity'); diff --git a/src/store.js b/src/store/store.js similarity index 100% rename from src/store.js rename to src/store/store.js diff --git a/src/store.overflow.js b/src/store/store.overflow.js similarity index 100% rename from src/store.overflow.js rename to src/store/store.overflow.js From a515cf646ef0404d783e18d3446e4cadb47d6e49 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sun, 13 Oct 2019 11:32:02 -0600 Subject: [PATCH 52/56] Update readme to document new methods. --- README.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/README.md b/README.md index 12360865..2018d558 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,93 @@ constructorio.recommendations.getUserFeaturedItems({ parameters }).then(function | --- | --- | --- | | `results` | number | Number of results to retrieve | +### Tracker + +The tracker module can be used to send tracking events. Returns `true` when successful, or will throw an error if an issue is encountered. + +#### Send autocomplete select event +```javascript +constructorio.tracker.sendAutocompleteSelect('dogs', { + parameters +}); +``` + +| Parameter | Type | Description | +| --- | --- | --- | +| `original_query` | string | ? | +| `result_id` | string | ? | +| `section` | string | ? | +| `tr` | string | ? | +| `group_id` | string | ? | +| `display_name` | string | ? | + +#### Send autocomplete search event +```javascript +constructorio.tracker.sendAutocompleteSearch('dogs', { + parameters +}); +``` + +| Parameter | Type | Description | +| --- | --- | --- | +| `original_query` | string | ? | +| `result_id` | string | ? | +| `group_id` | string | ? | +| `display_name` | string | ? | + +#### Send search results event +```javascript +constructorio.tracker.sendSearchResults('dogs', { + parameters +}); +``` + +| Parameter | Type | Description | +| --- | --- | --- | +| `num_results` | string | ? | +| `customer_ids` | string | ? | + +#### Send search result click event +```javascript +constructorio.tracker.sendSearchResultClick('dogs', { + parameters +}); +``` + +| Parameter | Type | Description | +| --- | --- | --- | +| `name` | string | ? | +| `customer_id` | string | ? | +| `result_id` | string | ? | + +#### Send conversion event +```javascript +constructorio.tracker.sendConversion('dogs', { + parameters +}); +``` + +| Parameter | Type | Description | +| --- | --- | --- | +| `name` | string | ? | +| `customer_id` | string | ? | +| `result_id` | string | ? | +| `revenue` | string | ? | +| `section` | string | ? | + +#### Send purchase event +```javascript +constructorio.tracker.sendPurchase({ + parameters +}); +``` + +| Parameter | Type | Description | +| --- | --- | --- | +| `customer_ids` | string | ? | +| `revenue` | string | ? | +| `section` | string | ? | + ## Development / npm commands ```bash From 6945e211cd891d0ea3a835ea1f5bc90d31603702 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Sun, 13 Oct 2019 11:35:23 -0600 Subject: [PATCH 53/56] Readme tweaks. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2018d558..c29a3d54 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm](https://img.shields.io/npm/v/@constructor-io/constructorio-client-javascript)](https://www.npmjs.com/package/@constructor-io/constructorio-client-javascript) ![David (path)](https://img.shields.io/david/Constructor-io/constructorio-client-javascript) -[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-javascript/blob/master/LICENSE) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-javascript/blob/master/LICENSE) A JavaScript client for [Constructor.io](http://constructor.io/). [Constructor.io](http://constructor.io/) provides search as a service that optimizes results using artificial intelligence (including natural language processing, re-ranking to optimize for conversions, and user personalization). @@ -26,7 +26,7 @@ var constructorio = new ConstructorIOClient({ ## 4. Retrieve Results -After instantiating an instance of the client, three modules will be exposed as properties to help retrieve data from Constructor.io: `search`, `autocomplete`, and `recommendations`. +After instantiating an instance of the client, four modules will be exposed as properties to help retrieve data from Constructor.io: `search`, `autocomplete`, `recommendations` and `tracking`. ### Search From 202b79cfa5ebbfa3fc543540a2f34f23a0e4ac42 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 15 Oct 2019 11:55:44 -0600 Subject: [PATCH 54/56] Pull out unrelated changes to keep branch focus targeted. --- spec/src/modules/autocomplete.js | 24 ++------- spec/src/modules/recommendations.js | 80 ++--------------------------- spec/src/modules/search.js | 43 ---------------- src/modules/autocomplete.js | 29 +++-------- src/modules/recommendations.js | 17 +++--- src/modules/search.js | 50 ++++-------------- 6 files changed, 28 insertions(+), 215 deletions(-) diff --git a/spec/src/modules/autocomplete.js b/spec/src/modules/autocomplete.js index c5e45bf0..4b386d22 100644 --- a/spec/src/modules/autocomplete.js +++ b/spec/src/modules/autocomplete.js @@ -21,7 +21,9 @@ describe('ConstructorIO - Autocomplete', () => { const clientVersion = 'cio-mocha'; let fetchSpy; - jsdom({ url: 'http://localhost' }); + jsdom({ + url: 'http://localhost', + }); beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -55,7 +57,6 @@ describe('ConstructorIO - Autocomplete', () => { 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'); done(); }); }); @@ -100,25 +101,6 @@ describe('ConstructorIO - Autocomplete', () => { }); }); - it('Should return a response with a valid query, and user id', (done) => { - const userId = 'user-id'; - const { autocomplete } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - }); - - autocomplete.getResults(query).then((res) => { - const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - - expect(res).to.have.property('request').to.be.an('object'); - expect(res).to.have.property('sections').to.be.an('object'); - expect(res).to.have.property('result_id').to.be.an('string'); - expect(requestedUrlParams).to.have.property('ui').to.equal(userId); - done(); - }); - }); - it('Should return a response with a valid query, and results', (done) => { const results = 2; const { autocomplete } = new ConstructorIO({ diff --git a/spec/src/modules/recommendations.js b/spec/src/modules/recommendations.js index b3e65ee1..b7d9b4ad 100644 --- a/spec/src/modules/recommendations.js +++ b/spec/src/modules/recommendations.js @@ -21,7 +21,9 @@ describe('ConstructorIO - Recommendations', () => { const clientVersion = 'cio-mocha'; let fetchSpy; - jsdom({ url: 'http://localhost' }); + jsdom({ + url: 'http://localhost', + }); beforeEach(() => { global.CLIENT_VERSION = 'cio-mocha'; @@ -100,25 +102,6 @@ describe('ConstructorIO - Recommendations', () => { }); }); - it('Should return a response with valid itemIds, and user id', (done) => { - const userId = 'user-id'; - const { recommendations } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - }); - - recommendations.getAlternativeItems(itemId).then((res) => { - const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - - expect(res).to.have.property('request').to.be.an('object'); - expect(res).to.have.property('response').to.be.an('object'); - expect(res).to.have.property('result_id').to.be.an('string'); - expect(requestedUrlParams).to.have.property('ui').to.equal(userId); - done(); - }); - }); - it('Should return a response with valid itemIds, and results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ @@ -244,25 +227,6 @@ describe('ConstructorIO - Recommendations', () => { }); }); - it('Should return a response with valid itemIds, and user id', (done) => { - const userId = 'user-id'; - const { recommendations } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - }); - - recommendations.getComplementaryItems(itemId).then((res) => { - const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - - expect(res).to.have.property('request').to.be.an('object'); - expect(res).to.have.property('response').to.be.an('object'); - expect(res).to.have.property('result_id').to.be.an('string'); - expect(requestedUrlParams).to.have.property('ui').to.equal(userId); - done(); - }); - }); - it('Should return a response with valid itemIds, and results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ @@ -371,25 +335,6 @@ describe('ConstructorIO - Recommendations', () => { }); }); - it('Should return a response with valid user id', (done) => { - const userId = 'user-id'; - const { recommendations } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - }); - - recommendations.getRecentlyViewedItems().then((res) => { - const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - - expect(res).to.have.property('request').to.be.an('object'); - expect(res).to.have.property('response').to.be.an('object'); - expect(res).to.have.property('result_id').to.be.an('string'); - expect(requestedUrlParams).to.have.property('ui').to.equal(userId); - done(); - }); - }); - it('Should return a response with valid results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ @@ -486,25 +431,6 @@ describe('ConstructorIO - Recommendations', () => { }); }); - it('Should return a response with valid user id', (done) => { - const userId = 'user-id'; - const { recommendations } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - }); - - recommendations.getUserFeaturedItems().then((res) => { - const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - - expect(res).to.have.property('request').to.be.an('object'); - expect(res).to.have.property('response').to.be.an('object'); - expect(res).to.have.property('result_id').to.be.an('string'); - expect(requestedUrlParams).to.have.property('ui').to.equal(userId); - done(); - }); - }); - it('Should return a response with valid results', (done) => { const results = 2; const { recommendations } = new ConstructorIO({ diff --git a/spec/src/modules/search.js b/spec/src/modules/search.js index ace75c19..99b71b3f 100644 --- a/spec/src/modules/search.js +++ b/spec/src/modules/search.js @@ -59,7 +59,6 @@ describe('ConstructorIO - Search', () => { expect(requestedUrlParams).to.have.property('s'); expect(requestedUrlParams).to.have.property('section').to.equal(section); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); - expect(requestedUrlParams).to.have.property('_dt'); done(); }); }); @@ -104,25 +103,6 @@ describe('ConstructorIO - Search', () => { }); }); - it('Should return a response with a valid query, section and user id', (done) => { - const userId = 'user-id'; - const { search } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - }); - - search.getSearchResults(query, { section }).then((res) => { - const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - - expect(res).to.have.property('request').to.be.an('object'); - expect(res).to.have.property('response').to.be.an('object'); - expect(res).to.have.property('result_id').to.be.an('string'); - expect(requestedUrlParams).to.have.property('ui').to.equal(userId); - done(); - }); - }); - it('Should return a response with a valid query, section, and page', (done) => { const page = 1; const { search } = new ConstructorIO({ @@ -358,7 +338,6 @@ describe('ConstructorIO - Search', () => { expect(requestedUrlParams).to.have.property('section').to.equal(section); expect(requestedUrlParams).to.have.property('filters'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); - expect(requestedUrlParams).to.have.property('_dt'); done(); }); }); @@ -409,28 +388,6 @@ describe('ConstructorIO - Search', () => { }); }); - it('Should return a response with a valid group_id, section and user id', (done) => { - const userId = 'user-id'; - const { search } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - }); - - search.getBrowseResults({ - section, - filters, - }).then((res) => { - const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); - - expect(res).to.have.property('request').to.be.an('object'); - expect(res).to.have.property('response').to.be.an('object'); - expect(res).to.have.property('result_id').to.be.an('string'); - expect(requestedUrlParams).to.have.property('ui').to.equal(userId); - done(); - }); - }); - it('Should return a response with a valid group_id, section, and page', (done) => { const page = 1; const { search } = new ConstructorIO({ diff --git a/src/modules/autocomplete.js b/src/modules/autocomplete.js index be52ce9a..b37110dc 100644 --- a/src/modules/autocomplete.js +++ b/src/modules/autocomplete.js @@ -1,8 +1,8 @@ -/* eslint-disable object-curly-newline, no-underscore-dangle */ +/* eslint-disable object-curly-newline */ const qs = require('qs'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const { throwHttpErrorFromResponse, cleanParams } = require('../utils'); +const { throwHttpErrorFromResponse } = require('../utils'); /** * Interface to autocomplete related API calls. @@ -16,17 +16,8 @@ const autocomplete = (options) => { // Create URL from supplied query (term) and parameters const createAutocompleteUrl = (query, parameters) => { - const { - apiKey, - version, - serviceUrl, - sessionId, - clientId, - userId, - segments, - testCells, - } = options; - let queryParams = { c: version }; + const { apiKey, version, serviceUrl, sessionId, clientId, segments, testCells } = options; + const queryParams = { c: version }; queryParams.key = apiKey; queryParams.i = clientId; @@ -49,11 +40,6 @@ const autocomplete = (options) => { queryParams.us = segments; } - // Pull user id from options - if (userId) { - queryParams.ui = userId; - } - if (parameters) { const { results, resultsPerSection, filters } = parameters; @@ -75,9 +61,6 @@ const autocomplete = (options) => { } } - queryParams._dt = Date.now(); - queryParams = cleanParams(queryParams); - const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/autocomplete/${encodeURIComponent(query)}?${queryString}`; @@ -138,4 +121,6 @@ const autocomplete = (options) => { }; }; -module.exports = autocomplete; +module.exports = { + autocomplete, +}; diff --git a/src/modules/recommendations.js b/src/modules/recommendations.js index acfdaa1f..3631aedb 100644 --- a/src/modules/recommendations.js +++ b/src/modules/recommendations.js @@ -2,7 +2,7 @@ const qs = require('qs'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const { throwHttpErrorFromResponse, cleanParams } = require('../utils'); +const { throwHttpErrorFromResponse } = require('../utils'); /** * Interface to recommendations related API calls. @@ -16,8 +16,8 @@ const recommendations = (options) => { // Create URL from supplied parameters const createRecommendationsUrl = (parameters, endpoint) => { - const { apiKey, version, serviceUrl, sessionId, userId, clientId, segments } = options; - let queryParams = { c: version }; + const { apiKey, version, serviceUrl, sessionId, clientId, segments } = options; + const queryParams = { c: version }; const validEndpoints = [ 'alternative_items', 'complementary_items', @@ -39,11 +39,6 @@ const recommendations = (options) => { queryParams.us = segments; } - // Pull user id from options - if (userId) { - queryParams.ui = userId; - } - if (parameters) { const { results, itemIds } = parameters; @@ -58,8 +53,6 @@ const recommendations = (options) => { } } - queryParams = cleanParams(queryParams); - const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/recommendations/${endpoint}/?${queryString}`; @@ -184,4 +177,6 @@ const recommendations = (options) => { }; }; -module.exports = recommendations; +module.exports = { + recommendations, +}; diff --git a/src/modules/search.js b/src/modules/search.js index 5031518f..3f3d93fb 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -1,8 +1,8 @@ -/* eslint-disable object-curly-newline, no-underscore-dangle */ +/* eslint-disable import/prefer-default-export, object-curly-newline */ const qs = require('qs'); const fetchPonyfill = require('fetch-ponyfill'); const Promise = require('es6-promise'); -const { throwHttpErrorFromResponse, cleanParams } = require('../utils'); +const { throwHttpErrorFromResponse } = require('../utils'); /** * Interface to search related API calls. @@ -16,17 +16,8 @@ const search = (options) => { // Create URL from supplied query (term) and parameters const createSearchUrl = (query, parameters) => { - const { - apiKey, - version, - serviceUrl, - sessionId, - clientId, - userId, - segments, - testCells, - } = options; - let queryParams = { c: version }; + const { apiKey, version, serviceUrl, sessionId, clientId, segments, testCells } = options; + const queryParams = { c: version }; queryParams.key = apiKey; queryParams.i = clientId; @@ -49,11 +40,6 @@ const search = (options) => { queryParams.us = segments; } - // Pull user id from options - if (userId) { - queryParams.ui = userId; - } - if (parameters) { const { page, resultsPerPage, filters, sortBy, sortOrder, section } = parameters; @@ -88,9 +74,6 @@ const search = (options) => { } } - queryParams._dt = Date.now(); - queryParams = cleanParams(queryParams); - const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/search/${encodeURIComponent(query)}?${queryString}`; @@ -98,17 +81,8 @@ const search = (options) => { // Create URL from supplied group ID and parameters const createBrowseUrl = (parameters) => { - const { - apiKey, - version, - serviceUrl, - sessionId, - clientId, - userId, - segments, - testCells, - } = options; - let queryParams = { c: version }; + const { apiKey, version, serviceUrl, sessionId, clientId, segments, testCells } = options; + const queryParams = { c: version }; queryParams.key = apiKey; queryParams.i = clientId; @@ -126,11 +100,6 @@ const search = (options) => { queryParams.us = segments; } - // Pull user id from options - if (userId) { - queryParams.ui = userId; - } - if (parameters) { const { page, resultsPerPage, filters, sortBy, sortOrder, section } = parameters; @@ -164,9 +133,6 @@ const search = (options) => { } } - queryParams._dt = Date.now(); - queryParams = cleanParams(queryParams); - const queryString = qs.stringify(queryParams, { indices: false }); return `${serviceUrl}/search/?${queryString}`; @@ -269,4 +235,6 @@ const search = (options) => { }; }; -module.exports = search; +module.exports = { + search, +}; From 6f7b0c8e248fce0b0ded603d79c6cbd1216c1b45 Mon Sep 17 00:00:00 2001 From: Steve Blaurock Date: Tue, 15 Oct 2019 12:12:33 -0600 Subject: [PATCH 55/56] Update tracking method names to more closely align with existing patterns. --- README.md | 12 ++--- spec/src/modules/tracker.js | 94 ++++++++++++++++++------------------- src/constructorio.js | 6 +-- src/modules/tracker.js | 24 +++++----- 4 files changed, 68 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index c29a3d54..b2b6a471 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ The tracker module can be used to send tracking events. Returns `true` when succ #### Send autocomplete select event ```javascript -constructorio.tracker.sendAutocompleteSelect('dogs', { +constructorio.tracker.trackAutocompleteSelect('dogs', { parameters }); ``` @@ -152,7 +152,7 @@ constructorio.tracker.sendAutocompleteSelect('dogs', { #### Send autocomplete search event ```javascript -constructorio.tracker.sendAutocompleteSearch('dogs', { +constructorio.tracker.trackSearchSubmit('dogs', { parameters }); ``` @@ -166,7 +166,7 @@ constructorio.tracker.sendAutocompleteSearch('dogs', { #### Send search results event ```javascript -constructorio.tracker.sendSearchResults('dogs', { +constructorio.tracker.trackSearchResultsLoaded('dogs', { parameters }); ``` @@ -178,7 +178,7 @@ constructorio.tracker.sendSearchResults('dogs', { #### Send search result click event ```javascript -constructorio.tracker.sendSearchResultClick('dogs', { +constructorio.tracker.trackSearchResultClick('dogs', { parameters }); ``` @@ -191,7 +191,7 @@ constructorio.tracker.sendSearchResultClick('dogs', { #### Send conversion event ```javascript -constructorio.tracker.sendConversion('dogs', { +constructorio.tracker.trackConversion('dogs', { parameters }); ``` @@ -206,7 +206,7 @@ constructorio.tracker.sendConversion('dogs', { #### Send purchase event ```javascript -constructorio.tracker.sendPurchase({ +constructorio.tracker.trackPurchase({ parameters }); ``` diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 2d0eb24d..c7c7b4e9 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -141,7 +141,7 @@ describe('ConstructorIO - Tracker', () => { }); }); - describe('sendAutocompleteSelect', () => { + describe('trackAutocompleteSelect', () => { const term = 'Where The Wild Things Are'; const parameters = { original_query: 'original-query', @@ -158,7 +158,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendAutocompleteSelect(term, parameters)).to.equal(true); + expect(tracker.trackAutocompleteSelect(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -185,7 +185,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendAutocompleteSelect(term, parameters)).to.equal(true); + expect(tracker.trackAutocompleteSelect(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -200,7 +200,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendAutocompleteSelect(term, parameters)).to.equal(true); + expect(tracker.trackAutocompleteSelect(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -210,29 +210,29 @@ describe('ConstructorIO - Tracker', () => { it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSelect([], parameters)).to.be.an('error'); + expect(tracker.trackAutocompleteSelect([], parameters)).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSelect(null, parameters)).to.be.an('error'); + expect(tracker.trackAutocompleteSelect(null, parameters)).to.be.an('error'); }); it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSelect(term, [])).to.be.an('error'); + expect(tracker.trackAutocompleteSelect(term, [])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSelect(term)).to.be.an('error'); + expect(tracker.trackAutocompleteSelect(term)).to.be.an('error'); }); }); - describe('sendAutocompleteSearch', () => { + describe('trackSearchSubmit', () => { const term = 'Where The Wild Things Are'; const parameters = { original_query: 'original-query', @@ -247,7 +247,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendAutocompleteSearch(term, parameters)).to.equal(true); + expect(tracker.trackSearchSubmit(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -273,7 +273,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendAutocompleteSearch(term, parameters)).to.equal(true); + expect(tracker.trackSearchSubmit(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -288,7 +288,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendAutocompleteSearch(term, parameters)).to.equal(true); + expect(tracker.trackSearchSubmit(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -298,29 +298,29 @@ describe('ConstructorIO - Tracker', () => { it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSearch([], parameters)).to.be.an('error'); + expect(tracker.trackSearchSubmit([], parameters)).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSearch(null, parameters)).to.be.an('error'); + expect(tracker.trackSearchSubmit(null, parameters)).to.be.an('error'); }); it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSearch(term, [])).to.be.an('error'); + expect(tracker.trackSearchSubmit(term, [])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendAutocompleteSearch(term)).to.be.an('error'); + expect(tracker.trackSearchSubmit(term)).to.be.an('error'); }); }); - describe('sendSearchResults', () => { + describe('trackSearchResultsLoaded', () => { const term = 'Cat in the Hat'; const parameters = { num_results: 1337, @@ -333,7 +333,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendSearchResults(term, parameters)).to.equal(true); + expect(tracker.trackSearchResultsLoaded(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -355,7 +355,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendSearchResults(term, parameters)).to.equal(true); + expect(tracker.trackSearchResultsLoaded(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -370,7 +370,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendSearchResults(term, parameters)).to.equal(true); + expect(tracker.trackSearchResultsLoaded(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -380,29 +380,29 @@ describe('ConstructorIO - Tracker', () => { it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResults([], parameters)).to.be.an('error'); + expect(tracker.trackSearchResultsLoaded([], parameters)).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResults(null, parameters)).to.be.an('error'); + expect(tracker.trackSearchResultsLoaded(null, parameters)).to.be.an('error'); }); it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResults(term, [])).to.be.an('error'); + expect(tracker.trackSearchResultsLoaded(term, [])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResults(term)).to.be.an('error'); + expect(tracker.trackSearchResultsLoaded(term)).to.be.an('error'); }); }); - describe('sendSearchResultClick', () => { + describe('trackSearchResultClick', () => { const term = 'Where The Wild Things Are'; const parameters = { name: 'name', @@ -416,7 +416,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendSearchResultClick(term, parameters)).to.equal(true); + expect(tracker.trackSearchResultClick(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -439,7 +439,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendSearchResultClick(term, parameters)).to.equal(true); + expect(tracker.trackSearchResultClick(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -454,7 +454,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendSearchResultClick(term, parameters)).to.equal(true); + expect(tracker.trackSearchResultClick(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -464,29 +464,29 @@ describe('ConstructorIO - Tracker', () => { it('Should throw an error when invalid term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResultClick([], parameters)).to.be.an('error'); + expect(tracker.trackSearchResultClick([], parameters)).to.be.an('error'); }); it('Should throw an error when no term is provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResultClick(null, parameters)).to.be.an('error'); + expect(tracker.trackSearchResultClick(null, parameters)).to.be.an('error'); }); it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResultClick(term, [])).to.be.an('error'); + expect(tracker.trackSearchResultClick(term, [])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResultClick(term)).to.be.an('error'); + expect(tracker.trackSearchResultClick(term)).to.be.an('error'); }); }); - describe('sendConversion', () => { + describe('trackConversion', () => { const term = 'Where The Wild Things Are'; const parameters = { name: 'name', @@ -502,7 +502,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendConversion(term, parameters)).to.equal(true); + expect(tracker.trackConversion(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -525,7 +525,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendConversion(term, parameters)).to.equal(true); + expect(tracker.trackConversion(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -540,7 +540,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendConversion(term, parameters)).to.equal(true); + expect(tracker.trackConversion(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -555,7 +555,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendConversion(term, parameters)).to.equal(true); + expect(tracker.trackConversion(term, parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -565,23 +565,23 @@ describe('ConstructorIO - Tracker', () => { it('Should respond with a valid response when no term is provided, but parameters are', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendConversion(null, parameters)).to.equal(true); + expect(tracker.trackConversion(null, parameters)).to.equal(true); }); it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResultClick(term, [])).to.be.an('error'); + expect(tracker.trackSearchResultClick(term, [])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendSearchResultClick(term)).to.be.an('error'); + expect(tracker.trackSearchResultClick(term)).to.be.an('error'); }); }); - describe('sendPurchase', () => { + describe('trackPurchase', () => { const parameters = { customer_ids: 'customer-id', revenue: 123, @@ -594,7 +594,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendPurchase(parameters)).to.equal(true); + expect(tracker.trackPurchase(parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -615,7 +615,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendPurchase(parameters)).to.equal(true); + expect(tracker.trackPurchase(parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -630,7 +630,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendPurchase(parameters)).to.equal(true); + expect(tracker.trackPurchase(parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -645,7 +645,7 @@ describe('ConstructorIO - Tracker', () => { fetch: fetchSpy, }); - expect(tracker.sendPurchase(parameters)).to.equal(true); + expect(tracker.trackPurchase(parameters)).to.equal(true); const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); @@ -655,13 +655,13 @@ describe('ConstructorIO - Tracker', () => { it('Should throw an error when invalid parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendPurchase([])).to.be.an('error'); + expect(tracker.trackPurchase([])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { const { tracker } = new ConstructorIO({ apiKey: testApiKey }); - expect(tracker.sendPurchase()).to.be.an('error'); + expect(tracker.trackPurchase()).to.be.an('error'); }); }); }); diff --git a/src/constructorio.js b/src/constructorio.js index 7a213ff6..2d66657a 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -2,9 +2,9 @@ const ConstructorioID = require('@constructor-io/constructorio-id'); // Modules -const search = require('./modules/search'); -const autocomplete = require('./modules/autocomplete'); -const recommendations = require('./modules/recommendations'); +const { search } = require('./modules/search'); +const { autocomplete } = require('./modules/autocomplete'); +const { recommendations } = require('./modules/recommendations'); const tracker = require('./modules/tracker'); const { version } = require('../package.json'); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index ce992104..ecac3e6f 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -84,7 +84,7 @@ const tracker = (options) => { /** * Send autocomplete select event to API * - * @function sendAutocompleteSelect + * @function trackAutocompleteSelect * @param {string} term - Term of selected autocomplete item * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.original_query - The current autocomplete search query @@ -95,7 +95,7 @@ const tracker = (options) => { * @param {string} [parameters.display_name] - Display name of group of selected item * @returns {(true|Error)} */ - sendAutocompleteSelect: (term, parameters) => { + trackAutocompleteSelect: (term, parameters) => { // Ensure term is provided (required) if (term && typeof term === 'string') { // Ensure parameters are provided (required) @@ -154,7 +154,7 @@ const tracker = (options) => { /** * Send autocomplete search event to API * - * @function sendAutocompleteSearch + * @function trackSearchSubmit * @param {string} term - Term of submitted autocomplete event * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.original_query - The current autocomplete search query @@ -163,7 +163,7 @@ const tracker = (options) => { * @param {string} [parameters.display_name] - Display name of group of selected item * @returns {(true|Error)} */ - sendAutocompleteSearch: (term, parameters) => { + trackSearchSubmit: (term, parameters) => { // Ensure term is provided (required) if (term && typeof term === 'string') { // Ensure parameters are provided (required) @@ -206,14 +206,14 @@ const tracker = (options) => { /** * Send search results event to API * - * @function sendSearchResults + * @function trackSearchResultsLoaded * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request * @param {number} parameters.num_results - Number of search results in total * @param {array} [parameters.customer_ids] - List of customer item id's returned from search * @returns {(true|Error)} */ - sendSearchResults: (term, parameters) => { + trackSearchResultsLoaded: (term, parameters) => { // Ensure term is provided (required) if (term && typeof term === 'string') { // Ensure parameters are provided (required) @@ -249,7 +249,7 @@ const tracker = (options) => { /** * Send click through event to API * - * @function sendSearchResultClick + * @function trackSearchResultClick * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.name - Identifier @@ -257,7 +257,7 @@ const tracker = (options) => { * @param {string} parameters.result_id - Result id * @returns {(true|Error)} */ - sendSearchResultClick: (term, parameters) => { + trackSearchResultClick: (term, parameters) => { // Ensure term is provided (required) if (term && typeof term === 'string') { // Ensure parameters are provided (required) @@ -297,7 +297,7 @@ const tracker = (options) => { /** * Send conversion event to API * - * @function sendConversion + * @function trackConversion * @param {string} term - Search results query term * @param {object} parameters - Additional parameters to be sent with request * @param {string} parameters.name - Identifier @@ -307,7 +307,7 @@ const tracker = (options) => { * @param {string} parameters.section - Autocomplete section * @returns {(true|Error)} */ - sendConversion: (term, parameters) => { + trackConversion: (term, parameters) => { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const searchTerm = utils.ourEncodeURIComponent(term) || 'TERM_UNKNOWN'; @@ -351,14 +351,14 @@ const tracker = (options) => { /** * Send purchase event to API * - * @function sendPurchase + * @function trackPurchase * @param {object} parameters - Additional parameters to be sent with request * @param {array} parameters.customer_ids - List of customer item id's * @param {string} parameters.revenue - Revenue * @param {string} parameters.section - Autocomplete section * @returns {(true|Error)} */ - sendPurchase: (parameters) => { + trackPurchase: (parameters) => { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { const url = `${options.serviceUrl}/autocomplete/TERM_UNKNOWN/purchase?`; From 897a77f38a6d22b6e8093a9af95240c64188acf3 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Tue, 15 Oct 2019 18:01:57 -0400 Subject: [PATCH 56/56] Updated customer_ids test in purchase --- spec/src/modules/tracker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index c7c7b4e9..a1fe8239 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -583,7 +583,7 @@ describe('ConstructorIO - Tracker', () => { describe('trackPurchase', () => { const parameters = { - customer_ids: 'customer-id', + customer_ids: ['customer-id1', 'customer-id1', 'customer-id2'], revenue: 123, section: 'Products', }; @@ -604,7 +604,7 @@ describe('ConstructorIO - Tracker', () => { expect(requestedUrlParams).to.have.property('s'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); expect(requestedUrlParams).to.have.property('_dt'); - expect(requestedUrlParams).to.have.property('customer_ids').to.equal(parameters.customer_ids); + expect(requestedUrlParams).to.have.property('customer_ids').to.deep.equal(parameters.customer_ids); expect(requestedUrlParams).to.have.property('revenue').to.equal(parameters.revenue.toString()); expect(requestedUrlParams).to.have.property('section').to.equal(parameters.section); });