modules/tracker.js

/* eslint-disable max-len */
/* eslint-disable camelcase, no-underscore-dangle, no-unneeded-ternary, brace-style */
const qs = require('qs');
const { AbortController } = require('node-abort-controller');
const EventEmitter = require('events');
const helpers = require('../utils/helpers');

function applyParams(parameters, userParameters, options) {
  const {
    apiKey,
    version,
  } = options;
  const {
    sessionId,
    clientId,
    userId,
    segments,
    testCells,
    originReferrer,
    dateTime,
  } = userParameters || {};
  let aggregateParams = Object.assign(parameters);

  // Validate session ID is provided
  if (!sessionId || typeof sessionId !== 'number') {
    throw new Error('sessionId is a required user parameter of type number');
  }

  // Validate client ID is provided
  if (!clientId || typeof clientId !== 'string') {
    throw new Error('clientId is a required user parameter of type string');
  }

  if (version) {
    aggregateParams.c = version;
  }

  if (clientId) {
    aggregateParams.i = clientId;
  }

  if (sessionId) {
    aggregateParams.s = sessionId;
  }

  if (userId) {
    aggregateParams.ui = String(userId);
  }

  if (segments && segments.length) {
    aggregateParams.us = segments;
  }

  if (apiKey) {
    aggregateParams.key = apiKey;
  }

  if (testCells) {
    Object.keys(testCells).forEach((testCellKey) => {
      aggregateParams[`ef-${testCellKey}`] = testCells[testCellKey];
    });
  }

  if (originReferrer) {
    aggregateParams.origin_referrer = originReferrer;
  }

  aggregateParams._dt = dateTime || Date.now();
  aggregateParams.beacon = true;
  aggregateParams = helpers.cleanParams(aggregateParams);

  return aggregateParams;
}

// Append common parameters to supplied parameters object and return as string
function applyParamsAsString(parameters, userParameters, options) {
  return qs.stringify(applyParams(parameters, userParameters, options), { indices: false });
}

// Send request to server
function send(url, userParameters, networkParameters, method = 'GET', body = {}) { // eslint-disable-line max-params
  let request;
  const { fetch } = this.options;
  const controller = new AbortController();
  const { signal } = controller;
  const headers = {};

  // PII Detection
  if (helpers.requestContainsPii(url)) return;

  Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));

  // Append security token as 'x-cnstrc-token' if available
  if (this.options.securityToken && typeof this.options.securityToken === 'string') {
    headers['x-cnstrc-token'] = this.options.securityToken;
  }

  if (userParameters) {
    // Append user IP as 'X-Forwarded-For' if available
    if (userParameters.userIp && typeof userParameters.userIp === 'string') {
      headers['X-Forwarded-For'] = userParameters.userIp;
    }

    // Append user agent as 'User-Agent' if available
    if (userParameters.userAgent && typeof userParameters.userAgent === 'string') {
      headers['User-Agent'] = userParameters.userAgent;
    }

    // Append language as 'Accept-Language' if available
    if (userParameters.acceptLanguage && typeof userParameters.acceptLanguage === 'string') {
      headers['Accept-Language'] = userParameters.acceptLanguage;
    }

    // Append referrer as 'Referer' if available
    if (userParameters.referer && typeof userParameters.referer === 'string') {
      headers.Referer = userParameters.referer;
    }
  }

  // Handle network timeout if specified
  helpers.applyNetworkTimeout(this.options, networkParameters, controller);

  if (method === 'GET') {
    request = fetch(url, { headers, signal });
  }

  if (method === 'POST') {
    request = fetch(url, {
      method,
      body: JSON.stringify(body),
      mode: 'cors',
      headers: {
        ...headers,
        'Content-Type': 'text/plain',
      },
      signal,
    });
  }

  if (request) {
    const instance = this;

    request.then((response) => {
      // Request was successful, and returned a 2XX status code
      if (response.ok) {
        instance.eventemitter.emit('success', {
          url,
          method,
          message: 'ok',
        });
      }

      // Request was successful, but returned a non-2XX status code
      else {
        response.json().then((json) => {
          instance.eventemitter.emit('error', {
            url,
            method,
            message: json && json.message,
          });
        }).catch((error) => {
          instance.eventemitter.emit('error', {
            url,
            method,
            message: error.type,
          });
        });
      }
    }).catch((error) => {
      instance.eventemitter.emit('error', {
        url,
        method,
        message: error.toString(),
      });
    });
  }
}

/**
 * Interface to tracking related API calls
 *
 * @module tracker
 * @inner
 * @returns {object}
 */
class Tracker {
  constructor(options) {
    this.options = options || {};
    this.eventemitter = new EventEmitter();
  }

  /**
   * Send session start event to API
   *
   * @function trackSessionStart
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @example
   * constructorio.tracker.trackSessionStart({
   *  sessionId: 1,
   *  clientId: '6c73138f-c27b-49f0-872d-63b00ed0e395',
   *  testCells: { testName: 'cellName' },
   * });
   */
  trackSessionStart(userParameters, networkParameters = {}) {
    const url = `${this.options.serviceUrl}/behavior?`;
    const queryParams = { action: 'session_start' };
    const requestUrl = `${url}${applyParamsAsString(queryParams, userParameters, this.options)}`;

    send.call(
      this,
      requestUrl,
      userParameters,
      networkParameters,
    );

    return true;
  }

  /**
   * Send input focus event to API
   *
   * @function trackInputFocus
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User focused on search input element
   * @example
   * constructorio.tracker.trackInputFocus({
   *     sessionId: 1,
   *     clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *     testCells: { testName: 'cellName' },
   * });
   */
  trackInputFocus(userParameters, networkParameters = {}) {
    const url = `${this.options.serviceUrl}/behavior?`;
    const queryParams = { action: 'focus' };
    const requestUrl = `${url}${applyParamsAsString(queryParams, userParameters, this.options)}`;

    send.call(
      this,
      requestUrl,
      userParameters,
      networkParameters,
    );

    return true;
  }

  /**
   * Send item detail load event to API
   *
   * @function trackItemDetailLoad
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.itemName - Product item name
   * @param {string} parameters.itemId - Product item unique identifier
   * @param {string} parameters.url - Current page URL
   * @param {string} [parameters.variationId] - Product item variation unique identifier
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {string} [parameters.section] - Index section
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User loaded an item detail page
   * @example
   * constructorio.tracker.trackItemDetailLoad(
   *     {
   *         itemName: 'Red T-Shirt',
   *         itemId: 'KMH876',
   *         url: 'https://constructor.io/product/KMH876',
   *     },
   * );
   */
  trackItemDetailLoad(parameters, userParameters, networkParameters = {}) {
    // Ensure parameters are provided (required)
    if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/item_detail_load?`;
      const bodyParams = {};
      const {
        item_name,
        name,
        item_id,
        customer_id,
        customerId = customer_id,
        variation_id,
        itemName = item_name || name,
        itemId = item_id || customerId,
        variationId = variation_id,
        url,
        analyticsTags,
        section,
      } = parameters;

      // Ensure support for both item_name and name as parameters
      if (itemName) {
        bodyParams.item_name = itemName;
      }

      // Ensure support for both item_id and customer_id as parameters
      if (itemId) {
        bodyParams.item_id = itemId;
      }

      if (variationId) {
        bodyParams.variation_id = variationId;
      }

      if (url) {
        bodyParams.url = url;
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      if (section) {
        bodyParams.section = section;
      }

      const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('parameters are required of type object');
  }

  /**
   * Send autocomplete select event to API
   *
   * @function trackAutocompleteSelect
   * @param {string} term - Term of selected autocomplete item (Search Suggestion or Product name)
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.originalQuery - The current autocomplete search query
   * @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 the group to search within. Only required if searching within a group, i.e. "Pumpkin in Canned Goods"
   * @param {string} [parameters.displayName] - Display name of the group to search within. Only required if searching within a group, i.e. "Pumpkin in Canned Goods"
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User selected (clicked, or navigated to via keyboard) a result that appeared
   * within autocomplete (Search Suggestions, Products, or a custom section eg. Brands, Categories)
   * @example
   * constructorio.tracker.trackAutocompleteSelect(
   *     'T-Shirt',
   *     {
   *         originalQuery: 'Shirt',
   *         section: 'Products',
   *         tr: 'click',
   *         groupId: '88JU230',
   *         displayName: 'apparel',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackAutocompleteSelect(term, parameters, userParameters, networkParameters = {}) {
    // 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 = `${this.options.serviceUrl}/autocomplete/${helpers.encodeURIComponentRFC3986(helpers.normalizeSpaces(term))}/select?`;
        const queryParams = {};
        const {
          original_query,
          originalQuery = original_query,
          section,
          original_section,
          originalSection = original_section,
          tr,
          group_id,
          groupId = group_id,
          display_name,
          displayName = display_name,
        } = parameters;

        if (originalQuery) {
          queryParams.original_query = originalQuery;
        }

        if (tr) {
          queryParams.tr = tr;
        }

        if (originalSection || section) {
          queryParams.section = originalSection || section;
        }

        if (groupId) {
          queryParams.group = {
            group_id: groupId,
            display_name: displayName,
          };
        }

        const requestUrl = `${url}${applyParamsAsString(queryParams, userParameters, this.options)}`;

        send.call(
          this,
          requestUrl,
          userParameters,
          networkParameters,
        );

        return true;
      }

      return new Error('parameters are required of type object');
    }

    return new Error('term is a required parameter of type string');
  }

  /**
   * Send autocomplete search event to API
   *
   * @function trackSearchSubmit
   * @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.groupId] - Group identifier of the group to search within. Only required if searching within a group, i.e. "Pumpkin in Canned Goods"
   * @param {string} [parameters.displayName] - Display name of the group to search within. Only required if searching within a group, i.e. "Pumpkin in Canned Goods"
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User submitted a search (pressing enter within input element, or clicking submit element)
   * @example
   * constructorio.tracker.trackSearchSubmit(
   *     'T-Shirt',
   *     {
   *         originalQuery: 'Shirt',
   *         groupId: '88JU230',
   *         displayName: 'apparel',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackSearchSubmit(term, parameters, userParameters, networkParameters = {}) {
    // 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 = `${this.options.serviceUrl}/autocomplete/${helpers.encodeURIComponentRFC3986(helpers.normalizeSpaces(term))}/search?`;
        const queryParams = {};
        const {
          original_query,
          originalQuery = original_query,
          group_id,
          groupId = group_id,
          display_name,
          displayName = display_name,
        } = parameters;

        if (originalQuery) {
          queryParams.original_query = originalQuery;
        }

        if (groupId) {
          queryParams.group = {
            group_id: groupId,
            display_name: displayName,
          };
        }

        const requestUrl = `${url}${applyParamsAsString(queryParams, userParameters, this.options)}`;

        send.call(
          this,
          requestUrl,
          userParameters,
          networkParameters,
        );

        return true;
      }

      return new Error('parameters are required of type object');
    }

    return new Error('term is a required parameter of type string');
  }

  /**
   * Send search results event to API
   *
   * @function trackSearchResultsLoaded
   * @param {string} term - Search results query term
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {number} parameters.numResults - Total number of results
   * @param {string[]} parameters.itemIds - List of product item unique identifiers in search results listing
   * @param {string} [parameters.section] - Index section
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User loaded a search product listing page
   * @example
   * constructorio.tracker.trackSearchResultsLoaded(
   *     'T-Shirt',
   *     {
   *         numResults: 167,
   *         itemIds: ['KMH876', 'KMH140', 'KMH437'],
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackSearchResultsLoaded(term, parameters, userParameters, networkParameters = {}) {
    // 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 = `${this.options.serviceUrl}/behavior?`;
        const queryParams = { action: 'search-results', term };
        const {
          num_results,
          numResults = num_results,
          customer_ids,
          customerIds = customer_ids,
          item_ids,
          itemIds = item_ids,
          section,
        } = parameters;
        let customerIDs;

        if (!helpers.isNil(numResults)) {
          queryParams.num_results = numResults;
        }

        // Ensure support for both item_ids and customer_ids as parameters
        if (itemIds && Array.isArray(itemIds)) {
          customerIDs = itemIds;
        } else if (customerIds && Array.isArray(customerIds)) {
          customerIDs = customerIds;
        }

        if (customerIDs && Array.isArray(customerIDs) && customerIDs.length) {
          queryParams.customer_ids = customerIDs.slice(0, 100).join(',');
        }

        if (section) {
          queryParams.section = section;
        }

        const requestUrl = `${url}${applyParamsAsString(queryParams, userParameters, this.options)}`;

        send.call(
          this,
          requestUrl,
          userParameters,
          networkParameters,
        );

        return true;
      }

      return new Error('parameters are required of type object');
    }

    return new Error('term is a required parameter of type string');
  }

  /**
   * Send click through event to API
   *
   * @function trackSearchResultClick
   * @param {string} term - Search results query term
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.itemName - Product item name
   * @param {string} parameters.itemId - Product item unique identifier
   * @param {string} [parameters.variationId] - Product item variation unique identifier
   * @param {string} [parameters.resultId] - Search result identifier (returned in response from Constructor)
   * @param {string} [parameters.itemIsConvertible] - Whether or not an item is available for a conversion
   * @param {string} [parameters.section] - The section name for the item Ex. "Products"
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User clicked a result that appeared within a search product listing page
   * @example
   * constructorio.tracker.trackSearchResultClick(
   *     'T-Shirt',
   *     {
   *         itemName: 'Red T-Shirt',
   *         itemId: 'KMH876',
   *         resultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackSearchResultClick(term, parameters, userParameters, networkParameters = {}) {
    // 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 = `${this.options.serviceUrl}/autocomplete/${helpers.encodeURIComponentRFC3986(helpers.normalizeSpaces(term))}/click_through?`;
        const queryParams = {};
        const {
          item_name,
          name,
          itemName = item_name || name,
          item_id,
          itemId = item_id,
          customer_id,
          customerId = customer_id || itemId,
          variation_id,
          variationId = variation_id,
          result_id,
          resultId = result_id,
          item_is_convertible,
          itemIsConvertible = item_is_convertible,
          section,
        } = parameters;

        // Ensure support for both item_name and name as parameters
        if (itemName) {
          queryParams.name = itemName;
        }

        // Ensure support for both item_id and customer_id as parameters
        if (customerId) {
          queryParams.customer_id = customerId;
        }

        if (variationId) {
          queryParams.variation_id = variationId;
        }

        if (resultId) {
          queryParams.result_id = resultId;
        }

        if (typeof itemIsConvertible === 'boolean') {
          queryParams.item_is_convertible = itemIsConvertible;
        }

        if (section) {
          queryParams.section = section;
        }

        const requestUrl = `${url}${applyParamsAsString(queryParams, userParameters, this.options)}`;

        send.call(
          this,
          requestUrl,
          userParameters,
          networkParameters,
        );

        return true;
      }

      return new Error('parameters are required of type object');
    }

    return new Error('term is a required parameter of type string');
  }

  /**
   * Send conversion event to API
   *
   * @function trackConversion
   * @param {string} [term] - Search results query term that led to conversion event
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.itemId - Product item unique identifier
   * @param {number} [parameters.revenue] - Sale price if available, otherwise the regular (retail) price of item
   * @param {string} [parameters.itemName] - Product item name
   * @param {string} [parameters.variationId] - Product item variation unique identifier
   * @param {string} [parameters.type='add_to_cart'] - Conversion type
   * @param {boolean} [parameters.isCustomType] - Specify if type is custom conversion type
   * @param {string} [parameters.displayName] - Display name for the custom conversion type
   * @param {string} [parameters.section] - Index section
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User performed an action indicating interest in an item (add to cart, add to wishlist, etc.)
   * @see https://docs.constructor.io/rest_api/behavioral_logging/conversions
   * @example
   * constructorio.tracker.trackConversion(
   *     'T-Shirt',
   *     {
   *         itemId: 'KMH876',
   *         revenue: 12.00,
   *         itemName: 'Red T-Shirt',
   *         variationId: 'KMH879-7632',
   *         type: 'like',
   *         section: 'Products',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackConversion(term, parameters, userParameters, networkParameters = {}) {
    // Ensure parameters are provided (required)
    if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
      const searchTerm = term || 'TERM_UNKNOWN';
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/conversion?`;
      const queryParams = {};
      const bodyParams = {};
      const {
        name,
        item_name,
        itemName = item_name || name,
        item_id,
        customer_id,
        itemId = item_id || customer_id,
        variation_id,
        variationId = variation_id,
        revenue,
        section = 'Products',
        display_name,
        displayName = display_name,
        type,
        is_custom_type,
        isCustomType = is_custom_type,
        analyticsTags,
      } = parameters;

      // Ensure support for both item_id and customer_id as parameters
      if (itemId) {
        bodyParams.item_id = itemId;
      }

      // Ensure support for both item_name and name as parameters
      if (itemName) {
        bodyParams.item_name = itemName;
      }

      if (variationId) {
        bodyParams.variation_id = variationId;
      }

      if (revenue) {
        bodyParams.revenue = revenue.toString();
      }

      if (section) {
        queryParams.section = section;
        bodyParams.section = section;
      }

      if (searchTerm) {
        bodyParams.search_term = searchTerm;
      }

      if (type) {
        bodyParams.type = type;
      }

      if (isCustomType) {
        bodyParams.is_custom_type = isCustomType;
      }

      if (displayName) {
        bodyParams.display_name = displayName;
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('parameters are required of type object');
  }

  /**
   * Send purchase event to API
   *
   * @function trackPurchase
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {object[]} parameters.items - List of product item objects
   * @param {number} parameters.revenue - The subtotal (excluding taxes, shipping, etc.) of the entire order
   * @param {string} [parameters.orderId] - Unique order identifier
   * @param {string} [parameters.section] - Index section
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User completed an order (usually fired on order confirmation page)
   * @example
   * constructorio.tracker.trackPurchase(
   *     {
   *         items: [{ itemId: 'KMH876' }, { itemId: 'KMH140' }],
   *         revenue: 12.00,
   *         orderId: 'OUNXBG2HMA',
   *         section: 'Products',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackPurchase(parameters, userParameters, networkParameters = {}) {
    // Ensure parameters are provided (required)
    if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/purchase?`;
      const queryParams = {};
      const bodyParams = {};
      const {
        items,
        revenue,
        order_id,
        orderId = order_id,
        section,
        analyticsTags,
      } = parameters;

      if (orderId) {
        bodyParams.order_id = orderId;
      }

      if (items && Array.isArray(items)) {
        bodyParams.items = items.slice(0, 100).map((item) => helpers.toSnakeCaseKeys(item, false));
      }

      if (revenue) {
        bodyParams.revenue = revenue;
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      if (section) {
        queryParams.section = section;
      } else {
        queryParams.section = 'Products';
      }

      const requestUrl = `${requestPath}${applyParamsAsString(queryParams, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('parameters are required of type object');
  }

  /**
   * Send recommendation view event to API
   *
   * @function trackRecommendationView
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.url - Current page URL
   * @param {string} parameters.podId - Pod identifier
   * @param {number} parameters.numResultsViewed - Number of results viewed
   * @param {object[]} [parameters.items] - List of Product Item objects
   * @param {number} [parameters.resultCount] - Total number of results
   * @param {number} [parameters.resultPage] - Page number of results
   * @param {string} [parameters.resultId] - Recommendation result identifier (returned in response from Constructor)
   * @param {string} [parameters.section="Products"] - Results section
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User clicked a result that appeared within a search product listing page
   * @example
   * constructorio.tracker.trackRecommendationView(
   *     {
   *         items: [{ itemId: 'KMH876' }, { itemId: 'KMH140' }],
   *         resultCount: 22,
   *         resultPage: 2,
   *         resultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2',
   *         url: 'https://demo.constructor.io/sandbox/farmstand',
   *         podId: '019927c2-f955-4020',
   *         numResultsViewed: 3,
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackRecommendationView(parameters, userParameters, networkParameters = {}) {
    // Ensure parameters are provided (required)
    if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/recommendation_result_view?`;
      const bodyParams = {};
      const {
        result_count,
        resultCount = result_count,
        result_page,
        resultPage = result_page,
        result_id,
        resultId = result_id,
        section,
        url,
        pod_id,
        podId = pod_id,
        num_results_viewed,
        numResultsViewed = num_results_viewed,
        items,
        analyticsTags,
      } = parameters;

      if (!helpers.isNil(resultCount)) {
        bodyParams.result_count = resultCount;
      }

      if (!helpers.isNil(resultPage)) {
        bodyParams.result_page = resultPage;
      }

      if (resultId) {
        bodyParams.result_id = resultId;
      }

      if (section) {
        bodyParams.section = section;
      } else {
        bodyParams.section = 'Products';
      }

      if (url) {
        bodyParams.url = url;
      }

      if (podId) {
        bodyParams.pod_id = podId;
      }

      if (!helpers.isNil(numResultsViewed)) {
        bodyParams.num_results_viewed = numResultsViewed;
      }

      if (items && Array.isArray(items)) {
        bodyParams.items = items.slice(0, 100).map((item) => helpers.toSnakeCaseKeys(item, false));
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('parameters are required of type object');
  }

  /**
   * Send recommendation click event to API
   *
   * @function trackRecommendationClick
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.podId - Pod identifier
   * @param {string} parameters.strategyId - Strategy identifier
   * @param {string} parameters.itemId - Product item unique identifier
   * @param {string} parameters.itemName - Product item name
   * @param {string} [parameters.variationId] - Product item variation unique identifier
   * @param {string} [parameters.section="Products"] - Index section
   * @param {string} [parameters.resultId] - Recommendation result identifier (returned in response from Constructor)
   * @param {number} [parameters.resultCount] - Total number of results
   * @param {number} [parameters.resultPage] - Page number of results
   * @param {number} [parameters.resultPositionOnPage] - Position of result on page
   * @param {number} [parameters.numResultsPerPage] - Number of results on page
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User clicked an item that appeared within a list of recommended results
   * @example
   * constructorio.tracker.trackRecommendationClick(
   *     {
   *         variationId: 'KMH879-7632',
   *         resultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2',
   *         resultCount: 22,
   *         resultPage: 2,
   *         resultPositionOnPage: 2,
   *         numResultsPerPage: 12,
   *         podId: '019927c2-f955-4020',
   *         strategyId: 'complimentary',
   *         itemId: 'KMH876',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackRecommendationClick(parameters, userParameters, networkParameters = {}) {
    // Ensure parameters are provided (required)
    if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/recommendation_result_click?`;
      const bodyParams = {};
      const {
        variation_id,
        variationId = variation_id,
        section,
        result_id,
        resultId = result_id,
        result_count,
        resultCount = result_count,
        result_page,
        resultPage = result_page,
        result_position_on_page,
        resultPositionOnPage = result_position_on_page,
        num_results_per_page,
        numResultsPerPage = num_results_per_page,
        pod_id,
        podId = pod_id,
        strategy_id,
        strategyId = strategy_id,
        item_id,
        itemId = item_id,
        item_name,
        itemName = item_name,
        analyticsTags,
      } = parameters;

      if (variationId) {
        bodyParams.variation_id = variationId;
      }

      if (section) {
        bodyParams.section = section;
      } else {
        bodyParams.section = 'Products';
      }

      if (resultId) {
        bodyParams.result_id = resultId;
      }

      if (!helpers.isNil(resultCount)) {
        bodyParams.result_count = resultCount;
      }

      if (!helpers.isNil(resultPage)) {
        bodyParams.result_page = resultPage;
      }

      if (!helpers.isNil(resultPositionOnPage)) {
        bodyParams.result_position_on_page = resultPositionOnPage;
      }

      if (!helpers.isNil(numResultsPerPage)) {
        bodyParams.num_results_per_page = numResultsPerPage;
      }

      if (podId) {
        bodyParams.pod_id = podId;
      }

      if (strategyId) {
        bodyParams.strategy_id = strategyId;
      }

      if (itemId) {
        bodyParams.item_id = itemId;
      }

      if (itemName) {
        bodyParams.item_name = itemName;
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('parameters are required of type object');
  }

  /**
   * Send browse results loaded event to API
   *
   * @function trackBrowseResultsLoaded
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.url - Current page URL
   * @param {string} parameters.filterName - Filter name
   * @param {string} parameters.filterValue - Filter value
   * @param {object[]} parameters.items - List of product item objects
   * @param {string} [parameters.section="Products"] - Index section
   * @param {number} [parameters.resultCount] - Total number of results
   * @param {number} [parameters.resultPage] - Page number of results
   * @param {string} [parameters.resultId] - Browse result identifier (returned in response from Constructor)
   * @param {object} [parameters.selectedFilters] - Selected filters
   * @param {string} [parameters.sortOrder] - Sort order ('ascending' or 'descending')
   * @param {string} [parameters.sortBy] - Sorting method
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User loaded a browse product listing page
   * @example
   * constructorio.tracker.trackBrowseResultsLoaded(
   *     {
   *         resultCount: 22,
   *         resultPage: 2,
   *         resultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2',
   *         selectedFilters: { brand: ['foo'], color: ['black'] },
   *         sortOrder: 'ascending',
   *         sortBy: 'price',
   *         items: [{ itemId: 'KMH876' }, { itemId: 'KMH140' }],
   *         url: 'https://demo.constructor.io/sandbox/farmstand',
   *         filterName: 'brand',
   *         filterValue: 'XYZ',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackBrowseResultsLoaded(parameters, userParameters, networkParameters = {}) {
    // Ensure parameters are provided (required)
    if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/browse_result_load?`;
      const bodyParams = {};
      const {
        section,
        result_count,
        resultCount = result_count,
        result_page,
        resultPage = result_page,
        result_id,
        resultId = result_id,
        selected_filters,
        selectedFilters = selected_filters,
        url,
        sort_order,
        sortOrder = sort_order,
        sort_by,
        sortBy = sort_by,
        filter_name,
        filterName = filter_name,
        filter_value,
        filterValue = filter_value,
        items,
        analyticsTags,
      } = parameters;

      if (section) {
        bodyParams.section = section;
      } else {
        bodyParams.section = 'Products';
      }

      if (!helpers.isNil(resultCount)) {
        bodyParams.result_count = resultCount;
      }

      if (!helpers.isNil(resultPage)) {
        bodyParams.result_page = resultPage;
      }

      if (resultId) {
        bodyParams.result_id = resultId;
      }

      if (selectedFilters) {
        bodyParams.selected_filters = selectedFilters;
      }

      if (url) {
        bodyParams.url = url;
      }

      if (sortOrder) {
        bodyParams.sort_order = sortOrder;
      }

      if (sortBy) {
        bodyParams.sort_by = sortBy;
      }

      if (filterName) {
        bodyParams.filter_name = filterName;
      }

      if (filterValue) {
        bodyParams.filter_value = filterValue;
      }

      if (items && Array.isArray(items)) {
        bodyParams.items = items.slice(0, 100).map((item) => helpers.toSnakeCaseKeys(item, false));
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('parameters are required of type object');
  }

  /**
   * Send browse result click event to API
   *
   * @function trackBrowseResultClick
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.filterName - Filter name
   * @param {string} parameters.filterValue - Filter value
   * @param {string} parameters.itemId - Product item unique identifier
   * @param {string} [parameters.section="Products"] - Index section
   * @param {string} [parameters.variationId] - Product item variation unique identifier
   * @param {string} [parameters.resultId] - Browse result identifier (returned in response from Constructor)
   * @param {number} [parameters.resultCount] - Total number of results
   * @param {number} [parameters.resultPage] - Page number of results
   * @param {number} [parameters.resultPositionOnPage] - Position of clicked item
   * @param {number} [parameters.numResultsPerPage] - Number of results shown
   * @param {object} [parameters.selectedFilters] -  Selected filters
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {object} userParameters - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User clicked a result that appeared within a browse product listing page
   * @example
   * constructorio.tracker.trackBrowseResultClick(
   *     {
   *         variationId: 'KMH879-7632',
   *         resultId: '019927c2-f955-4020-8b8d-6b21b93cb5a2',
   *         resultCount: 22,
   *         resultPage: 2,
   *         resultPositionOnPage: 2,
   *         numResultsPerPage: 12,
   *         selectedFilters: { brand: ['foo'], color: ['black'] },
   *         filterName: 'brand',
   *         filterValue: 'XYZ',
   *         itemId: 'KMH876',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackBrowseResultClick(parameters, userParameters, networkParameters = {}) {
    // Ensure parameters are provided (required)
    if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/browse_result_click?`;
      const bodyParams = {};
      const {
        section,
        variation_id,
        variationId = variation_id,
        result_id,
        resultId = result_id,
        result_count,
        resultCount = result_count,
        result_page,
        resultPage = result_page,
        result_position_on_page,
        resultPositionOnPage = result_position_on_page,
        num_results_per_page,
        numResultsPerPage = num_results_per_page,
        selected_filters,
        selectedFilters = selected_filters,
        filter_name,
        filterName = filter_name,
        filter_value,
        filterValue = filter_value,
        item_id,
        itemId = item_id,
        analyticsTags,
      } = parameters;

      if (section) {
        bodyParams.section = section;
      } else {
        bodyParams.section = 'Products';
      }

      if (variationId) {
        bodyParams.variation_id = variationId;
      }

      if (resultId) {
        bodyParams.result_id = resultId;
      }

      if (!helpers.isNil(resultCount)) {
        bodyParams.result_count = resultCount;
      }

      if (!helpers.isNil(resultPage)) {
        bodyParams.result_page = resultPage;
      }

      if (!helpers.isNil(resultPositionOnPage)) {
        bodyParams.result_position_on_page = resultPositionOnPage;
      }

      if (!helpers.isNil(numResultsPerPage)) {
        bodyParams.num_results_per_page = numResultsPerPage;
      }

      if (selectedFilters) {
        bodyParams.selected_filters = selectedFilters;
      }

      if (filterName) {
        bodyParams.filter_name = filterName;
      }

      if (filterValue) {
        bodyParams.filter_value = filterValue;
      }

      if (itemId) {
        bodyParams.item_id = itemId;
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('parameters are required of type object');
  }

  /**
   * Send generic result click event to API
   *
   * @function trackGenericResultClick
   * @param {object} parameters - Additional parameters to be sent with request
   * @param {string} parameters.itemId - Product item unique identifier
   * @param {string} [parameters.itemName] - Product item name
   * @param {string} [parameters.variationId] - Product item variation unique identifier
   * @param {string} [parameters.section="Products"] - Index section
   * @param {object} [parameters.analyticsTags] - Pass additional analytics data
   * @param {object} [userParameters] - Parameters relevant to the user request
   * @param {number} userParameters.sessionId - Session ID, utilized to personalize results
   * @param {string} userParameters.clientId - Client ID, utilized to personalize results
   * @param {string} [userParameters.userId] - User ID, utilized to personalize results
   * @param {string[]} [userParameters.segments] - User segments
   * @param {object} [userParameters.testCells] - User test cells
   * @param {string} [userParameters.originReferrer] - Client page URL (including path)
   * @param {string} [userParameters.referer] - Client page URL (including path)
   * @param {string} [userParameters.userIp] - Client user IP
   * @param {string} [userParameters.userAgent] - Client user agent
   * @param {string} [userParameters.acceptLanguage] - Client accept language
   * @param {string} [userParameters.dateTime] - Time since epoch in milliseconds
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {(true|Error)}
   * @description User clicked a result that appeared within a browse product listing page
   * @example
   * constructorio.tracker.trackGenericResultClick(
   *     {
   *         itemId: 'KMH876',
   *         itemName: 'Red T-Shirt',
   *         variationId: 'KMH879-7632',
   *     },
   *     {
   *         sessionId: 1,
   *         clientId: '7a43138f-c87b-29c0-872d-65b00ed0e392',
   *         testCells: {
   *             testName: 'cellName',
   *         },
   *     },
   * );
   */
  trackGenericResultClick(parameters, userParameters, networkParameters = {}) {
    // Ensure required parameters are provided
    if (typeof parameters === 'object' && parameters && (parameters.item_id || parameters.itemId)) {
      const requestPath = `${this.options.serviceUrl}/v2/behavioral_action/result_click?`;
      const bodyParams = {};
      const {
        item_id,
        itemId = item_id,
        item_name,
        itemName = item_name,
        variation_id,
        variationId = variation_id,
        section,
        analyticsTags,
      } = parameters;

      bodyParams.section = section || 'Products';
      bodyParams.item_id = itemId;

      if (itemName) {
        bodyParams.item_name = itemName;
      }

      if (variationId) {
        bodyParams.variation_id = variationId;
      }

      if (analyticsTags) {
        bodyParams.analytics_tags = analyticsTags;
      }

      const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
      const requestMethod = 'POST';
      const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });

      send.call(
        this,
        requestUrl,
        userParameters,
        networkParameters,
        requestMethod,
        requestBody,
      );

      return true;
    }

    return new Error('A parameters object with an "itemId" property is required.');
  }

  /**
   * Subscribe to success or error messages emitted by tracking requests
   *
   * @function on
   * @param {string} messageType - Type of message to listen for ('success' or 'error')
   * @param {function} callback - Callback to be invoked when message received
   * @returns {(true|Error)}
   * @description
   * If an error event is emitted and does not have at least one listener registered for the
   * 'error' event, the error is thrown, a stack trace is printed, and the Node.js process
   * exits - it is best practice to always bind a `.on('error')` handler
   * @see https://nodejs.org/api/events.html#events_error_events
   * @example
   * constructorio.tracker.on('error', (data) => {
   *     // Handle tracking error
   * });
   */
  on(messageType, callback) {
    if (messageType !== 'success' && messageType !== 'error') {
      return new Error('messageType must be a string of value "success" or "error"');
    }

    if (!callback || typeof callback !== 'function') {
      return new Error('callback is required and must be a function');
    }

    this.eventemitter.on(messageType, callback);

    return true;
  }
}

module.exports = Tracker;