UNPKG

autochart-tracker

Version:

JavaScript API for tracking visitor activities on automotive websites with Autochart.io

332 lines (305 loc) 12.4 kB
import _extend from 'lodash/extend'; import _includes from 'lodash/includes'; import config from './config'; import EventDispatcher from './event-dispatcher'; import BrowserContext from './browser-context'; import Utils from './utils'; import { version } from '../package.json'; const { disabledAccounts } = config; const ACTIONS_COLLECTION = 'VisitorActions'; const TAGS_COLLECTION = 'VisitorTags'; /** * Tracking API instance. * @constructor */ function Tracker() {} // @accountKey: String // @context: { session: { visitorId, sessionId, referrer, userAgent, startTime }, page: { url} } /** * Initialise the Tracker with account key and custom options. * @param {string} accountKey - unique tracking key for your AutoChart account. Required. * @param {object} options - Array of options to override certain tracking behaviour * @param {BrowserContext} browserContext - used to access browser data (DOM, URL, cookies) */ Tracker.prototype.init = function init(accountKey, options, browserContext) { if (!accountKey) { throw new Error('accountKey must be specified'); } if (typeof accountKey !== 'string' || accountKey.length !== 24) { throw new Error('Invalid account key. It must be a 24 character long string.'); } if (options) { this.options = options; Utils.log(`Initilised with options: ${JSON.stringify(options)}`); } else { this.options = {}; } this.timeout = 1000; this.trackingDisabled = (window.AUTOCHART_DISABLED === true) || _includes(disabledAccounts, accountKey); if (this.trackingDisabled) { Utils.log('Tracking disabled. No events will be sent'); } const context = browserContext || new BrowserContext(window); this.dispatcher = new EventDispatcher(accountKey); // ============================================================================================= // INIT // ============================================================================================= // Setup global properties to be sent with all events on this page this.globalProperties = { session: context.session, urlRaw: context.page.url, pageTitle: context.page.title, dayOfWeek: (context.page.timestamp && context.page.timestamp.getDay) ? context.page.timestamp.getDay() : new Date().getDay(), timestamp: context.page.timestamp || undefined, ac: { clientTrackerLibraryVersion: version } }; this.globalProperties.session.customerAccountId = accountKey; this.globalProperties.session.userAgentRaw = context.session.userAgentRaw; this.globalProperties.session.ipAddress = context.session.ipAddress; // Send pageview event on load this.page(); }; // ============================================================================================= // TRACKING API FUNCTIONS // ============================================================================================= /** * Send a 'PageView' VisitorAction event. Automatically called in init so not usually required. * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.page = function page(done) { return this.trackVisitorAction('PageView', null, null, done); }; /** * Send a 'VehicleView' VisitorAction event for a specific vehicle. * @param {Vehicle} vehicle * @param {Date} timestamp - time the event was sent. If not specified, defaults to Date.now(). * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.trackVehicleView = function trackVehicleView(vehicle, timestamp, done) { return this.trackVisitorAction('VehicleView', { vehicles: [vehicle] }, timestamp, done); }; /** * Send a 'VehicleAction' VisitorAction event for a specific vehicle. * @param {Vehicle} vehicle * @param {string} actionCategory - category of the action, e.g. 'Save', 'Compare', 'Print' * @param {Date} timestamp - time the event was sent. If not specified, defaults to Date.now(). * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.trackVehicleAction = function trackVehicleAction( vehicle, actionCategory, timestamp, done ) { return this.trackVisitorAction( 'VehicleAction', { vehicles: [vehicle], actionCategory }, timestamp, done ); }; /** * Sends a 'Search' VisitorAction event for a specific vehicle. * @param {SearchCriteria} searchCriteria - criteria used to do the search. * @param {Date} timestamp - time the event was sent. If not specified, defaults to Date.now(). * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.trackSearch = function trackSearch(searchCriteria, timestamp, done) { return this.trackVisitorAction('Search', { searchCriteria }, timestamp, done); }; /** * Sends a 'VisitIntent' VisitorAction event for a specific vehicle. * @param {string} intentAction * @param {Date} timestamp - time the event was sent. If not specified, defaults to Date.now(). * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.trackVisitIntent = function trackVisitIntent(intentAction, timestamp, done) { return this.trackVisitorAction('VisitIntent', { intentAction }, timestamp, done); }; /** * Sends a Tag event with one or more tags. * @param {[string]} tags - array of tags to associate with this visitor. Required. * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.tag = function tag(tags, done) { if (this.trackingDisabled) { return false; } this.ensureInit(); if (!tags) { throw new Error('tags parameter must be specified'); } const tagsArray = (tags instanceof Array) ? tags : [tags.toString()]; // Sanitize tags (strip out non alphanumeric plus space dash underscore) for (let i = 0; i < tagsArray.length; i += 1) { tagsArray[i] = tagsArray[i].toString().replace(/[^\w\s-]/gi, ''); } const actionData = this.mergeGlobalProps({ tags: tagsArray }); Utils.log('Tag tracked', tags); return this.dispatcher.addEvent(TAGS_COLLECTION, actionData, done); }; /** * Sends a Lead event after visitor has performed an action such as submitting an enquiry form. * @param {Lead} lead * @param {Date} timestamp - time the event was sent. If not specified, defaults to Date.now(). * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.trackLead = function trackLead(lead, timestamp, done) { if (this.trackingDisabled) { return false; } this.ensureInit(); if (!lead) { throw new Error('Lead must be specified'); } const data = this.mergeGlobalProps({ channel: lead.channel, subject: lead.subject, contact: lead.contact, message: lead.message, vehicle: lead.vehicle, recipient: lead.recipient }, timestamp); Utils.log('Lead tracked', lead); return this.dispatcher.addEvent('Leads', data, done); }; /** * Tells the Tracker to send a lead event whenever a form is submitted. * @param {HTMLElement} form - <form> node * @param {function} leadFunction - function which will be evaluated when the form is submitted. * It must return a Lead object which will then be sent to AutoChart. * @param {function} hasValidationErrors - optionally provide function which returns true if * validation errors are present. If so, lead will not be tracked. * @param {Date} timestamp - time the event was sent. If not specified, defaults to Date.now(). * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.trackLeadForm = function trackLeadForm(form, leadFunction, hasValidationErrors, timestamp, done) { if (this.trackingDisabled) { return false; } const self = this; self.ensureInit(); if (!leadFunction || !Utils.isFunction(leadFunction)) { throw new Error('A function must be specified to return the lead data when the form is submitted.'); } if (form) { Utils.log('Wiring up submit handler'); const handler = function handler(e) { // First check for validation errors if (hasValidationErrors && hasValidationErrors(form)) { Utils.log('Validation errors detected. Lead not tracked'); return; } Utils.log('Handler fired. Preventing default submit...'); Utils.prevent(e); const lead = leadFunction(); // Track the lead self.trackLead(lead, timestamp, done); // Proceed with submitting the form if (!self.options.preventFormSubmits) { self.callback(() => { form.submit(); }); } else { Utils.log('Preventing submit of form.'); } }; // Wire up submit handler (preferably via jquery) const $ = window.jQuery || window.Zepto; if ($) { $(form).submit(handler); } else if (form.addEventListener) { form.addEventListener('submit', handler, false); } else if (form.attachEvent) { form.attachEvent('onsubmit', handler); } } return true; }; Tracker.prototype.trackLeadFormAspNet = function trackLeadFormAspNet(options = {}, leadFunction, timestamp, done) { if (this.trackingDisabled) { return false; } const self = this; self.ensureInit(); if (!leadFunction || !Utils.isFunction(leadFunction)) { throw new Error('A function must be specified to return the lead data when the form is submitted.'); } Utils.aspnet.beforePostbackAsync(options.submitButtonId, (doPostback) => { const lead = leadFunction(); self.trackLead(lead, timestamp, done); self.callback(() => { doPostback(); }); }); return true; }; /** * Sends a 'Finance' VisitorAction event, with finance data. * @param {Finance} financeData - financial data to save. Required. * @param {[type]} vehicle - vehicle to which finance action was related * @param {Date} timestamp - time the event was sent. If not specified, defaults to Date.now(). * @param {function(err, response)} done - callback fired when event completes */ Tracker.prototype.trackFinance = function trackFinance(financeData, vehicle, timestamp, done) { if (!financeData) { throw new Error('financeData object must be specified'); } return this.trackVisitorAction('Finance', { finance: financeData, vehicles: [vehicle] }, timestamp, done); }; /** * Calls specified function whenever the library has asynchronously loaded. * @param {Function} callback function to call */ Tracker.prototype.ready = function ready(callback) { if (callback && Utils.isFunction(callback)) { callback(); } }; // ============================================================================================= // PRIVATES // ============================================================================================= Tracker.prototype.ensureInit = function ensureInit() { if (!this.globalProperties.session.customerAccountId) { throw new Error('init() must be called before calling this method.'); } }; Tracker.prototype.trackVisitorAction = function trackVisitorAction(actionType, data, timestamp, done) { if (this.trackingDisabled) { return false; } this.ensureInit(); const actionData = this.mergeGlobalProps(data || {}, timestamp); actionData.actionType = actionType; Utils.log(`VisitorAction tracked: ${actionType}`, data); return this.dispatcher.addEvent(ACTIONS_COLLECTION, actionData, done); }; Tracker.prototype.mergeGlobalProps = function mergeGlobalProps(eventData, timestamp) { const merged = _extend({}, this.globalProperties, eventData); if (timestamp) { merged.timestamp = eventData.timestamp; } return merged; }; Tracker.prototype.callback = function callback(fn) { Utils.callAsync(fn, this.timeout || 1000); return this; }; export default Tracker;