UNPKG

jsaction

Version:

Google's event delegation library

1,473 lines (1,267 loc) 43.8 kB
// Copyright 2008 Google Inc. All rights reserved. goog.provide('jsaction.ActionFlow'); goog.provide('jsaction.ActionFlow.Event'); goog.provide('jsaction.ActionFlow.EventType'); goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.events.Event'); goog.require('goog.events.EventTarget'); goog.require('goog.object'); goog.require('goog.structs.Set'); goog.require('goog.style'); goog.require('jsaction.Attribute'); goog.require('jsaction.Branch'); goog.require('jsaction.Char'); goog.require('jsaction.Name'); goog.require('jsaction.Property'); goog.require('jsaction.Tick'); goog.require('jsaction.UrlParam'); goog.require('jsaction.event'); /** * Object wrapper around action flow that deals with overlapping action * flow instances and provides a nicer API than the procedural * API. The constructor implicitly records the start tick. * * @param {string} flowType For a ActionFlow that tracks a jsaction, * this is the name of the jsaction, including the * namespace. Otherwise it is whatever name the client application * choses to track its actions by. * @param {Element} opt_node The node. * @param {Event} opt_event The event. * @param {number} opt_startTime The time at which the flow started, * defaulting to the current time. * @param {?string=} opt_eventType The jsaction event type, e.g. "click". * @constructor * @extends {goog.events.EventTarget} */ jsaction.ActionFlow = function(flowType, opt_node, opt_event, opt_startTime, opt_eventType) { jsaction.ActionFlow.base(this, 'constructor'); /** * The flow type. For an ActionFlow instance that tracks a jsaction, * this is the name of the jsaction including the jsnamespace. This * is cleaned so that CSI likes it as an action name. TODO(user): * However, this cleanup should be done at reporting time, and * actually by the report event handler that formats the CSI * request, not here. * @type {string} * @private */ this.flowType_ = flowType.replace(jsaction.ActionFlow.FLOWNAME_CLEANUP_RE_, jsaction.ActionFlow.FLOWNAME_SAFE_CHAR_); /** * The flow type, without modification. Cf. flowType_, above. * @type {string} * @private */ this.unobfuscatedFlowType_ = flowType; /** * The node at which the jsaction originated, if any. * @type {Element} * @private */ this.node_ = opt_node || null; /** * The event which triggered the jsaction, or a copy thereof, if any. * @type {Event} * @private */ this.event_ = opt_event ? jsaction.event.maybeCopyEvent(opt_event) : null; /** * The jsaction event type. * @type {?string} * @private */ this.eventType_ = opt_eventType || null; /** * The collection of timers, as an array of pairs of [name,value]. * There are two interfaces for timers: tick() records a timer as * differences from start; intervalStart()/intervalEnd() records a * timer as time difference between arbitrary points in time after * start. The array is kept sorted by the tick times. * @type {!Array.<!Array>} * @private */ this.timers_ = []; /** * A map from tick name to tick time (in absolute time). * @type {!Object} * @private */ this.ticks_ = {}; /** * The start time, recorded in the constructor. * @type {number} * @private */ this.start_ = opt_startTime || goog.now(); /** * The maximum tick time in absolute time. * @type {number} * @private */ this.maxTickTime_ = this.start_; /** * The opened branches and the number of times each branch was * opened (i.e. how many times should done() be called for each * particular branch). * We initialize the main branch as opened (as the constructor itself * is an implicit branch). * @type {!Object.<string, number>} * @private */ this.branches_ = {}; this.branches_[jsaction.Branch.MAIN] = 1; /** * The set of duplicate ticks. They are reported in extra data in the * jsaction.Name.DUP key. * @type {goog.structs.Set} * @private */ this.duplicateTicks_ = new goog.structs.Set; /** * A flag that indicates that a report was sent for this * flow. Used for diagnosis of errors due to calls after the flow * has finished. * @type {boolean} * @private */ this.reportSent_ = false; /** * Collects the data for jsaction tracking related to this ActionFlow * instance that are extraced from the DOM context of the * jsaction. Added by action(). * @type {!Object} * @private */ this.actionData_ = {}; /** * Collects additional data to be reported after action is done. * The object contains string key-value pairs. Added by * addExtraData(). * @type {!Object.<string, string>} * @private */ this.extraData_ = {}; /** * Collects the data for log impressions related to this ActionFlow * instance. Set by impression(). * @type {!Object} * @private */ this.impressionData_ = {}; /** * Flag that indicates if the flow was abandoned. If it was, no report will * be sent when the flow completes. * @type {boolean} * @private */ this.abandoned_ = false; // If event is a click (plain or modified), generically track the // action. Can possibly be extended to other event types. // // The handler of the action may modify the DOM context, which is // included in the tracking information. Hence, it's important to // track the action *before* the handler executes. // // The flow must be fully constructed before calling action(), // which relies at least on this.actionData_ being defined. if (jsaction.ActionFlow.ENABLE_GENERIC_EVENT_TRACKING && opt_event && opt_node && opt_event['type'] == 'click') { this.action(opt_node); } // We store all pending flows to make it easier to find a hung // flow. This is effective only in debug. jsaction.ActionFlow.registerInstance_(this); /** * A unique identifier for this flow. * @type {number} * @private */ this.id_ = ++jsaction.ActionFlow.nextId_; // NOTE(user): Dispatching this event must always be the last line in // the constructor so that listeners will receive an initialized flow. var event = new jsaction.ActionFlow.Event( jsaction.ActionFlow.EventType.CREATED, this); if (jsaction.ActionFlow.report != null) { jsaction.ActionFlow.report.dispatchEvent(event); } }; goog.inherits(jsaction.ActionFlow, goog.events.EventTarget); /** * @define {boolean} Whether to do generic event tracking based on the * 'oi' attribute on action targets or their parent nodes. */ goog.define('jsaction.ActionFlow.ENABLE_GENERIC_EVENT_TRACKING', true); /** * A registry of action flow instances. This makes it easy to find hung * ones. * @type {!Array.<!jsaction.ActionFlow>} */ jsaction.ActionFlow.instances = []; /** * Registers a new instance in the instances registry. * @param {!jsaction.ActionFlow} instance The instance (of course, gjslint). * @private */ jsaction.ActionFlow.registerInstance_ = function(instance) { jsaction.ActionFlow.instances.push(instance); }; /** * Removes an instance from the instances registry when it's * done. * @param {!jsaction.ActionFlow} instance The instance (of course, gjslint). * @private */ jsaction.ActionFlow.removeInstance_ = function(instance) { goog.array.remove(jsaction.ActionFlow.instances, instance); }; /** * The dispatcher of the events that report about ActionFlow * instances. ActionFlow instances trigger events at the end of their * life for the application to handle, and e.g. send CSI and click * tracking reports. See jsaction.ActionFlow.Event for the event detail * data associated with such an event, and * jsaction.ActionFlow.EventType for the different events that are * fired. * If set to null, no reports will be sent. * @type {goog.events.EventTarget} */ jsaction.ActionFlow.report = new goog.events.EventTarget; jsaction.ActionFlow.FLOWNAME_CLEANUP_RE_ = /[~.,?&-]/g; /** * The character which we use to replace unsafe characters when * reporting to CSI. * @type {string} * @const * @private */ jsaction.ActionFlow.FLOWNAME_SAFE_CHAR_ = '_'; /** * The marker for the last processed output template element. * @type {string} * @const * @private */ jsaction.ActionFlow.TEMPLATE_LAST_OUTPUT_MARKER_ = '*'; /** * Errors reported by the action flow. * @enum {string} */ jsaction.ActionFlow.Error = { /** * Method action() was called after the flow finished. */ ACTION: 'action', /** * Method branch() was called after the flow finished. */ BRANCH: 'branch', /** * Method done() was called after the flow finished or on a branch * that was not pending. */ DONE: 'done', /** * Method addExtraData() was called after the flow finished. */ EXTRA_DATA: 'extradata', /** * Method impression() was called after the flow finished. */ IMPRESSION: 'impression', /** * A tick was added on the flow after the flow finished. */ TICK: 'tick', /** * Flow didn't have done() called within a time threshold. * * NOTE: There is no detection of this error within the ActionFlow itself. * It's up to the ActionFlow client to implement detection and define the * time threshold. */ HUNG: 'hung' }; /** * A counter used for generating unique identifiers. * @type {number} * @private */ jsaction.ActionFlow.nextId_ = 0; if (goog.DEBUG) { /** * Specifies the flow type we want to show logging for. Only messages for this * flow will show up at the console. * @type {Array.<string>} */ jsaction.ActionFlow.LOG_FOR_FLOW_TYPES = [/* e.g. 'application_link', '*' */]; /** * Checks whether a particular value of flowType should be logged. * @param {string} flowType The value of the flowType. * @return {boolean} Whether we should log or not for this flow type. */ jsaction.ActionFlow.shouldLog = function(flowType) { // This is very inefficient, but it's debug time, so that's okay and we // prefer shorter simpler code. for (var i = 0; i < jsaction.ActionFlow.LOG_FOR_FLOW_TYPES.length; i++) { var flow = jsaction.ActionFlow.LOG_FOR_FLOW_TYPES[i]; if (flow == '*' || flowType.indexOf(flow) == 0) { return true; } } return false; }; /** * A bit to flip to enable really verbose action flow logging or not. * @param {string} msg The message to log. * @private */ jsaction.ActionFlow.prototype.log_ = function(msg) { if (jsaction.ActionFlow.shouldLog(this.flowType_)) { if (window.console) { window.console.log(this.flowType_ + '(' + this.id_ + '): ' + msg); } } }; } /** * Returns a unique flow identifier. * @return {number} The unique flow identifier. */ jsaction.ActionFlow.prototype.id = function() { return this.id_; }; /** * Mark this flow as abandoned. No report will be sent when the flow completes. */ jsaction.ActionFlow.prototype.abandon = function() { this.abandoned_ = true; }; /** * @return {number} The starting tick. */ jsaction.ActionFlow.prototype.getStartTick = function() { return this.start_; }; /** * Returns the absolute value of a tick or undefined if the tick hasn't been * recorded. Requesting the special 'start' tick returns the start timestamp. * If the tick was recorded multiple times the method will return the latest * value. * @param {string} name The name of the tick. * @return {number|undefined} The absolute value of the tick. */ jsaction.ActionFlow.prototype.getTick = function(name) { if (name == jsaction.Name.START) { return this.start_; } return this.ticks_[name]; }; /** * Returns a list of tick names for all ticks recorded in this ActionFlow. * May also include a 'start' name -- the 'start' tick contains the time * when the timer was created. * @return {Array} An array of tick names. */ jsaction.ActionFlow.prototype.getTickNames = function() { var tickNames = []; tickNames.push(jsaction.Name.START); for (var i = 0; i < this.timers_.length; ++i) { tickNames.push(this.timers_[i][0]); } return tickNames; }; /** * Returns the largest tick time of all the ticks recorded so far. * @return {number} The max tick time in absolute time. */ jsaction.ActionFlow.prototype.getMaxTickTime = function() { return this.maxTickTime_; }; /** * Adopts externally recorded action ticks. Must be invoked immediately * after constructor. * * @param {Object} timers The timers object is used as an associative * container, where each attribute is a key/value pair of tick-label/ * tick-time. A tick labeled "start" is assumed to exist and will be * used as the flow's start time. All other ticks will be imported into * the flow's timers. If the start tick is missing no ticks are adopted * into the action flow. * * @param {Object.<string, number>=} opt_branches The names and counts for all * the opened branches. */ jsaction.ActionFlow.prototype.adopt = function(timers, opt_branches) { if (!timers || !goog.isDef(timers[jsaction.Name.START])) { return; } this.start_ = timers[jsaction.Name.START]; jsaction.ActionFlow.merge(this, timers); if (opt_branches) { // Method adopt() must be invoked immediately after the // constructor, so the only open branch will be the constructor // one. We can just copy the adopted branches over without // worrying that we'll overwrite. goog.object.forEach(opt_branches, goog.bind(function(count, branch) { this.branches_[branch] = count; }, this)); } }; /** * Checks if the ActionFlow instance is of a given type. * @param {string} type Flow type. * @return {boolean} Whether the type matches. */ jsaction.ActionFlow.prototype.isOfType = function(type) { return this.flowType_ == type.replace( jsaction.ActionFlow.FLOWNAME_CLEANUP_RE_, jsaction.ActionFlow.FLOWNAME_SAFE_CHAR_); }; /** * Returns the type of the ActionFlow instance. * @return {string} Flow type. */ jsaction.ActionFlow.prototype.getType = function() { return this.flowType_; }; /** * Sets the type of the ActionFlow instance. This can be used in cases where we * don't know the type of action at the time we create the ActionFlow, e.g. when * a second click produces a doubleclick action. This method should be used * sparingly, if at all. * @param {string} flowType The flow type. */ jsaction.ActionFlow.prototype.setType = function(flowType) { this.flowType_ = flowType.replace(jsaction.ActionFlow.FLOWNAME_CLEANUP_RE_, jsaction.ActionFlow.FLOWNAME_SAFE_CHAR_); this.unobfuscatedFlowType_ = flowType; }; /** * Records one tick. The tick value is relative to the start tick that * was recorded in the constructor. * @param {string} name The name of the tick. * @param {Object=} opt_opts Options with the following optional fields: * time: The timestamp, if it's not goog.now(). * doNotReportToServer: If true, do not report this tick to the * server (e.g. csi or mfe). The tick can still be used in puppet * tests. * doNotIncludeInMaxTime: If true, do not use this tick when calculating * 'max time' ticks, e.g. pdt, plt. */ jsaction.ActionFlow.prototype.tick = function(name, opt_opts) { if (this.reportSent_) { this.error_(jsaction.ActionFlow.Error.TICK, undefined, name); } opt_opts = opt_opts || {}; if (goog.DEBUG && this.reportSent_) { this.log_(this.flowType_ + ': late tick ' + name); } // If we have already recorded this tick, note that. if (name in this.ticks_) { // The duplicate ticks will get reported in extra data in the dup key. this.duplicateTicks_.add(name); } var time = opt_opts.time || goog.now(); if (!opt_opts.doNotReportToServer && !opt_opts.doNotIncludeInMaxTime && time > this.maxTickTime_) { // Only ticks that are reported to the server should affect max tick time. this.maxTickTime_ = time; } var t = time - this.start_; var i = this.timers_.length; while (i > 0 && this.timers_[i - 1][1] > t) { i--; } goog.array.insertAt(this.timers_, [name, t, opt_opts.doNotReportToServer], i); this.ticks_[name] = time; }; /** * Ends a linear, non-branched fragment of the flow of * control. Decrements the expect counter and sends report if there * are no more done() calls outstanding. * * Since the end of the flow is a time when you want to record a tick, * this also takes an optional tick name. * * @param {string} branch The name of the branch that ends. Closes the * flow opened by the branch() call with the same name. The * implicit branch in the constructor has a reserved name * (jsaction.Branch.MAIN). * @param {string=} opt_tick Optional tick to record while we are at it. * @param {Object=} opt_tickOpts An options object for the tick. */ jsaction.ActionFlow.prototype.done = function(branch, opt_tick, opt_tickOpts) { if (this.reportSent_ || !this.branches_[branch]) { // Either the flow has finished or the branch is not pending. this.error_(jsaction.ActionFlow.Error.DONE, branch, opt_tick); return; } if (opt_tick) { this.tick(opt_tick, opt_tickOpts); } this.branches_[branch]--; if (this.branches_[branch] == 0) { // Branch is closed, remove it from the map. delete this.branches_[branch]; } if (goog.DEBUG) { this.log_(' < done(' + branch + ':' + opt_tick + ')'); } if (goog.object.isEmpty(this.branches_)) { if (goog.DEBUG) { this.log_(' = report time ' + branch + ':'); } // Method report_() returns true if the DONE event was actually // fired. Then we can finalize the instance. if (this.report_()) { this.reportSent_ = true; this.finish_(); } } }; /** * Called when no more done() calls are outstanding and after the DONE * event was fired. * @private */ jsaction.ActionFlow.prototype.finish_ = function() { jsaction.ActionFlow.removeInstance_(this); this.node_ = null; this.event_ = null; this.dispose(); }; /** * Branches this flow, creating a subflow. done() must be called on the * subflow. * * Branch announces an asynchronous operation, and that a done() call * will arrive asynchronously at some later time. This allows a * ActionFlow to account for multiple concurrent asynchronous * operations to finish in arbitrary order. * * Since the begin of an asynchronous operation is a time when you * want to record a tick, this also takes an optional tick name. * * @param {string} branch The name of the branch that is created. The * corresponding done() should use the same name to signal that * the branch has finished. * @param {string=} opt_tick Optional tick to record while we are at. * @param {Object=} opt_tickOpts Tick configuration object. See tick() * for more details. */ jsaction.ActionFlow.prototype.branch = function(branch, opt_tick, opt_tickOpts) { if (this.reportSent_) { // Branch was called after the report was called. Trigger an error report. this.error_(jsaction.ActionFlow.Error.BRANCH, branch, opt_tick); } if (goog.DEBUG) { this.log_('> branch(' + branch + ':' + opt_tick + ')'); } if (opt_tick) { this.tick(opt_tick, opt_tickOpts); } if (this.branches_[branch]) { this.branches_[branch]++; } else { this.branches_[branch] = 1; } }; /** * Returns the current timers. Mostly for testing, but may become the * primary interface to obtain timers, and relegate reporting to a * library function. Note that the array is sorted by tick times. * @return {!Array} Timers. */ jsaction.ActionFlow.prototype.timers = function() { return this.timers_; }; /** * Returns the branchs registry. Mostly for testing. * @return {!Object} Branches. */ jsaction.ActionFlow.prototype.branches = function() { return this.branches_; }; /** * First triggers a BEFOREDONE event on this ActionFlow instance. This * can be used for example to add additional ticks to a ActionFlow * instance right before sending the report, or even to create a fresh * branch, in which case the event handler must cancel the event. * * If the BEFOREDONE event was not cancelled, sends the DONE event on * the ActionFlow class. Usually this is handled by the reporting code * of the application, which sends one or more reports to the server. * * The Event instance is shared between BEFOREDONE and DONE. * * @return {boolean} Whether the flow is really done and can be * disposed. * @private */ jsaction.ActionFlow.prototype.report_ = function() { if (!jsaction.ActionFlow.report) { return true; } if (this.abandoned_) { var event = new jsaction.ActionFlow.Event( jsaction.ActionFlow.EventType.ABANDONED, this); this.dispatchEvent(event); jsaction.ActionFlow.report.dispatchEvent(event); return true; } if (this.duplicateTicks_.getCount() > 0) { this.extraData_[jsaction.Name.DUP] = this.duplicateTicks_.getValues().join('|'); } event = new jsaction.ActionFlow.Event( jsaction.ActionFlow.EventType.BEFOREDONE, this); // BEFOREDONE fires on both the instance and the class. if (!this.dispatchEvent(event) || !jsaction.ActionFlow.report.dispatchEvent(event)) { return false; } // Must come after the BEFOREDONE event fires because event handlers // can add additional data. var cad = jsaction.ActionFlow.foldCadObject_(this.extraData_); if (cad) { this.actionData_[jsaction.UrlParam.CLICK_ADDITIONAL_DATA] = cad; } event.type = jsaction.ActionFlow.EventType.DONE; return jsaction.ActionFlow.report.dispatchEvent(event); }; /** * Triggers an error report if: * - data is added to the flow after it finished (e.g via tick(), * addExtraData(), etc) * - branch/done are called after the flow finished * - done is called on a branch that is not open * The error report will contain the timing data of the flow and the current * opened branches. If the error was triggered by an incorrect branch/done call * the name of the branch is passed in and included in the report as well. * * @param {jsaction.ActionFlow.Error} error The type of error that * triggered the report. * @param {string=} opt_branch If the error comes due to an incorrect * call to branch/done, this is the name of the branch. * @param {string=} opt_tick If the call that triggered the error has a tick * (i.e. tick()/branch()/done()) this is the name of the tick. * @private */ jsaction.ActionFlow.prototype.error_ = function(error, opt_branch, opt_tick) { if (!jsaction.ActionFlow.report) { return; } var event = new jsaction.ActionFlow.Event( jsaction.ActionFlow.EventType.ERROR, this); event.error = error; event.branch = opt_branch; event.tick = opt_tick; event.finished = this.reportSent_; jsaction.ActionFlow.report.dispatchEvent(event); }; /** * Folds a key-value data object into a string to be used as "cad" * URL parameter value. Keys and values are separated by colons, and * key-value pairs are separated by commas. Both keys and values * are escaped with encodeURIComponent to prevent them from having * unescaped separator characters. Empty data object will produce * empty string. * * Example: * "key1:value1,key2:value2" * * @param {Object.<string, string>} object Data object containing of key-value * pairs. Both key and value must be strings. * @return {string} The string representation of the object suitable * for "cad" URL parameter value. * @private */ jsaction.ActionFlow.foldCadObject_ = function(object) { var cadArray = []; goog.object.forEach(object, function(value, key) { var escKey = encodeURIComponent(key); // Don't escape '|' to make it a practical character to use as a separator // within the value. var escValue = encodeURIComponent(value).replace(/%7C/g, '|'); cadArray.push(escKey + jsaction.Char.CAD_KEY_VALUE_SEPARATOR + escValue); }); return cadArray.join(jsaction.Char.CAD_SEPARATOR); }; /** * Logs the tracking of jsactions, e.g. click event. It traverses the * DOM tree from the target element on which the action is initiated * upwards to the document.body, collects the values of the custom * attribute 'oi' attached on the nodes along the path, and then * concatenates them as a dotted string that is set to the URL * parameter 'oi' of the log request sent to MFE. When 'ved' custom * attribute is found in the DOM tree, it is set to the URL parameter * 'ved' of the log request. * * The log record will be created only if there is jstrack is * specified on the target element or up its DOM tree. If jstrack is * not "1", the value of jstrack is used as the log event ID. * * An example: for a DOM tree * <div jstrack="1"> * ... * <div oi="tag1"> * <div oi="tag2" jsaction="action2" jsinstance="x"></div> * </div> * ... * </div> * * @param {Element} target The DOM element the action is acted on. */ jsaction.ActionFlow.prototype.action = function(target) { if (this.reportSent_) { this.error_(jsaction.ActionFlow.Error.ACTION); } var ois = []; var jsinstance = null; var jstrack = null; var ved = null; var vet = null; jsaction.ActionFlow.visitDomNodesUpwards_(target, function(element) { var oi = jsaction.ActionFlow.getOi_(element); if (oi) { ois.unshift(oi); // Find the 1st node with the jsinstance attribute. if (!jsinstance) { jsinstance = element.getAttribute(jsaction.Attribute.JSINSTANCE); } } // We should not try to find a ved outside of the scope of the EventId we // found. If jstrack is present and different from '1', it is assumed to be // an EventId. Imagine the following case: // // <div jstrack=eventid1 ved=ved1> // <div jstrack=eventid2> // <div ved=ved2>Imagine we do not touch this div.</div> // <div jsaction=log.my_action>But we interact with this div.</div> // </div> // </div> // // In that case, we would report (eventid2, ved1), which is wrong because // ved1 is relative to eventid1, not eventid2. // As soon as we have found eventid2, we should stop looking for a ved. if (!ved && (!jstrack || jstrack == '1')) { ved = element.getAttribute(jsaction.Attribute.VED); } if (!vet) { vet = element.getAttribute(jsaction.Attribute.VET); } if (!jstrack) { jstrack = element.getAttribute(jsaction.Attribute.JSTRACK); } }); if (vet) { this.actionData_[jsaction.UrlParam.VISUAL_ELEMENT_TYPE] = vet; } // Record no other action data if we found no jstrack. if (!jstrack) { return; } this.actionData_[jsaction.UrlParam.CLICK_TYPE] = this.flowType_; if (ois.length > 0) { this.addExtraData( jsaction.Attribute.OI, ois.join(jsaction.Char.OI_SEPARATOR)); } if (jsinstance) { if (jsinstance.charAt(0) == jsaction.ActionFlow.TEMPLATE_LAST_OUTPUT_MARKER_) { jsinstance = parseInt(jsinstance.substr(1), 10); } else { jsinstance = parseInt(/** @type {string} */(jsinstance), 10); } this.actionData_[jsaction.UrlParam.CLICK_DATA] = jsinstance; } if (jstrack != '1') { // Use jstrack as the log event ID. this.actionData_[jsaction.UrlParam.EVENT_ID] = jstrack; } // A ved parameter only makes sense if we found a corresponding EventId in the // DOM. However, we always put it in the ActionData, so that we can detect the // issue and report it. if (ved) { this.actionData_[jsaction.UrlParam.VISUAL_ELEMENT_CLICK] = ved; } }; /** * Sets the event id action data field, if it is not already set. This is * useful for ActionFlows that do not originate from a DOM tree that has a * specified event id. * @param {string} ei The event id. */ jsaction.ActionFlow.prototype.maybeSetEventId = function(ei) { if (!this.actionData_[jsaction.UrlParam.EVENT_ID]) { this.actionData_[jsaction.UrlParam.EVENT_ID] = ei; } }; /** * Adds custom key-value pair to the action log record within * the cad parameter value. When the log record * is sent, the pairs are converted to a string of the form: * "key1:value1,key2:value2,...". * The key-value pairs will be added to the cad parameter value * in no particular order. * @see jsaction.ActionFlow#foldCadObject_ * * @param {string} key Key. * @param {string} value Value. */ jsaction.ActionFlow.prototype.addExtraData = function(key, value) { if (this.reportSent_) { this.error_(jsaction.ActionFlow.Error.EXTRA_DATA); } // Replace all deliminators ':', ':', and '," used by CAD with // underscores. Also replace white space with underscore. this.extraData_[key] = value.toString().replace(/[:;,\s]/g, '_'); }; /** * Gets the extra data as set by addExtraData(). * * @return {Object!} The extra data object. */ jsaction.ActionFlow.prototype.getExtraData = function() { return this.extraData_; }; /** * Gets the data collected by the call to action() from the * constructor. * * @return {Object!} The action data object. */ jsaction.ActionFlow.prototype.getActionData = function() { return this.actionData_; }; /** * Gets the data collected by the call to impression(). * * @return {Object!} The impression data object. */ jsaction.ActionFlow.prototype.getImpressionData = function() { return this.impressionData_; }; /** * Collects impression data when a jstemplate is rendered. It * traverses the DOM tree rooted from the target node downwards, * aggregates the number of nodes with the same hierarchical * impression key, and appends them to the 'imp' parameter of the log * request sent to MFE. * * An example: for a DOM tree * <div oi="tag1"> * <div oi="tag2" jsinstance="1"></div> * <div oi="tag2" jsinstance="*2"></div> * </div> * * when the template is rendered, the log request will be: * /maps/gen_204?imp=jsaction,tag1:1,tag1.tag2:2... * * @param {Element} target The DOM container element of the template. */ jsaction.ActionFlow.prototype.impression = function(target) { if (this.reportSent_) { this.error_(jsaction.ActionFlow.Error.IMPRESSION); } this.tick(jsaction.Tick.IMP0); var ois = []; if (target.parentNode) { jsaction.ActionFlow.visitDomNodesUpwards_( target.parentNode, function(element) { var oi = jsaction.ActionFlow.getOi_(element); if (oi) { ois.unshift(oi); } }); } var oiCounters = this.impressionData_; /** * The callback to be called when visiting a node. * * @param {Element} node The DOM node to be visited. * @return {boolean} Whether some cleanup shall be done on calling * context before leaving the node. */ var enterFn = function(node) { var oi = jsaction.ActionFlow.getOi_(node); if (oi) { ois.push(oi); var fullOi = ois.join(jsaction.Char.OI_SEPARATOR); if (!oiCounters[fullOi]) { oiCounters[fullOi] = 0; } oiCounters[fullOi]++; return true; } return false; }; /** * The callback to be called when leaving a node. */ var leaveFn = function() { ois.pop(); }; jsaction.ActionFlow.visitDomNodesDownwards_(target, enterFn, leaveFn); this.tick(jsaction.Tick.IMP1); }; /** * Checks whether the impression data whose tags match the given * pattern exist. * * @param {RegExp} pattern The regular expression to be matched with * impression tags. * @return {boolean} True if the impression data are not empty. */ jsaction.ActionFlow.prototype.hasImpression = function(pattern) { for (var tag in this.impressionData_) { if (tag.match(pattern)) { return true; } } return false; }; /** * Traverses the DOM tree from the start node upwards, and invokes the * callback provided on each node visited. Stops at document.body. * * @param {Node} start The node the traversal starts from. * @param {function(!Element)} visitFn The callback to be invoked on each * visited node. * @private */ jsaction.ActionFlow.visitDomNodesUpwards_ = function(start, visitFn) { for (var node = start; node && node.nodeType == goog.dom.NodeType.ELEMENT; node = node.parentNode) { visitFn(/** @type {!Element} */ (node)); } }; /** * Traverses the DOM tree from the start node downwards, and invokes * the callbacks provided when entering a node or leaving it. * * @param {Element} start The node the traversal starts from. * @param {Function} enterFn The callback to be invoked when visiting * a node. * @param {Function} leaveFn The callback to be invoked when tracing * back to the parent of a node. enterFn returns a boolean value that * indicates whether leaveFn must be invoked or not. * @private */ jsaction.ActionFlow.visitDomNodesDownwards_ = function(start, enterFn, leaveFn) { if (!goog.dom.isElement(start) || goog.style.getStyle(start, 'display') == 'none' || goog.style.getStyle(start, 'visibility') == 'hidden') { // Hidden elements are not counted as impressions. return; } var postCallbackNeeded = enterFn(start); for (var node = start.firstChild; node; node = node.nextSibling) { jsaction.ActionFlow.visitDomNodesDownwards_( /** @type {Element} */(node), enterFn, leaveFn); } if (postCallbackNeeded) { leaveFn(); } }; /** * Returns the value of the attribute 'oi' attached to the designated node. * * @param {Element} node The DOM node to be checked. * @return {?string} The value of the attribute 'oi'. * @private */ jsaction.ActionFlow.getOi_ = function(node) { if (!node[jsaction.Property.OI] && node.getAttribute) { node[jsaction.Property.OI] = node.getAttribute(jsaction.Attribute.OI); } return node[jsaction.Property.OI]; }; /** * Calls tick on provided flow object if it is defined. * * @param {jsaction.ActionFlow|undefined} flow The jsaction.ActionFlow object. * @param {string} tick The tick name. * @param {number=} opt_time The timestamp, if it's not goog.now(). * @param {Object=} opt_opts Options. See ActionFlow.tick for details. */ jsaction.ActionFlow.tick = function(flow, tick, opt_time, opt_opts) { if (flow) { var opts = opt_opts || {}; opts.time = opts.time || opt_time; // Technically we do not need to specify doNotReportToServer or // doNotIncludeMaxTime here since the default is false, but // jscompiler otherwise generates an error in tick() above about // the property being read but never set unless we set it // somewhere. So we set it here to silence that error. opts.doNotReportToServer = !!opts.doNotReportToServer; opts.doNotIncludeInMaxTime = !!opts.doNotIncludeInMaxTime; flow.tick(tick, opts); } }; /** * Calls branch on provided flow object if it is defined. * * @param {jsaction.ActionFlow|undefined} flow The jsaction.ActionFlow object. * @param {string} branch The name of the branch that is created. The * corresponding done() should use the same name to signal that the * branch has finished. * @param {string=} opt_tick The tick name. * @param {Object=} opt_tickOpts The options for the tick. */ jsaction.ActionFlow.branch = function(flow, branch, opt_tick, opt_tickOpts) { if (flow) { flow.branch(branch, opt_tick, opt_tickOpts); } }; /** * Calls done on provided flow object with optional tick if it is defined. * * @param {jsaction.ActionFlow|undefined} flow The jsaction.ActionFlow object. * @param {string} branch The name of the branch that ends. Closes the * flow opened by the branch() call with the same name. The * implicit branch in the constructor has a reserved name * (jsaction.Branch.MAIN). * @param {string} opt_tick The tick name. * @param {Object} opt_tickOpts The options for the tick. */ jsaction.ActionFlow.done = function(flow, branch, opt_tick, opt_tickOpts) { if (flow) { flow.done(branch, opt_tick, opt_tickOpts); } }; /** * Merges externally recorded flow ticks. The start time of the flow * is not changed ("start" tick is skipped.). * * @param {jsaction.ActionFlow} flow The ActionFlow to tick. * @param {Object} timers Timers as an associative container where each * attribute is a key/value pair of tick-label/ tick-time. All other ticks * except "start" tick will be imported into the flow's timers. */ jsaction.ActionFlow.merge = function(flow, timers) { if (!timers) { return; } goog.object.forEach(timers, function(value, name) { if (name != jsaction.Name.START) { flow.tick(name, { time: value }); } }); }; /** * Calls addExtraData on the given flow object if it is defined. * * @param {jsaction.ActionFlow|undefined} flow The jsaction.ActionFlow object. * @param {string} key The key to add. * @param {string} value The value for the given key. */ jsaction.ActionFlow.addExtraData = function(flow, key, value) { if (flow) { flow.addExtraData(key, value); } }; /** * Returns the flow type of the jsaction for which this flow was created. * @return {string} The flow type. */ jsaction.ActionFlow.prototype.flowType = function() { return this.unobfuscatedFlowType_; }; /** * Returns the namespace of the jsaction. * @return {string} The namespace. If the jsaction doesn't have a namespace, * the empty string. */ jsaction.ActionFlow.prototype.actionNamespace = function() { var type = this.unobfuscatedFlowType_; return type.substr(0, type.indexOf(jsaction.Char.NAMESPACE_ACTION_SEPARATOR)); }; /** * Returns a actionflow tracked callback that will call the given function and * done() on the action flow. Calls branch() with the given branch name. If * the optional ticks are supplied they will be called on branch() and done() * respectively. * * Example: * var myCallback = function() { * ... * }; * .... * setTimeout(flow.callback(myCallback, 'branchfoo', 'tick0', 'tick1'), 0); * * @param {!Function} fn The callback that we want to track with the current * actionflow. * @param {string} branchName The name of the branch to be opened before the * callback is used. The branch will be closed when the tracked callback * returned by this method is called. * @param {string=} opt_branchTick An optional tick to be called on branch. * @param {string=} opt_doneTick An optional tick to be called on done. * @return {!Function} The tracked callback. */ jsaction.ActionFlow.prototype.callback = function(fn, branchName, opt_branchTick, opt_doneTick) { this.branch(branchName, opt_branchTick); var flow = this; return function() { try { var ret = fn.apply(this, arguments); } finally { flow.done(branchName, opt_doneTick); } return ret; }; }; /** * Returns the node associated with this jsaction.ActionFlow. * * When a jsaction.ActionFlow created, the node is always set. The node is set * to null when the ActionFlow report is sent and should not be accessed * after that. * * In opt, this returns null if the node is not set. In debug, we * fail immediately. * * @return {Element} The node. */ jsaction.ActionFlow.prototype.node = function() { return this.node_; }; /** * Returns the event associated with this ActionFlow. * * When a jsaction.ActionFlow created, the event (copy) is always * set. The event is set to null when the ActionFlow report is sent and * should not be accessed after that. * * In opt, this returns null if the event is not set. In debug, we * fail immediately. * * @return {Event} The event. */ jsaction.ActionFlow.prototype.event = function() { return this.event_; }; /** * Returns the jsaction event type as specified in the jsaction attribute, * which may be different from the type obtained from the event. * * @return {?string} Event type. */ jsaction.ActionFlow.prototype.eventType = function() { return this.eventType_; }; /** * Returns values of properties or attributes stored on the node or * undefined if the node is not set. * @param {string} key The name of the property or attribute being * asked for. * @return {*} The value of the property or attribute. */ jsaction.ActionFlow.prototype.value = function(key) { var node = this.node_; return !node ? undefined : key in node ? node[key] : // HACK(user): The getAttribute check protects against gratuitous mocks. node.getAttribute ? node.getAttribute(key) : undefined; }; /** * Event detail object for all the events defined above. This object * contains the action flow instance that fired it. It's not the event * target, because the ActionFlow instances fires their events on * ActionFlow.report, where the application can actually listen for * them. * * The event handlers can inquiry the source ActionFlow instance for * the actual details. * * @param {jsaction.ActionFlow.EventType} type The type of event. * @param {!jsaction.ActionFlow} flow The instance that fires this event. * @constructor * @extends {goog.events.Event} */ jsaction.ActionFlow.Event = function(type, flow) { goog.events.Event.call(this, type, flow); this.flow = flow; }; goog.inherits(jsaction.ActionFlow.Event, goog.events.Event); /** * The ActionFlow instance that fired this event. This is also set as * target, but as flow it's properly typed. * @type {!jsaction.ActionFlow} */ jsaction.ActionFlow.Event.prototype.flow; /** * If type is ERROR, contains the error condition. * @type {(jsaction.ActionFlow.Error|undefined)} */ jsaction.ActionFlow.Event.prototype.error; /** * If type is ERROR, optionally contains the branch where the error * condition occurred. * @type {(string|undefined)} */ jsaction.ActionFlow.Event.prototype.branch; /** * If type is ERROR, optionally it contains the name of the tick that was being * recorded when the error occurred. * @type {(string|undefined)} */ jsaction.ActionFlow.Event.prototype.tick; /** * If type is error, includes whether the flow had finished when the error * occurred. * @type {boolean} */ jsaction.ActionFlow.Event.prototype.finished; /** * Events fired by ActionFlow instances. * @enum {string} */ jsaction.ActionFlow.EventType = { /** * Fired when a flow is created. This event cannot be canceled, and so the * return type of the handler is inconsequential. Because the event is * triggered inside the ActionFlow constructor, handlers will be called * synchronously with the new ActionFlow instance. Also because the triggering * happens inside the constructor, the event is only fired on * jsaction.ActionFlow.report. */ CREATED: 'created', /** * Fired when the flow is done and before the DONE event is * fired. If a handler cancels the default action, then no DONE * event is fired, and the ActionFlow is not disposed of. This must * happen if a beforedone handler calls branch(). */ BEFOREDONE: 'beforedone', /** * Fired when the flow is done and no BEFOREDONE handler cancelled * the event. */ DONE: 'done', /** * Fired when the flow is done if abandon() was called on the flow. * Neither BEFOREDONE nor DONE are fired for abandoned flows. */ ABANDONED: 'abandoned', /** * Fired whenever an error occurs. Can be handled even in production * to obtain error reports from deployed code. Specifically, it's * called when the following conditions ooccur: * * - branch/done/tick/addActionData/action/impression are called * after the flow finished, or * * - done called on a branch that is not pending. * * - an action flow client detects a suspected HUNG flow. */ ERROR: 'error' };