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

Commit

Permalink
Use CSS selector to name the interaction (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
crdgonzalezca authored and draffensperger committed Jul 24, 2019
1 parent 423b608 commit 76fbd7c
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright 2019, OpenCensus Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { tracing } from '@opencensus/web-core';
import { isRootSpanNameReplaceable } from './util';

/**
* Monkey-patch `History API` to detect route transitions. This is necessary
* because there might be some cases when there are several interactions being
* tracked at the same time but if there is an user interaction that triggers a
* route transition while those interactions are still in tracking, only that
* interaction will have a `Navigation` name. Otherwise, if this is not patched
* the other interactions will change the name to `Navigation` even if they did
* not cause the route transition.
*/
export function patchHistoryApi() {
const pushState = history.pushState;
history.pushState = (
data: unknown,
title: string,
url?: string | null | undefined
) => {
patchHistoryApiMethod(pushState, data, title, url);
};

const replaceState = history.replaceState;
history.replaceState = (
data: unknown,
title: string,
url?: string | null | undefined
) => {
patchHistoryApiMethod(replaceState, data, title, url);
};

const back = history.back;
history.back = () => {
patchHistoryApiMethod(back);
};

const forward = history.forward;
history.forward = () => {
patchHistoryApiMethod(forward);
};

const go = history.go;
history.go = (delta?: number) => {
patchHistoryApiMethod(go, delta);
};

const patchHistoryApiMethod = (func: Function, ...args: unknown[]) => {
// Store the location.pathname before it changes calling `func`.
const currentPathname = location.pathname;
func.call(history, ...args);
maybeUpdateInteractionName(currentPathname);
};
}

function maybeUpdateInteractionName(previousLocationPathname: string) {
const rootSpan = tracing.tracer.currentRootSpan;
// If for this interaction, the developer did not give any explicit
// attibute (`data-ocweb-id`) and the generated name can be replaced,
// that means the name might change to `Navigation <pathname>` as this is a
// more understadable name for the interaction in case the location
// pathname actually changed.
if (
rootSpan &&
isRootSpanNameReplaceable(Zone.current) &&
previousLocationPathname !== location.pathname
) {
rootSpan.name = 'Navigation ' + location.pathname;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import {
SpanKind,
RootSpan,
} from '@opencensus/web-core';
import { AsyncTask } from './zone-types';
import { AsyncTask, InteractionName } from './zone-types';
import {
OnPageInteractionStopwatch,
startOnPageInteraction,
} from './on-page-interaction-stop-watch';

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

// Allows us to monkey patch Zone prototype without TS compiler errors.
Expand Down Expand Up @@ -59,7 +59,6 @@ export class InteractionTracker {
this.patchZoneRunTask();
this.patchZoneScheduleTask();
this.patchZoneCancelTask();
this.patchHistoryApi();
}

static startTracking(): void {
Expand Down Expand Up @@ -147,11 +146,11 @@ export class InteractionTracker {
interceptingElement: HTMLElement,
eventName: string,
taskZone: Zone,
interactionName: string
interactionName: InteractionName
) {
const traceId = randomTraceId();
const spanOptions = {
name: interactionName,
name: interactionName.name,
spanContext: {
traceId,
// This becomes the parentSpanId field of the root span, and the actual
Expand All @@ -168,6 +167,8 @@ export class InteractionTracker {
// to capture the new zone, also, start the `OnPageInteraction` to capture the
// new root span.
this.currentEventTracingZone = Zone.current;
this.currentEventTracingZone.get('data')['isRootSpanNameReplaceable'] =
interactionName.isReplaceable;
this.interactions[traceId] = startOnPageInteraction({
startLocationHref: location.href,
startLocationPath: location.pathname,
Expand Down Expand Up @@ -266,81 +267,6 @@ export class InteractionTracker {
stopWatch.stopAndRecord();
delete this.interactions[interactionId];
}

// Monkey-patch `History API` to detect route transitions.
// This is necessary because there might be some cases when
// there are several interactions being tracked at the same time
// but if there is an user interaction that triggers a route transition
// while those interactions are still in tracking, only that interaction
// will have a `Navigation` name. Otherwise, if this is not patched, the
// other interactions will change the name to `Navigation` even if they
// did not cause the route transition.
private patchHistoryApi() {
const pushState = history.pushState;
history.pushState = (
data: unknown,
title: string,
url?: string | null | undefined
) => {
patchHistoryApiMethod(pushState, data, title, url);
};

const replaceState = history.replaceState;
history.replaceState = (
data: unknown,
title: string,
url?: string | null | undefined
) => {
patchHistoryApiMethod(replaceState, data, title, url);
};

const back = history.back;
history.back = () => {
patchHistoryApiMethod(back);
};

const forward = history.forward;
history.forward = () => {
patchHistoryApiMethod(forward);
};

const go = history.go;
history.go = (delta?: number) => {
patchHistoryApiMethod(go, delta);
};

const patchHistoryApiMethod = (func: Function, ...args: unknown[]) => {
// Store the location.pathname before it changes calling `func`.
const currentPathname = location.pathname;
func.call(history, ...args);
this.maybeUpdateInteractionName(currentPathname);
};
}

private maybeUpdateInteractionName(previousLocationPathname: string) {
const rootSpan = tracing.tracer.currentRootSpan;
// If for this interaction, the developer did not give any
// explicit attibute (`data-ocweb-id`) the current interaction
// name will start with a '<' that stands to the tag name. If that is
// the case, change the name to `Navigation <pathname>` as this is a more
// understadable name for the interaction.
// Also, we check if the location pathname did change.
if (
rootSpan &&
rootSpan.name.startsWith('<') &&
previousLocationPathname !== location.pathname
) {
rootSpan.name = 'Navigation ' + location.pathname;
}
}
}

/**
* Get the trace ID from the zone properties.
* @param zone
*/
function getTraceId(zone: Zone): string {
return zone && zone.get('data') ? zone.get('data').traceId : '';
}

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

/**
* Look for 'data-ocweb-id' attibute in the HTMLElement in order to
* give a name to the user interaction and Root span. If this attibute is
* not present, use the element ID, tag name, event that triggered the interaction.
* Thus, the resulting interaction name will be: "tag_name> id:'ID' event"
* (e.g. "<BUTTON> id:'save_changes' click").
* In case the the name is not resolvable, return empty string (e.g. element is the document).
* @param element
*/
function resolveInteractionName(
element: HTMLElement | null,
eventName: string
): string {
if (!element) return '';
if (!element.getAttribute) return '';
if (element.hasAttribute('disabled')) {
return '';
}
let interactionName = element.getAttribute('data-ocweb-id');
if (!interactionName) {
const elementId = element.getAttribute('id') || '';
const tagName = element.tagName;
if (!tagName) return '';
interactionName =
'<' +
tagName +
'>' +
(elementId ? " id:'" + elementId + "' " : '') +
eventName;
}
return interactionName;
}

/**
* Whether or not a task should be tracked as part of an interaction.
*/
Expand All @@ -394,8 +287,8 @@ function shouldCountTask(task: Task): boolean {
// This case only applies for `setInterval` as we support `setTimeout`.
// TODO: ideally OpenCensus Web can manage this kind of tasks, so for example
// if a periodic task ends up doing some work in the future it will still
// be associated with that same older tracing zone. This is something we have to
// think of.
// be associated with that same older tracing zone. This is something we have
// to think of.
if (task.data.isPeriodic) return false;

// We're only interested in macroTasks and microTasks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { XhrWithOcWebData } from './zone-types';
import { patchHistoryApi } from './history-api-patch';

export function doPatching() {
patchHistoryApi();
patchXmlHttpRequestOpen();
patchXmlHttpRequestSend();
}
Expand Down
53 changes: 52 additions & 1 deletion packages/opencensus-web-instrumentation-zone/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

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

/** Check that the trace */
Expand All @@ -38,3 +38,54 @@ export function isTrackedTask(task: Task): boolean {
task.zone.get('data').isTracingZone
);
}

/**
* Get the trace ID from the zone properties.
*/
export function getTraceId(zone: Zone): string {
return zone && zone.get('data') ? zone.get('data').traceId : '';
}

/**
* Get the trace ID from the zone properties.
*/
export function isRootSpanNameReplaceable(zone: Zone): boolean {
return zone && zone.get('data')
? zone.get('data').isRootSpanNameReplaceable
: false;
}

/**
* Look for 'data-ocweb-id' attibute in the HTMLElement in order to
* give a name to the user interaction and Root span. If this attibute is
* not present, use the element ID, tag name and event name, generating a CSS
* selector. In this case, also mark the interaction name as replaceable.
* Thus, the resulting interaction name will be: "<tag_name>#id event_name"
* (e.g. "button#save_changes click").
* In case the name is not resolvable, return undefined (e.g. element is the
* `document`).
* @param element
*/
export function resolveInteractionName(
element: HTMLElement | null,
eventName: string
): InteractionName | undefined {
if (!element) return undefined;
if (!element.getAttribute) return undefined;
if (element.hasAttribute('disabled')) {
return undefined;
}
let interactionName = element.getAttribute('data-ocweb-id');
let nameCanChange = false;
if (!interactionName) {
const elementId = element.getAttribute('id') || '';
const tagName = element.tagName;
if (!tagName) return undefined;
nameCanChange = true;
interactionName =
tagName.toLowerCase() +
(elementId ? '#' + elementId : '') +
(eventName ? ' ' + eventName : '');
}
return { name: interactionName, isReplaceable: nameCanChange };
}
11 changes: 11 additions & 0 deletions packages/opencensus-web-instrumentation-zone/src/zone-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ export interface XhrPerformanceResourceTiming {
corsPreFlightRequest?: PerformanceResourceTiming;
mainRequest: PerformanceResourceTiming;
}

/**
* Type to allow the interaction tracker know whether the interaction name can
* change or not. As part of naming the interaction, the name could be given
* using the `data-ocweb-id` attribute or as a CSS selector, however when there
* are route transitions, the name might change to `Navigation URL`.
*/
export interface InteractionName {
name: string;
isReplaceable: boolean;
}
1 change: 1 addition & 0 deletions packages/opencensus-web-instrumentation-zone/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
import './test-on-page-interaction-stop-watch';
import './test-interaction-tracker';
import './test-perf-resource-timing-selector';
import './test-util';

0 comments on commit 76fbd7c

Please sign in to comment.