Skip to content

Commit

Permalink
feat(user-interaction): support for custom events and span enhancement (
Browse files Browse the repository at this point in the history
open-telemetry#653)

Co-authored-by: Bartlomiej Obecny <bobecny@gmail.com>
Co-authored-by: Valentin Marchaud <contact@vmarchaud.fr>
  • Loading branch information
3 people authored Dec 11, 2021
1 parent c0d31ce commit 27e37e3
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,31 @@
* limitations under the License.
*/

import {
isWrapped,
InstrumentationBase,
InstrumentationConfig,
} from '@opentelemetry/instrumentation';
import { isWrapped, InstrumentationBase } from '@opentelemetry/instrumentation';

import * as api from '@opentelemetry/api';
import { hrTime } from '@opentelemetry/core';
import { getElementXPath } from '@opentelemetry/sdk-trace-web';
import { AttributeNames } from './enums/AttributeNames';
import {
AsyncTask,
EventName,
RunTaskFunction,
ShouldPreventSpanCreation,
SpanData,
UserInteractionInstrumentationConfig,
WindowWithZone,
ZoneTypeWithPrototype,
} from './types';
import { VERSION } from './version';

const ZONE_CONTEXT_KEY = 'OT_ZONE_CONTEXT';
const EVENT_NAVIGATION_NAME = 'Navigation:';
const DEFAULT_EVENT_NAMES: EventName[] = ['click'];

function defaultShouldPreventSpanCreation() {
return false;
}

/**
* This class represents a UserInteraction plugin for auto instrumentation.
Expand All @@ -57,9 +61,16 @@ export class UserInteractionInstrumentation extends InstrumentationBase<unknown>
Event,
api.Span
>();
private _eventNames: Set<EventName>;
private _shouldPreventSpanCreation: ShouldPreventSpanCreation;

constructor(config?: InstrumentationConfig) {
constructor(config?: UserInteractionInstrumentationConfig) {
super('@opentelemetry/instrumentation-user-interaction', VERSION, config);
this._eventNames = new Set(config?.eventNames ?? DEFAULT_EVENT_NAMES);
this._shouldPreventSpanCreation =
typeof config?.shouldPreventSpanCreation === 'function'
? config.shouldPreventSpanCreation
: defaultShouldPreventSpanCreation;
}

init() {}
Expand Down Expand Up @@ -89,17 +100,18 @@ export class UserInteractionInstrumentation extends InstrumentationBase<unknown>
/**
* Controls whether or not to create a span, based on the event type.
*/
protected _allowEventType(eventType: string): boolean {
return eventType === 'click';
protected _allowEventName(eventName: EventName): boolean {
return this._eventNames.has(eventName);
}

/**
* Creates a new span
* @param element
* @param eventName
*/
private _createSpan(
element: EventTarget | null | undefined,
eventName: string,
eventName: EventName,
parentSpan?: api.Span | undefined
): api.Span | undefined {
if (!(element instanceof HTMLElement)) {
Expand All @@ -111,7 +123,7 @@ export class UserInteractionInstrumentation extends InstrumentationBase<unknown>
if (element.hasAttribute('disabled')) {
return undefined;
}
if (!this._allowEventType(eventName)) {
if (!this._allowEventName(eventName)) {
return undefined;
}
const xpath = getElementXPath(element, true);
Expand All @@ -133,6 +145,10 @@ export class UserInteractionInstrumentation extends InstrumentationBase<unknown>
: undefined
);

if (this._shouldPreventSpanCreation(eventName, element, span) === true) {
return undefined;
}

this._spansData.set(span, {
taskCount: 0,
});
Expand Down Expand Up @@ -262,7 +278,7 @@ export class UserInteractionInstrumentation extends InstrumentationBase<unknown>
return (original: EventTarget['addEventListener']) => {
return function addEventListenerPatched(
this: HTMLElement,
type: string,
type: keyof HTMLElementEventMap,
listener: EventListenerOrEventListenerObject | null,
useCapture?: boolean | AddEventListenerOptions
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,38 @@
* limitations under the License.
*/

import * as types from '@opentelemetry/api';
import { HrTime, Span } from '@opentelemetry/api';
import { InstrumentationConfig } from '@opentelemetry/instrumentation';

export type EventName = keyof HTMLElementEventMap;

export type ShouldPreventSpanCreation = (
eventType: EventName,
element: HTMLElement,
span: Span
) => boolean | void;

export interface UserInteractionInstrumentationConfig
extends InstrumentationConfig {
/**
* List of events to instrument (like 'mousedown', 'touchend', 'play' etc).
* By default only 'click' event is instrumented.
*/
eventNames?: EventName[];

/**
* Callback function called each time new span is being created.
* Return `true` to prevent span recording.
* You can also use this handler to enhance created span with extra attributes.
*/
shouldPreventSpanCreation?: ShouldPreventSpanCreation;
}

/**
* Async Zone task
*/
export type AsyncTask = Task & {
eventName: string;
eventName: EventName;
target: EventTarget;
// Allows access to the private `_zone` property of a Zone.js Task.
_zone: Zone;
Expand All @@ -39,7 +64,7 @@ export type RunTaskFunction = (
* interface to store information in weak map per span
*/
export interface SpanData {
hrTimeLastTimeout?: types.HrTime;
hrTimeLastTimeout?: HrTime;
taskCount: number;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function createButton(disabled?: boolean): HTMLElement {
return button;
}

export function fakeInteraction(
export function fakeClickInteraction(
callback: Function = function () {},
element: HTMLElement = createButton()
) {
Expand All @@ -45,14 +45,41 @@ export function fakeInteraction(
element.click();
}

export function fakeEventInteraction(
eventType: string,
callback: Function = function () {},
elem?: HTMLElement
) {
const element: HTMLElement = elem || createButton();
const event = document.createEvent('Event');
event.initEvent(eventType, true, true);

element.addEventListener(eventType, () => {
callback();
});

element.dispatchEvent(event);
}

export function assertClickSpan(span: tracing.ReadableSpan, id = 'testBtn') {
assert.equal(span.name, 'click');
assertInteractionSpan(span, { name: 'click', elementId: id });
}

export function assertInteractionSpan(
span: tracing.ReadableSpan,
{
name,
eventType = name,
elementId = 'testBtn',
}: { name: string; eventType?: string; elementId?: string }
) {
assert.strictEqual(span.name, name);

const attributes = span.attributes;
assert.equal(attributes.component, 'user-interaction');
assert.equal(attributes.event_type, 'click');
assert.equal(attributes.target_element, 'BUTTON');
assert.equal(attributes.target_xpath, `//*[@id="${id}"]`);
assert.strictEqual(attributes.component, 'user-interaction');
assert.strictEqual(attributes.event_type, eventType);
assert.strictEqual(attributes.target_element, 'BUTTON');
assert.strictEqual(attributes.target_xpath, `//*[@id="${elementId}"]`);
assert.ok(attributes['http.url'] !== '');
assert.ok(attributes['user_agent'] !== '');
}
Expand Down
Loading

0 comments on commit 27e37e3

Please sign in to comment.