1515 */
1616
1717import { randomTraceId } from '@opencensus/web-core' ;
18+ import { AsyncTask } from './zone-types' ;
19+ import {
20+ OnPageInteractionStopwatch ,
21+ startOnPageInteraction ,
22+ } from './on-page-interaction' ;
1823
1924// Allows us to monkey patch Zone prototype without TS compiler errors.
2025declare const Zone : ZoneType & { prototype : Zone } ;
2126
22- export interface AsyncTaskData {
23- interactionId : string ;
24- pageView : string ;
25- }
26-
27- export type AsyncTask = Task & {
28- data : AsyncTaskData ;
29- eventName : string ;
30- target : HTMLElement ;
31- // Allows access to the private `_zone` property of a Zone.js Task.
32- _zone : Zone ;
33- } ;
34-
3527// Delay of 50 ms to reset currentEventTracingZone.
3628const RESET_TRACING_ZONE_DELAY = 50 ;
3729
3830export class InteractionTracker {
3931 // Allows to track several events triggered by the same user interaction in the right Zone.
4032 private currentEventTracingZone ?: Zone ;
4133
34+ // Map of interaction Ids to stopwatches.
35+ private readonly interactions : {
36+ [ index : string ] : OnPageInteractionStopwatch ;
37+ } = { } ;
38+
39+ // Map of interaction Ids to timeout ids.
40+ private readonly interactionCompletionTimeouts : {
41+ [ index : string ] : number ;
42+ } = { } ;
43+
4244 constructor ( ) {
4345 const runTask = Zone . prototype . runTask ;
4446 Zone . prototype . runTask = (
4547 task : AsyncTask ,
4648 applyThis : unknown ,
4749 applyArgs : unknown
4850 ) => {
49- const time = Date . now ( ) ;
50-
5151 console . warn ( 'Running task' ) ;
5252 console . log ( task ) ;
5353 console . log ( task . zone ) ;
5454
55+ const interceptingElement = getTrackedElement ( task ) ;
5556 let taskZone = Zone . current ;
56- if ( isTrackedElement ( task ) ) {
57+ if ( interceptingElement ) {
5758 console . log ( 'Click detected' ) ;
58-
59- if ( this . currentEventTracingZone === undefined ) {
59+ if ( this . currentEventTracingZone === undefined ) {
6060 const traceId = randomTraceId ( ) ;
6161 this . currentEventTracingZone = Zone . root . fork ( {
6262 name : traceId ,
@@ -66,6 +66,11 @@ export class InteractionTracker {
6666 } ,
6767 } ) ;
6868
69+ this . interactions [ traceId ] = startOnPageInteraction ( {
70+ id : traceId ,
71+ eventType : task . eventName ,
72+ target : task . target ,
73+ } ) ;
6974 // Timeout to reset currentEventTracingZone to allow the creation of a new
7075 // zone for a new user interaction.
7176 Zone . root . run ( ( ) =>
@@ -82,6 +87,7 @@ export class InteractionTracker {
8287 // Change the zone task.
8388 task . _zone = this . currentEventTracingZone ;
8489 taskZone = this . currentEventTracingZone ;
90+ this . incrementTaskCount ( task . zone . get ( 'traceId' ) ) ;
8591 } else if ( task . zone && task . zone . get ( 'isTracingZone' ) ) {
8692 // If we already are in a tracing zone, just run the task in our tracing zone.
8793 taskZone = task . zone ;
@@ -90,43 +96,144 @@ export class InteractionTracker {
9096 return runTask . call ( taskZone as { } , task , applyThis , applyArgs ) ;
9197 } finally {
9298 console . log ( 'Run task finished.' ) ;
93- console . log ( 'Time to complete: ' + ( Date . now ( ) - time ) ) ;
99+ if (
100+ interceptingElement ||
101+ ( shouldCountTask ( task ) && isTrackedTask ( task ) )
102+ ) {
103+ this . decrementTaskCount ( task . zone . get ( 'traceId' ) ) ;
104+ }
94105 }
95106 } ;
96107
97108 const scheduleTask = Zone . prototype . scheduleTask ;
98- Zone . prototype . scheduleTask = function < T extends Task > ( task : T ) : T {
109+ Zone . prototype . scheduleTask = < T extends Task > ( task : T ) => {
99110 console . warn ( 'Scheduling task' ) ;
100111 console . log ( task ) ;
101112
102- let taskZone : Zone = this ;
103- if ( task . zone && task . zone && task . zone . get ( 'isTracingZone' ) ) {
113+ let taskZone = Zone . current ;
114+ if ( isTrackedTask ( task ) ) {
104115 taskZone = task . zone ;
105116 }
106117 try {
107118 return scheduleTask . call ( taskZone as { } , task ) as T ;
108119 } finally {
120+ if ( shouldCountTask ( task ) && isTrackedTask ( task ) ) {
121+ this . incrementTaskCount ( task . zone . get ( 'traceId' ) ) ;
122+ }
123+ console . warn ( 'Finished Scheduling task' ) ;
109124 }
110125 } ;
111126
112127 const cancelTask = Zone . prototype . cancelTask ;
113- Zone . prototype . cancelTask = function ( task : AsyncTask ) {
128+ Zone . prototype . cancelTask = ( task : AsyncTask ) => {
114129 console . warn ( 'Cancel task' ) ;
115130 console . log ( task ) ;
116131
117- let taskZone : Zone = this ;
118- if ( task . zone && task . zone . get ( 'isTracingZone' ) ) {
132+ let taskZone = Zone . current ;
133+ if ( isTrackedTask ( task ) ) {
119134 taskZone = task . zone ;
120135 }
121136
122137 try {
123138 return cancelTask . call ( taskZone as { } , task ) ;
124139 } finally {
140+ if ( isTrackedTask ( task ) && shouldCountTask ( task ) ) {
141+ this . decrementTaskCount ( task . zone . get ( 'traceId' ) ) ;
142+ }
143+ console . warn ( 'Finished cancel task' ) ;
125144 }
126145 } ;
127146 }
147+
148+ /** Increments the count of outstanding tasks for a given interaction id. */
149+ private incrementTaskCount ( interactionId : string ) {
150+ const stopWatch = this . getStopwatch ( interactionId ) ;
151+ if ( ! stopWatch ) return ;
152+ stopWatch . incrementTaskCount ( ) ;
153+
154+ if ( interactionId in this . interactionCompletionTimeouts ) {
155+ // Clear the task that is supposed to complete the interaction as there are new
156+ // tasks incrementing the task cout. Sometimes the task count might be 0
157+ // but the interaction has more scheduled tasks.
158+ Zone . root . run ( ( ) => {
159+ clearTimeout ( this . interactionCompletionTimeouts [ interactionId ] ) ;
160+ delete this . interactionCompletionTimeouts [ interactionId ] ;
161+ } ) ;
162+ }
163+ }
164+
165+ /** Decrements the count of outstanding tasks for a given interaction id. */
166+ private decrementTaskCount ( interactionId : string ) {
167+ const stopWatch = this . getStopwatch ( interactionId ) ;
168+ if ( ! stopWatch ) return ;
169+ stopWatch . decrementTaskCount ( ) ;
170+
171+ if ( ! stopWatch . hasRemainingTasks ( ) ) {
172+ this . maybeCompleteInteraction ( interactionId ) ;
173+ }
174+ }
175+
176+ private getStopwatch (
177+ interactionId : string
178+ ) : OnPageInteractionStopwatch | undefined {
179+ if ( ! ( interactionId in this . interactions ) ) return ;
180+ return this . interactions [ interactionId ] ;
181+ }
182+
183+ /**
184+ * Instead of declaring an interaction to be complete when the number of
185+ * active interactions reaches 0 we add a task to the queue that will actually
186+ * complete the interaction if none of the tasks scheduled ahead of it try
187+ * and increment the task counter for the given interaction id.
188+ */
189+ private maybeCompleteInteraction ( interactionId : string ) {
190+ const stopWatch = this . getStopwatch ( interactionId ) ;
191+ if ( ! stopWatch ) return ;
192+
193+ if ( this . interactionCompletionTimeouts [ interactionId ] !== undefined ) return ;
194+
195+ // Add a task to the queue that will actually complete the interaction in case
196+ // there are no more scheduled tasks ahead it.
197+ Zone . root . run ( ( ) => {
198+ this . interactionCompletionTimeouts [ interactionId ] = setTimeout ( ( ) => {
199+ this . completeInteraction ( interactionId ) ;
200+ delete this . interactionCompletionTimeouts [ interactionId ] ;
201+ } ) ;
202+ } ) ;
203+ }
204+
205+ private completeInteraction ( interactionId : string ) {
206+ const stopWatch = this . getStopwatch ( interactionId ) ;
207+ if ( ! stopWatch ) return ;
208+ stopWatch . stopAndRecord ( ) ;
209+ delete this . interactions [ interactionId ] ;
210+ }
211+ }
212+
213+ function getTrackedElement ( task : AsyncTask ) : HTMLElement | null {
214+ if ( ! ( task . eventName && task . eventName === 'click' ) ) return null ;
215+
216+ return task . target as HTMLElement ;
128217}
129218
130- function isTrackedElement ( task : AsyncTask ) : boolean {
131- return ! ! ( task . eventName && task . eventName === 'click' ) ;
219+ /**
220+ * Whether or not a task is being tracked as part of an interaction.
221+ */
222+ function isTrackedTask ( task : Task ) : boolean {
223+ return ! ! ( task . zone && task . zone . get ( 'isTracingZone' ) ) ;
224+ }
225+
226+ /**
227+ * Whether or not a task should be tracked as part of an interaction.
228+ */
229+ function shouldCountTask ( task : Task ) : boolean {
230+ if ( ! task . data ) return false ;
231+
232+ // Don't count periodic tasks with a delay greater than 1 s.
233+ if ( task . data . isPeriodic && ( task . data . delay && task . data . delay >= 1000 ) ) {
234+ return false ;
235+ }
236+
237+ // We're only interested in macroTasks and microTasks.
238+ return task . type === 'macroTask' || task . type === 'microTask' ;
132239}
0 commit comments