(function() {
/**
* @namespace tracking
* @memberof Help4
*/
Help4.tracking = {};
/**
* @namespace connector
* @memberof Help4.tracking
*/
Help4.tracking.connector = {};
/**
* @typedef {Object} Help4.tracking.Tracking.Params
* @property {Help4.controller.Controller} controller
* @property {string} [sessionId]
*/
/**
* @typedef {Object} Help4.tracking.Tracking.Connector
* @property {string} type
* @property {string} [url]
* @property {string} [token]
* @property {function} [callback]
*/
/**
* @typedef {Object} Help4.tracking.Tracking.Data
* @property {{id: string}} verb
* @property {Help4.tracking.Tracking.Object} object
* @property {{id: string, app: string}} context
*/
/**
* @typedef {Object} Help4.tracking.Tracking.Object
* @property {string} screenId
* @property {string} product
* @property {string} version
* @property {string} system
* @property {?string} role
* @property {boolean} editor
* @property {string} language
* @property {string} playbackTag
* @property {string} sessionId
* @property {string} solution
* @property {?string} customer_tenant
* @property {?string} id
* @property {?string} name
* @property {Help4.tracking.Tracking.ModelInfo} _modelInfo
* @property {string} [theme]
* @property {boolean} [mobile]
* @property {boolean} [rtl]
*/
/**
* @typedef {Object} Help4.tracking.Tracking.ModelInfo
* @property {?string} projectTitle
* @property {string} backendType
* @property {?string} projectId
* @property {{project: string, tile: string}} _prefix
*/
/**
* @typedef {
* Help4.tracking.Tracking.TrackParamsBasic|
* Help4.tracking.Tracking.TrackParamsHelpOpenClose|
* Help4.tracking.Tracking.TrackParamsTile|
* Help4.tracking.Tracking.TrackParamsLink|
* Help4.tracking.Tracking.TrackParamSearch|
* Help4.tracking.Tracking.TrackParamTourOpen|
* Help4.tracking.Tracking.TrackParamTourClose|
* Help4.tracking.Tracking.TrackParamTourStep|
* Help4.tracking.Tracking.TrackParamLearningApp
* } Help4.tracking.Tracking.TrackParams
*/
/**
* @typedef {Object} Help4.tracking.Tracking.TrackParamsBasic
* @property {string} verb
* @property {string} [type]
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamsHelpOpenClose
* @property {boolean} editMode
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamsTile
* @property {string} tile
* @property {string} title
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamsLink
* @property {string} url
* @property {string} title
* @property {string} url
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamSearch
* @property {string} term
* @property {function} getResults
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamTourOpen
* @property {boolean} editMode
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamTourClose
* @property {boolean} editMode
* @property {string} step
* @property {boolean} finished
* @property {number} stepIndex
* @property {number} totalSteps
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamTourStep
* @property {string} step
* @property {string} title
*/
/**
* @typedef {Help4.tracking.Tracking.TrackParamsBasic} Help4.tracking.Tracking.TrackParamLearningApp
* @property {string} [objectType]
* @property {string} id
* @property {string} name
*/
/**
* @augments Help4.jscore.ControlBase
* @property {Help4.controller.Controller} controller
* @property {string} sessionId
* @property {?number} _searchInterval
* @property {Object[]} _connectors
*/
Help4.tracking.Tracking = class extends Help4.jscore.ControlBase {
/**
* @override
* @param {Help4.tracking.Tracking.Params} params
*/
constructor(params) {
const {TYPES: T} = Help4.jscore.ControlBase;
params.sessionId ||= Help4.createId().replace(/_/g, '');
super(params, {
params: {
controller: {type: T.instance, mandatory: true, readonly: true},
sessionId: {type: T.string, mandatory: true}
},
statics: {
_searchInterval: {init: null, destroy: false},
_connectors: {init: [], destroy: false}
}
});
}
static TRACKING_DATA = ['screenId', 'product', 'version', 'system', 'role', 'editor', 'language', 'playbackTag'];
static HELP_BUTTON_DATA = ['theme', 'mobile', 'rtl'];
static SEARCH_INTERVAL_TIME = 1500;
/** @override */
destroy() {
const {_searchInterval} = this;
_searchInterval && clearTimeout(_searchInterval);
super.destroy();
}
/** @returns {Object} */
serialize() {
return {i: this.sessionId};
}
/** @param {?Object} data */
deserialize(data) {
if (data?.i) this.sessionId = data.i;
}
/** @returns {string} */
getSessionId() {
return this.sessionId;
}
/** @param {Help4.tracking.Tracking.Connector[]} connectors */
addConnector(connectors) {
const {connector} = Help4.tracking;
const {_connectors} = this;
for (const con of connectors) {
if (connector.hasOwnProperty(con.type)) {
_connectors.push(new connector[con.type](con));
}
}
}
/**
* tracks Help ? button interactions
* @returns {?Promise<void>} returns Promise for CMP4 only
*/
async trackHelpButton() {
const {/** @type {Help4.controller.Controller} */ controller} = this;
const {CMP4} = controller.getConfiguration();
const modelInfo = /** @type {Help4.tracking.Tracking.ModelInfo} */ CMP4
? await _getModelInfoCMP4.call(this)
: _getModelInfoCMP3.call(this);
const object = _getObject.call(this, modelInfo, this.constructor.HELP_BUTTON_DATA);
object.objectType = 'button';
_send.call(this, {
verb: {id: 'start'},
context: {id: object.id, app: 'WA'},
object
});
}
async trackVideoPlay(videoSrc) {
return this.trackProject({type: 'help', verb: 'video', url: videoSrc})
}
async trackExternalLink(url) {
return this.trackProject({type: 'help', verb: 'external-link', url})
}
/**
* tracks help or tour projects
* @param {Help4.tracking.Tracking.TrackParams} params
* @returns {?Promise<void>} returns Promise for CMP4 only
*/
async trackProject(params) {
const {/** @type {Help4.controller.Controller} */ controller} = this;
const {CMP4} = controller.getConfiguration();
const modelInfo = /** @type {Help4.tracking.Tracking.ModelInfo} */ CMP4
? await _getModelInfoCMP4.call(this)
: _getModelInfoCMP3.call(this);
const object = _getObject.call(this, modelInfo);
const {type, editMode, verb} = params;
const {_modelInfo: {_prefix}} = object;
object.objectType = `project:${type}`;
const {TRACK_STATUS} = Help4.widget;
const data = {verb: {id: verb}, object, context: {id: object.id, app: 'WA'}};
const model = CMP4 ? null : controller.getService('model');
switch (`${type}-${verb}`) {
case 'help-open':
object.editMode = editMode;
if (object.id) {
TRACK_STATUS.object = Help4.cloneObject(object);
_send.call(this, data);
}
break;
case 'help-close': {
const {object} = TRACK_STATUS;
if (object?.id) {
TRACK_STATUS.object = {}; // reset
const data = {verb: {id: verb}, object, context: {id: object.id, app: 'WA'}};
_send.call(this, data);
}
break;
}
case 'help-video': {
const {url} = params;
object.objectType = `help:${verb}`;
object.name = '';
object.id = url;
data.verb.id = 'play';
_send.call(this, data);
break;
}
case 'help-external-link': {
const {url} = params;
object.objectType = `help:${verb}`;
object.name = '';
object.id = url;
data.verb.id = 'open';
_send.call(this, data);
break;
}
case 'help-tile':
case 'help-link': {
const {tile, url} = params;
const {title} = CMP4
? params
: model.getTile(tile) || {};
object.objectType = `help:${verb}`;
object.name = title || '';
object.id = verb === 'tile'
? _prefix.tile + tile
: url;
data.verb.id = 'open';
_send.call(this, data);
break;
}
case 'help-search':
_trackSearch.call(this, 'webassistant', data, params);
break;
case 'tour-open':
case 'tour-close':
if (!(object.editMode = editMode)) {
if (verb === 'close') {
const {finished, step, stepIndex, totalSteps} = params;
if (!(object.finished = finished)) {
object.step = _prefix.tile + step;
object.stepIndex = stepIndex;
object.totalSteps = totalSteps;
}
}
data.verb.id = {open: 'start', close: 'stop'}[verb];
}
_send.call(this, data);
break;
case 'tour-step': {
const {step} = params;
const {title} = CMP4
? params
: model.getTile(step) || {};
data.verb.id = 'open';
object.objectType = 'tour:step';
object.id = _prefix.tile + step;
object.name = title || '';
_send.call(this, data);
break;
}
}
}
/**
* tracks LearningApp interactions
* @param {Help4.tracking.Tracking.TrackParamLearningApp|Help4.tracking.Tracking.TrackParamSearch} params
* @returns {?Promise<void>} returns Promise for CMP4 only
*/
async trackLearningApp(params) {
const {/** @type {Help4.controller.Controller} */ controller} = this;
const {CMP4} = controller.getConfiguration();
const modelInfo = /** @type {Help4.tracking.Tracking.ModelInfo} */ CMP4
? await _getModelInfoCMP4.call(this)
: _getModelInfoCMP3.call(this);
const object = _getObject.call(this, modelInfo);
object.objectType = `learningApp:${params.objectType || params.type}`;
const data = {
verb: {id: params.verb},
context: {id: object.id, app: 'WA'},
object
};
if (params.type === 'content') {
object.id = params.id;
object.name = params.name;
}
params.verb === 'search'
? _trackSearch.call(this, 'learningApp', data, params)
: _send.call(this, data);
}
}
/**
* @memberof Help4.tracking.Tracking#
* @private
* @param {Help4.tracking.Tracking.ModelInfo} modelInfo
* @param {string[]} [additionalMap = []]
* @returns {Help4.tracking.Tracking.Object}
*/
function _getObject(modelInfo, additionalMap = []) {
const {
constructor: {/** @type {string[]} */ TRACKING_DATA},
/** @type {Help4.controller.Controller} */ controller,
/** @type {string} */ sessionId
} = this;
const configuration = controller.getConfiguration();
const map = TRACKING_DATA.concat(additionalMap);
const result = Help4.filterObject(configuration, map);
if (result.language?.wpb) result.language = result.language.wpb;
const {projectId: id, projectTitle: name, _prefix} = modelInfo;
let {solution, tenant} = configuration.core || {};
// CMP and learningApp config
solution ||= configuration.solution;
tenant ||= configuration.tenant;
return Help4.extendObject(result, {
sessionId,
id: id ? _prefix.project + id : id,
name,
solution,
customer_tenant: tenant,
_modelInfo: modelInfo
});
}
/**
* @memberof Help4.tracking.Tracking#
* @private
* @returns {Help4.tracking.Tracking.ModelInfo}
*/
function _getModelInfoCMP3() {
const {/** @type {Help4.controller.Controller} */ controller} = this;
const model = /** @type {Help4.model.Model} */ controller.getService('model');
const project = model.getProject() || {id: null, title: null, _ext: 'ro'};
return _completeModelInfo.call(this, project);
}
/**
* @memberof Help4.tracking.Tracking#
* @private
* @returns {Promise<Help4.tracking.Tracking.ModelInfo>}
*/
async function _getModelInfoCMP4() {
const {/** @type {Help4.controller.Controller} */ controller} = this;
const configuration = controller.getConfiguration();
let project = null;
/** @type {{
* help: ?Help4.widget.help.Widget,
* tour: ?Help4.widget.tour.Widget,
* learning: ?Help4.widget.learning.Widget
* }}
*/
const {help, tour, whatsnew, filter} = Help4.widget.getInstance();
const hasActiveInstance = !!Help4.widget.getActiveInstance();
const isBackendProject = project => project._catalogueType === 'UACP' || project._catalogueType === 'SEN' || project._catalogueType === 'SEN2';
const getTourProject = () => {
const {Core} = Help4.widget.companionCore;
const context = /** @type {Help4.widget.tour.Widget.Context} */ tour.getContext();
const {/** @type {Help4.widget.help.Data} */ data} = context.widget.help || {};
const {/** @type {string} */ projectId} = tour;
const catalogueKey = Core.getCatalogueKey({configuration});
return /** @type {?Help4.widget.help.CatalogueProject} */ data.getCatalogueProject(projectId, catalogueKey);
}
const getWhatsnewProject = async () => {
const context = /** @type {Help4.widget.whatsnew.Widget.Context} */ whatsnew.getContext();
const {/** @type {Help4.widget.whatsnew.Data} */ data} = context.widget.help || {};
const projects = /** @type {Help4.widget.help.Project[]} */ await data.getHelp(); // CMP4 is multi-project capable
return /** @type {?Help4.widget.help.Project} */ projects.find(isBackendProject);
}
const getHelpProject = async () => {
const context = /** @type {Help4.widget.help.Widget.Context} */ help?.getContext();
const {/** @type {Help4.widget.help.Data} */ data} = context?.widget.help || {};
if (!data) return null;
const projects = /** @type {Help4.widget.help.Project[]} */ await data.getHelp(); // CMP4 is multi-project capable
return /** @type {?Help4.widget.help.Project} */ projects.find(isBackendProject);
}
if (!hasActiveInstance) { // home screen
project = await getHelpProject();
} else if (tour?.isActive()) {
project = getTourProject();
} else if (whatsnew?.isActive()) {
project = await getWhatsnewProject();
} else if (help?.isActive()) {
project = await getHelpProject();
} else if (filter?.isActive()) {
project = await getHelpProject() || await getWhatsnewProject();
}
return _completeModelInfo.call(this, project);
}
/**
* @memberof Help4.tracking.Tracking#
* @private
* @param {?{id: ?string, title: ?string, _ext: ?string, _rw: ?string}} project
* @returns {Help4.tracking.Tracking.ModelInfo}
*/
function _completeModelInfo(project) {
const {/** @type {Help4.controller.Controller} */ controller} = this;
const configuration = controller.getConfiguration();
const {serviceLayer, roModel} = configuration.help || configuration; // make sure it works in learning app
const prefixMap = {
uacp: {project: 'loio!', tile: 'loio!'},
wpb: {project: 'project!', tile: 'macro!'}
};
project ||= {id: null, title: null, _ext: 'ro', _rw: null};
const {id, title, _ext, _rw} = project;
const prefixKey = serviceLayer === 'ext'
? _ext === 'ro' && roModel !== 'wpb' ? 'uacp' : 'wpb'
: serviceLayer || 'uacp';
return {
backendType: serviceLayer,
projectId: serviceLayer === 'ext' ? _rw || id : id,
projectTitle: title,
_prefix: prefixMap[prefixKey]
};
}
/**
* @memberof Help4.tracking.Tracking#
* @private
* @param {string} appName
* @param {Help4.tracking.Tracking.Data} data
* @param {Help4.tracking.Tracking.TrackParamSearch} params
*/
function _trackSearch(appName, data, params) {
const callback = () => {
if (this.isDestroyed()) return;
this._searchInterval = null;
const results = params.getResults(); // read the latest tile count shown in the UI
// XRAY-4491: handler is destroyed do not send track data since tile count will be incorrect
if (results == null) return;
const {object} = data;
object.objectType = `${appName}:search`;
object.id = `${appName}!search`;
object.name = 'search';
object.search = params.term;
object.results = results;
_send.call(this, data);
}
const {_searchInterval, constructor: {SEARCH_INTERVAL_TIME}} = this;
_searchInterval && clearTimeout(_searchInterval);
this._searchInterval = setTimeout(callback, SEARCH_INTERVAL_TIME);
}
/**
* @memberof Help4.tracking.Tracking#
* @private
* @param {Help4.tracking.Tracking.Data} data
*/
function _send(data) {
// remove all internal and undefined information
Object.entries(data.object).forEach(([key, value]) => {
if (key.charAt(0) === '_' || value == null) delete data.object[key];
});
// track using all connectors
const {_connectors} = this;
for (const connector of _connectors) {
connector.track(data).catch(xhr => {
// XXX: error handling
});
}
}
})();