This repository has been archived by the owner on May 16, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 66
/
actionflow.js
1423 lines (1226 loc) · 41.3 KB
/
actionflow.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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('jsaction.Attribute');
goog.require('jsaction.Branch');
goog.require('jsaction.Char');
goog.require('jsaction.Name');
goog.require('jsaction.Property');
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".
* @param {!Element=} opt_target The event target
* @constructor
* @extends {goog.events.EventTarget}
*/
jsaction.ActionFlow = function(
flowType, opt_node, opt_event, opt_startTime, opt_eventType, opt_target) {
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 target of the event.
* @type {?Element}
* @private
*/
this.target_ = opt_target || null;
if (!this.target_ && opt_event && opt_event.target &&
goog.dom.isElement(opt_event.target)) {
this.target_ = /** @type {!Element} */ (opt_event.target);
}
/**
* 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.
* @const {!Object}
* @private
*/
this.duplicateTicks_ = {};
/**
* 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_ = {};
/**
* 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;
/**
* A flag that indicates if the action is from a wiz controller, false if it
* is from a reactive controller or native event.
* @type {boolean}
* @private
*/
this.isWiz_ = 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.
const evt = new jsaction.ActionFlow.Event(
jsaction.ActionFlow.EventType.CREATED, this);
if (jsaction.ActionFlow.report != null) {
jsaction.ActionFlow.report.dispatchEvent(evt);
}
};
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.
*/
jsaction.ActionFlow.ENABLE_GENERIC_EVENT_TRACKING =
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',
/**
* 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 (let i = 0; i < jsaction.ActionFlow.LOG_FOR_FLOW_TYPES.length; i++) {
const 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;
};
/**
* Mark this flow wraps a wiz event.
*/
jsaction.ActionFlow.prototype.setWiz = function() {
this.isWiz_ = 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() {
const tickNames = [];
tickNames.push(jsaction.Name.START);
for (let 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 || timers[jsaction.Name.START] === undefined) {
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 {?jsaction.ActionFlow.TickOptions=} opt_opts Options.
*/
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_[name] = true;
}
const 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;
}
const t = time - this.start_;
let 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_) {
const evt = new jsaction.ActionFlow.Event(
jsaction.ActionFlow.EventType.ABANDONED, this);
this.dispatchEvent(evt);
jsaction.ActionFlow.report.dispatchEvent(evt);
return true;
}
let sep = '';
let dup = '';
for (let k in this.duplicateTicks_) {
if (this.duplicateTicks_.hasOwnProperty(k)) {
dup = dup + sep + k;
sep = '|';
}
}
if (dup) {
this.extraData_[jsaction.Name.DUP] = dup;
}
const evt = new jsaction.ActionFlow.Event(
jsaction.ActionFlow.EventType.BEFOREDONE, this);
// BEFOREDONE fires on both the instance and the class.
if (!this.dispatchEvent(evt) ||
!jsaction.ActionFlow.report.dispatchEvent(evt)) {
return false;
}
// Must come after the BEFOREDONE event fires because event handlers
// can add additional data.
const cad = jsaction.ActionFlow.foldCadObject_(this.extraData_);
if (cad) {
this.actionData_[jsaction.UrlParam.CLICK_ADDITIONAL_DATA] = cad;
}
evt.type = jsaction.ActionFlow.EventType.DONE;
return jsaction.ActionFlow.report.dispatchEvent(evt);
};
/**
* 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;
}
const evt = new jsaction.ActionFlow.Event(
jsaction.ActionFlow.EventType.ERROR, this);
evt.error = error;
evt.branch = opt_branch;
evt.tick = opt_tick;
evt.finished = this.reportSent_;
jsaction.ActionFlow.report.dispatchEvent(evt);
};
/**
* 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) {
const cadArray = [];
goog.object.forEach(object, function(value, key) {
const escKey = encodeURIComponent(key);
// Don't escape '|' to make it a practical character to use as a separator
// within the value.
const 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);
}
const ois = [];
let jsinstance = null;
let jstrack = null;
let ved = null;
let vet = null;
jsaction.ActionFlow.visitDomNodesUpwards_(target, function(element) {
const 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_;
};
/**
* Traverses the DOM tree from the start node upwards, and invokes the
* callback provided on each node visited. Stops at document.body.
*