Source: helpers/analytics.js

import jQ from "jquery";

import Api from "./api";
import AnalyticsEnum from "../enum/analytics-enum";
import Globals from "./globals";
import Selector from "./selector";
import Utils from "./utils";
import Settings from "./settings";
import Class from "./class";

const ANALYTICS_KEY = 'boostPFSAnalytics';
const SESSION_KEY = 'boostPFSSessionId';
var CART_TOKEN = '';
var SESSION = '';
var VIEWED_PRODUCT_DATA = null;

/**
 * Init the analytics events
 */
const init = () => {

	if (!window.XMLHttpRequest) return;

	CART_TOKEN = "";
	SESSION = getLocalStorage(SESSION_KEY);
	if (!SESSION) {
		SESSION = generateUUID();
		setLocalStorage(SESSION_KEY, SESSION);
	}

	initInstantSearch();
	initCollectionSearchPage();
	initOtherPage();
}

/**
 * Init analytics on instant search
 */
const initInstantSearch = () => {
	if (Settings.getSettingValue('search.enableSuggestion')) {
		if (jQ('.' + Class.searchSuggestionWrapper).length > 0) {
			jQ('.' + Class.searchSuggestionWrapper).each((index, suggestionElement) => {
				suggestionElement.addEventListener('click', onClickProductInSuggestion, true);
				document.addEventListener('keydown', onClickProductInSuggestion, true);
			});
		}
	}
}

/**
 * Init analytics on collection/search page
 */
const initCollectionSearchPage = () => {
	if (Selector.trackingProduct && jQ(Selector.products).length > 0) {
		document.addEventListener('click', onClickProductInFilterResult, true);
	}
}

/**
 * Init analytics on product page.
 * Find and send a product click data in localStorage to server.
 */
const initOtherPage = () => {
	// Send any analytics that was cancelled before it was sent
	var dataList = getLocalStorage(ANALYTICS_KEY);
	if (!Array.isArray(dataList)) return;
	dataList.forEach((data)=> {
		sendProductClickData(data);
		if (data.pid == boostPFSAppConfig.general.product_id) {
			VIEWED_PRODUCT_DATA = data;
		}
	})

	// If go to product page through our app, bind add to cart & buy now event
	if (Utils.isProductPage()) {
		if (Selector.trackingAddToCart && jQ(Selector.trackingAddToCart).length > 0) {
			jQ(Selector.trackingAddToCart)[0].addEventListener('click', onClickAddToCartInProductPage, true);
		}
		if (Selector.trackingBuyNow && jQ(Selector.trackingBuyNow).length > 0) {
			jQ(Selector.trackingBuyNow)[0].addEventListener('click', onClickBuyNowInProductPage, true);
		}
	}
}

const refreshCartToken = (dataToRetry) => {
	// Set up HTTP request
	var xhr = new XMLHttpRequest();
	xhr.open('GET', '/cart.js');
	xhr.onload = function () {
		if (xhr.readyState > 3 && xhr.status == 200) {
			// On sucesss
			var cart = JSON.parse(xhr.responseText);
			var cartToken = (cart.item_count <= 0) ? "" : cart.token;
			CART_TOKEN = cartToken;
			if (dataToRetry) {
				dataToRetry.ct = cartToken;
				sendProductClickData(dataToRetry, true);
			}
		}
	};
	xhr.send();
}

/**
 * Generates a random unique session ID
 * @return {string} random unique ID
 */
const generateUUID = () => {
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
		var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
		return v.toString(16);
	});
}

/**
 * Handle analytic on click product list in the collection/search page.
 * Save the clicked product data to localStorage.
 * @param {Event} event - the click event
 */
const onClickProductInFilterResult = (event) => {
	if (!event || !event.target) return;
	var $clickedElement = jQ(event.target);

	var action = Utils.isSearchPage() ? AnalyticsEnum.Action.SEARCH : AnalyticsEnum.Action.FILTER;
	var userAction = AnalyticsEnum.UserAction.VIEW_PRODUCT;
	if (Selector.trackingQuickView && $clickedElement.closest(Selector.trackingQuickView).length > 0) {
		userAction = AnalyticsEnum.UserAction.QUICK_VIEW;
	}
	if (Selector.trackingAddToCart && $clickedElement.closest(Selector.trackingAddToCart).length > 0) {
		userAction = AnalyticsEnum.UserAction.ADD_TO_CART;
	}
	if (Selector.trackingBuyNow && $clickedElement.closest(Selector.trackingBuyNow).length > 0) {
		userAction = AnalyticsEnum.UserAction.BUY_NOW;
	}

	// If the user clicked quickview button,
	// and then click add to cart/buy now within the quick view modal,
	// but the modal is outside of the product grid item,
	// we'll use the last clicked id from the quick view event.
	var productId = '';
	var $productElement = $clickedElement.closest(Selector.trackingProduct);
	// If found product grid item
	if ($productElement.length > 0) {
		productId = $productElement.attr('data-id');
	// If not found product grid item, maybe we're inside a quickview modal.
	} else if (VIEWED_PRODUCT_DATA) {
		// Add to cart and buy now within modal
		if (userAction == AnalyticsEnum.UserAction.ADD_TO_CART || userAction == AnalyticsEnum.UserAction.BUY_NOW) {
			productId = VIEWED_PRODUCT_DATA.pid;
		}
	}
	if (!productId) return;

	var data = buildProductClickData(productId, userAction, action);
	addProductClickData(data);
	sendProductClickData(data);

	if (userAction == AnalyticsEnum.UserAction.QUICK_VIEW) {
		VIEWED_PRODUCT_DATA = data;
	} else {
		VIEWED_PRODUCT_DATA = null;
	}
}


/**
 * Handle analytic on click product in search suggestion.
 * Save the clicked product data to localStorage.
 * @param {Event} event - the click event
 */
const onClickProductInSuggestion = (event) => {
	if (!event || !event.target) return;

		// Check for keyboard enter event
	if (event.type == 'keydown' && event.keyCode != 13) return;

	var $clickedElement = jQ(event.target);
	var $productElement = $clickedElement.closest('.' + Class.searchSuggestionItem + '-product');
	if (!$productElement) return;

	var productId = $productElement.attr('data-id');
	if (!productId) return;

	var data = buildProductClickData(productId, AnalyticsEnum.UserAction.VIEW_PRODUCT, AnalyticsEnum.Action.SUGGEST);
	addProductClickData(data);
}

const onClickAddToCartInProductPage = (event) => {
	var data = {
		tid: Globals.shopDomain,
		pid: boostPFSAppConfig.general.product_id.toString(),
		u: AnalyticsEnum.UserAction.ADD_TO_CART,
		ct: CART_TOKEN
	};
	addProductClickData(data);
	sendProductClickData(data);
}

const onClickBuyNowInProductPage = (event) => {
	var data = {
		tid: Globals.shopDomain,
		pid: boostPFSAppConfig.general.product_id.toString(),
		u: AnalyticsEnum.UserAction.BUY_NOW
	};
	addProductClickData(data);
	sendProductClickData(data);
}

/**
 * Build product click data in collection/search page and in instant search.
 * @param {Number} productId
 * @param {AnalyticsEnum.UserAction} userAction - UserAction enum
 * @param {AnalyticsEnum.Action} action - Action enum
 * @return {Object} data - the click data to be add to localStorage/send to server.
 */
const buildProductClickData = (productId, userAction, action) => {
	var currentTime = new Date();

	// Get cart token from global
	var cartToken = CART_TOKEN;

	// Merge quick_view and view_product when sending to backend
	var mergeUserAction = userAction == AnalyticsEnum.UserAction.QUICK_VIEW ? AnalyticsEnum.UserAction.VIEW_PRODUCT : userAction;

	// Get query string data
	var queryString = '';
	if (action == AnalyticsEnum.Action.FILTER) {
		queryString += 'collection_scope=' + Globals.collectionId;
	} else {
		queryString += 'q=' + Globals.currentTerm;
	}
	if (action == AnalyticsEnum.Action.FILTER || action == AnalyticsEnum.Action.SEARCH) {
		var filteredKeys = Object.keys(Globals.queryParams).filter(key => key.startsWith(Globals.prefix));
		if (filteredKeys && filteredKeys.length > 0) {
			filteredKeys.forEach(key => {
				var values = Globals.queryParams[key];
				if (Array.isArray(values)) {
					values.forEach(value => {
						queryString += '&' + key + '=' + encodeURIComponent(value);
					})
				} else {
					queryString += '&' + key + '=' + encodeURIComponent(values);
				}
			})
		}
	}

	// Build data
	var data = {
		tid: Globals.shopDomain,
		ct: cartToken,
		pid: productId,
		t: currentTime.toISOString(),
		u: mergeUserAction,
		a: action,
		qs: queryString,
		r: document.referrer
	}
	return data;
}

/**
 * Add product click data in local storage.
 * @param {Object} data - product click data
 */
const addProductClickData = (data) => {
	// Get data list from local storage
	var dataList = getLocalStorage(ANALYTICS_KEY)
	if (!Array.isArray(dataList)) dataList = [];

	// Add new data to the list, without duplicated id
	var newDataList = dataList.filter(x => x.pid != data.productId);
	newDataList.push(data);

	setLocalStorage(ANALYTICS_KEY, newDataList);
}

const removeProductClickData = (productId) => {
	// Get data list from local storage
	var dataList = getLocalStorage(ANALYTICS_KEY);
	if (!Array.isArray(dataList)) return;

	// Filter for products that doesn't match the id
	var newDataList = dataList.filter(x => x.pid != productId);
	setLocalStorage(ANALYTICS_KEY, newDataList);
}

const getProductClickData = (productId) => {
	// Get data list from local storage
	var dataList = getLocalStorage(ANALYTICS_KEY);
	if (!Array.isArray(dataList)) return null;

	// Find by product id
	var matchedData = dataList.find(x => x.pid == productId);
	return matchedData;
}

const getLocalStorage = (key) => {
	try {
		return JSON.parse(localStorage.getItem(key));
	} catch {
		return null;
	}
}

const setLocalStorage = (key, value) => {
	try {
		if (value != null) {
			localStorage.setItem(key, JSON.stringify(value));
		} else {
			localStorage.setItem(key, "");
		}

	} catch {}
}

/**
 * Send product click data to server.
 * @param {Object} data - product click data
 * @param {boolean} triedToGetToken - tried to get cart token by calling cart.js or not
 */
const sendProductClickData = (data, triedToGetToken) => {

	if (!triedToGetToken && !data.ct) {
		setTimeout(function () {
			refreshCartToken(data);
		}, 1000);
		return;
	}

	data.sid = SESSION;

	// Set up HTTP request
	var xhr = new XMLHttpRequest();
	xhr.open('POST', Api.getApiUrl('analytics'));
	xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
	xhr.onload = function () {
		// On sucess
		if (xhr.readyState > 3 && xhr.status == 200) {
			removeProductClickData(data.pid);
		}
	};
	xhr.send(JSON.stringify(data));
}

const Analytics = {
	init: init
}

export default Analytics;