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;