Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 76fbd7c

Browse files
crdgonzalezcadraffensperger
authored andcommitted
Use CSS selector to name the interaction (#143)
1 parent 423b608 commit 76fbd7c

8 files changed

Lines changed: 272 additions & 137 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright 2019, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { tracing } from '@opencensus/web-core';
18+
import { isRootSpanNameReplaceable } from './util';
19+
20+
/**
21+
* Monkey-patch `History API` to detect route transitions. This is necessary
22+
* because there might be some cases when there are several interactions being
23+
* tracked at the same time but if there is an user interaction that triggers a
24+
* route transition while those interactions are still in tracking, only that
25+
* interaction will have a `Navigation` name. Otherwise, if this is not patched
26+
* the other interactions will change the name to `Navigation` even if they did
27+
* not cause the route transition.
28+
*/
29+
export function patchHistoryApi() {
30+
const pushState = history.pushState;
31+
history.pushState = (
32+
data: unknown,
33+
title: string,
34+
url?: string | null | undefined
35+
) => {
36+
patchHistoryApiMethod(pushState, data, title, url);
37+
};
38+
39+
const replaceState = history.replaceState;
40+
history.replaceState = (
41+
data: unknown,
42+
title: string,
43+
url?: string | null | undefined
44+
) => {
45+
patchHistoryApiMethod(replaceState, data, title, url);
46+
};
47+
48+
const back = history.back;
49+
history.back = () => {
50+
patchHistoryApiMethod(back);
51+
};
52+
53+
const forward = history.forward;
54+
history.forward = () => {
55+
patchHistoryApiMethod(forward);
56+
};
57+
58+
const go = history.go;
59+
history.go = (delta?: number) => {
60+
patchHistoryApiMethod(go, delta);
61+
};
62+
63+
const patchHistoryApiMethod = (func: Function, ...args: unknown[]) => {
64+
// Store the location.pathname before it changes calling `func`.
65+
const currentPathname = location.pathname;
66+
func.call(history, ...args);
67+
maybeUpdateInteractionName(currentPathname);
68+
};
69+
}
70+
71+
function maybeUpdateInteractionName(previousLocationPathname: string) {
72+
const rootSpan = tracing.tracer.currentRootSpan;
73+
// If for this interaction, the developer did not give any explicit
74+
// attibute (`data-ocweb-id`) and the generated name can be replaced,
75+
// that means the name might change to `Navigation <pathname>` as this is a
76+
// more understadable name for the interaction in case the location
77+
// pathname actually changed.
78+
if (
79+
rootSpan &&
80+
isRootSpanNameReplaceable(Zone.current) &&
81+
previousLocationPathname !== location.pathname
82+
) {
83+
rootSpan.name = 'Navigation ' + location.pathname;
84+
}
85+
}

packages/opencensus-web-instrumentation-zone/src/interaction-tracker.ts

Lines changed: 8 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import {
2020
SpanKind,
2121
RootSpan,
2222
} from '@opencensus/web-core';
23-
import { AsyncTask } from './zone-types';
23+
import { AsyncTask, InteractionName } from './zone-types';
2424
import {
2525
OnPageInteractionStopwatch,
2626
startOnPageInteraction,
2727
} from './on-page-interaction-stop-watch';
2828

29-
import { isTrackedTask } from './util';
29+
import { isTrackedTask, getTraceId, resolveInteractionName } from './util';
3030
import { interceptXhrTask } from './xhr-interceptor';
3131

3232
// Allows us to monkey patch Zone prototype without TS compiler errors.
@@ -59,7 +59,6 @@ export class InteractionTracker {
5959
this.patchZoneRunTask();
6060
this.patchZoneScheduleTask();
6161
this.patchZoneCancelTask();
62-
this.patchHistoryApi();
6362
}
6463

6564
static startTracking(): void {
@@ -147,11 +146,11 @@ export class InteractionTracker {
147146
interceptingElement: HTMLElement,
148147
eventName: string,
149148
taskZone: Zone,
150-
interactionName: string
149+
interactionName: InteractionName
151150
) {
152151
const traceId = randomTraceId();
153152
const spanOptions = {
154-
name: interactionName,
153+
name: interactionName.name,
155154
spanContext: {
156155
traceId,
157156
// This becomes the parentSpanId field of the root span, and the actual
@@ -168,6 +167,8 @@ export class InteractionTracker {
168167
// to capture the new zone, also, start the `OnPageInteraction` to capture the
169168
// new root span.
170169
this.currentEventTracingZone = Zone.current;
170+
this.currentEventTracingZone.get('data')['isRootSpanNameReplaceable'] =
171+
interactionName.isReplaceable;
171172
this.interactions[traceId] = startOnPageInteraction({
172173
startLocationHref: location.href,
173174
startLocationPath: location.pathname,
@@ -266,81 +267,6 @@ export class InteractionTracker {
266267
stopWatch.stopAndRecord();
267268
delete this.interactions[interactionId];
268269
}
269-
270-
// Monkey-patch `History API` to detect route transitions.
271-
// This is necessary because there might be some cases when
272-
// there are several interactions being tracked at the same time
273-
// but if there is an user interaction that triggers a route transition
274-
// while those interactions are still in tracking, only that interaction
275-
// will have a `Navigation` name. Otherwise, if this is not patched, the
276-
// other interactions will change the name to `Navigation` even if they
277-
// did not cause the route transition.
278-
private patchHistoryApi() {
279-
const pushState = history.pushState;
280-
history.pushState = (
281-
data: unknown,
282-
title: string,
283-
url?: string | null | undefined
284-
) => {
285-
patchHistoryApiMethod(pushState, data, title, url);
286-
};
287-
288-
const replaceState = history.replaceState;
289-
history.replaceState = (
290-
data: unknown,
291-
title: string,
292-
url?: string | null | undefined
293-
) => {
294-
patchHistoryApiMethod(replaceState, data, title, url);
295-
};
296-
297-
const back = history.back;
298-
history.back = () => {
299-
patchHistoryApiMethod(back);
300-
};
301-
302-
const forward = history.forward;
303-
history.forward = () => {
304-
patchHistoryApiMethod(forward);
305-
};
306-
307-
const go = history.go;
308-
history.go = (delta?: number) => {
309-
patchHistoryApiMethod(go, delta);
310-
};
311-
312-
const patchHistoryApiMethod = (func: Function, ...args: unknown[]) => {
313-
// Store the location.pathname before it changes calling `func`.
314-
const currentPathname = location.pathname;
315-
func.call(history, ...args);
316-
this.maybeUpdateInteractionName(currentPathname);
317-
};
318-
}
319-
320-
private maybeUpdateInteractionName(previousLocationPathname: string) {
321-
const rootSpan = tracing.tracer.currentRootSpan;
322-
// If for this interaction, the developer did not give any
323-
// explicit attibute (`data-ocweb-id`) the current interaction
324-
// name will start with a '<' that stands to the tag name. If that is
325-
// the case, change the name to `Navigation <pathname>` as this is a more
326-
// understadable name for the interaction.
327-
// Also, we check if the location pathname did change.
328-
if (
329-
rootSpan &&
330-
rootSpan.name.startsWith('<') &&
331-
previousLocationPathname !== location.pathname
332-
) {
333-
rootSpan.name = 'Navigation ' + location.pathname;
334-
}
335-
}
336-
}
337-
338-
/**
339-
* Get the trace ID from the zone properties.
340-
* @param zone
341-
*/
342-
function getTraceId(zone: Zone): string {
343-
return zone && zone.get('data') ? zone.get('data').traceId : '';
344270
}
345271

346272
function getTrackedElement(task: AsyncTask): HTMLElement | null {
@@ -349,39 +275,6 @@ function getTrackedElement(task: AsyncTask): HTMLElement | null {
349275
return task.target as HTMLElement;
350276
}
351277

352-
/**
353-
* Look for 'data-ocweb-id' attibute in the HTMLElement in order to
354-
* give a name to the user interaction and Root span. If this attibute is
355-
* not present, use the element ID, tag name, event that triggered the interaction.
356-
* Thus, the resulting interaction name will be: "tag_name> id:'ID' event"
357-
* (e.g. "<BUTTON> id:'save_changes' click").
358-
* In case the the name is not resolvable, return empty string (e.g. element is the document).
359-
* @param element
360-
*/
361-
function resolveInteractionName(
362-
element: HTMLElement | null,
363-
eventName: string
364-
): string {
365-
if (!element) return '';
366-
if (!element.getAttribute) return '';
367-
if (element.hasAttribute('disabled')) {
368-
return '';
369-
}
370-
let interactionName = element.getAttribute('data-ocweb-id');
371-
if (!interactionName) {
372-
const elementId = element.getAttribute('id') || '';
373-
const tagName = element.tagName;
374-
if (!tagName) return '';
375-
interactionName =
376-
'<' +
377-
tagName +
378-
'>' +
379-
(elementId ? " id:'" + elementId + "' " : '') +
380-
eventName;
381-
}
382-
return interactionName;
383-
}
384-
385278
/**
386279
* Whether or not a task should be tracked as part of an interaction.
387280
*/
@@ -394,8 +287,8 @@ function shouldCountTask(task: Task): boolean {
394287
// This case only applies for `setInterval` as we support `setTimeout`.
395288
// TODO: ideally OpenCensus Web can manage this kind of tasks, so for example
396289
// if a periodic task ends up doing some work in the future it will still
397-
// be associated with that same older tracing zone. This is something we have to
398-
// think of.
290+
// be associated with that same older tracing zone. This is something we have
291+
// to think of.
399292
if (task.data.isPeriodic) return false;
400293

401294
// We're only interested in macroTasks and microTasks.

packages/opencensus-web-instrumentation-zone/src/monkey-patching.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
import { XhrWithOcWebData } from './zone-types';
17+
import { patchHistoryApi } from './history-api-patch';
1818

1919
export function doPatching() {
20+
patchHistoryApi();
2021
patchXmlHttpRequestOpen();
2122
patchXmlHttpRequestSend();
2223
}

packages/opencensus-web-instrumentation-zone/src/util.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WindowWithOcwGlobals } from './zone-types';
17+
import { WindowWithOcwGlobals, InteractionName } from './zone-types';
1818
import { parseUrl } from '@opencensus/web-core';
1919

2020
/** Check that the trace */
@@ -38,3 +38,54 @@ export function isTrackedTask(task: Task): boolean {
3838
task.zone.get('data').isTracingZone
3939
);
4040
}
41+
42+
/**
43+
* Get the trace ID from the zone properties.
44+
*/
45+
export function getTraceId(zone: Zone): string {
46+
return zone && zone.get('data') ? zone.get('data').traceId : '';
47+
}
48+
49+
/**
50+
* Get the trace ID from the zone properties.
51+
*/
52+
export function isRootSpanNameReplaceable(zone: Zone): boolean {
53+
return zone && zone.get('data')
54+
? zone.get('data').isRootSpanNameReplaceable
55+
: false;
56+
}
57+
58+
/**
59+
* Look for 'data-ocweb-id' attibute in the HTMLElement in order to
60+
* give a name to the user interaction and Root span. If this attibute is
61+
* not present, use the element ID, tag name and event name, generating a CSS
62+
* selector. In this case, also mark the interaction name as replaceable.
63+
* Thus, the resulting interaction name will be: "<tag_name>#id event_name"
64+
* (e.g. "button#save_changes click").
65+
* In case the name is not resolvable, return undefined (e.g. element is the
66+
* `document`).
67+
* @param element
68+
*/
69+
export function resolveInteractionName(
70+
element: HTMLElement | null,
71+
eventName: string
72+
): InteractionName | undefined {
73+
if (!element) return undefined;
74+
if (!element.getAttribute) return undefined;
75+
if (element.hasAttribute('disabled')) {
76+
return undefined;
77+
}
78+
let interactionName = element.getAttribute('data-ocweb-id');
79+
let nameCanChange = false;
80+
if (!interactionName) {
81+
const elementId = element.getAttribute('id') || '';
82+
const tagName = element.tagName;
83+
if (!tagName) return undefined;
84+
nameCanChange = true;
85+
interactionName =
86+
tagName.toLowerCase() +
87+
(elementId ? '#' + elementId : '') +
88+
(eventName ? ' ' + eventName : '');
89+
}
90+
return { name: interactionName, isReplaceable: nameCanChange };
91+
}

packages/opencensus-web-instrumentation-zone/src/zone-types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,14 @@ export interface XhrPerformanceResourceTiming {
8282
corsPreFlightRequest?: PerformanceResourceTiming;
8383
mainRequest: PerformanceResourceTiming;
8484
}
85+
86+
/**
87+
* Type to allow the interaction tracker know whether the interaction name can
88+
* change or not. As part of naming the interaction, the name could be given
89+
* using the `data-ocweb-id` attribute or as a CSS selector, however when there
90+
* are route transitions, the name might change to `Navigation URL`.
91+
*/
92+
export interface InteractionName {
93+
name: string;
94+
isReplaceable: boolean;
95+
}

packages/opencensus-web-instrumentation-zone/test/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
import './test-on-page-interaction-stop-watch';
2020
import './test-interaction-tracker';
2121
import './test-perf-resource-timing-selector';
22+
import './test-util';

0 commit comments

Comments
 (0)