Skip to content

Commit

Permalink
feat: Add keyboard input breadcrumbs. (#513)
Browse files Browse the repository at this point in the history
Prototype implementation.
  • Loading branch information
kinyoklion authored Jul 15, 2024
1 parent 41b03e3 commit 3c79e47
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ it('can set each option', () => {
expect(outOptions).toEqual({
maxPendingEvents: 1,
breadcrumbs: {
keyboardInput: true,
maxBreadcrumbs: 1,
click: false,
evaluations: false,
flagChange: false,
http: {
customUrlFilter: undefined,
instrumentFetch: true,
instrumentXhr: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Collector } from './api/Collector';
import { ErrorData } from './api/ErrorData';
import { EventData } from './api/EventData';
import ClickCollector from './collectors/dom/ClickCollector';
import KeypressCollector from './collectors/dom/KeypressCollector';
import ErrorCollector from './collectors/error';
import FetchCollector from './collectors/http/fetch';
import XhrCollector from './collectors/http/xhr';
Expand Down Expand Up @@ -96,6 +97,10 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry {
this.collectors.push(new ClickCollector());
}

if (options.breadcrumbs.keyboardInput) {
this.collectors.push(new KeypressCollector());
}

this.collectors.forEach((collector) => collector.register(this as BrowserTelemetry));

const impl = this;
Expand Down
2 changes: 1 addition & 1 deletion packages/telemetry/browser-telemetry/src/api/Breadcrumb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export type UiBreadcrumb = ImplementsCrumb<{
class: 'ui';
timestamp: number;
level: 'info';
type: 'click';
type: 'click' | 'input';
message: string;
}>;

Expand Down
7 changes: 7 additions & 0 deletions packages/telemetry/browser-telemetry/src/api/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ export interface Options {
*/
click?: boolean;

/**
* True to enable input breadcrumbs for keypresses. Defaults to true.
*
* Input breadcrumbs do not include entered text, just that text was entered.
*/
keyboardInput?: boolean;

/**
* Controls instrumentation and breadcrumbs for HTTP requests.
* The default is to instrument XMLHttpRequests and fetch requests.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
import { UiBreadcrumb } from '../../api/Breadcrumb';
import { BrowserTelemetry } from '../../api/BrowserTelemetry';
import { Collector } from '../../api/Collector';
import getTarget from './getTarget';
import toSelector from './toSelector';

/**
* Get the event target. This is wrapped because in some situations a browser may throw when
* accessing the event target.
*
* @param event The event to get the target from.
* @returns The event target, or undefined if one is not available.
*/
function getTarget(event: MouseEvent): Element | undefined {
try {
return event.target as Element;
} catch {
return undefined;
}
}

/**
* Collects mouse click events and adds them as breadcrumbs.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Breadcrumb, UiBreadcrumb } from '../../api/Breadcrumb';
import { BrowserTelemetry } from '../../api/BrowserTelemetry';
import { Collector } from '../../api/Collector';
import getTarget from './getTarget';
import toSelector from './toSelector';

const THROTTLE_TIME_MS = 1000;

const INPUT_TAG_NAMES = ['INPUT', 'TEXTAREA'];

/**
* Collects mouse click events and adds them as breadcrumbs.
*/
export default class KeypressCollector implements Collector {
private destination?: BrowserTelemetry;
private lastEvent?: UiBreadcrumb;

constructor() {
window.addEventListener(
'keypress',
(event: KeyboardEvent) => {
const target = getTarget(event);
const htmlElement = target as HTMLElement;
// An example of `isContentEditable` would be an editable <p> tag.
// Input and textarea tags do not have the isContentEditable property.
if (
target &&
(INPUT_TAG_NAMES.includes(target.tagName) || htmlElement?.isContentEditable)
) {
const breadcrumb: UiBreadcrumb = {
class: 'ui',
type: 'input',
level: 'info',
timestamp: Date.now(),
message: toSelector(target),
};

if (!this.shouldDeduplicate(breadcrumb)) {
this.destination?.addBreadcrumb(breadcrumb);
this.lastEvent = breadcrumb;
}
}
},
true,
);
}

register(telemetry: BrowserTelemetry): void {
this.destination = telemetry;
}
unregister(): void {
this.destination = undefined;
}

private shouldDeduplicate(crumb: Breadcrumb): boolean {
// TODO: Consider de-duplication at the dom level.
if (this.lastEvent) {
const timeDiff = Math.abs(crumb.timestamp - this.lastEvent.timestamp);
return this.lastEvent.message === crumb.message && timeDiff <= THROTTLE_TIME_MS;
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Get the event target. This is wrapped because in some situations a browser may throw when
* accessing the event target.
*
* @param event The event to get the target from.
* @returns The event target, or undefined if one is not available.
*/
export default function getTarget(event: { target: any }): Element | undefined {
try {
return event.target as Element;
} catch {
return undefined;
}
}
10 changes: 10 additions & 0 deletions packages/telemetry/browser-telemetry/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function defaultOptions(): ParsedOptions {
evaluations: true,
flagChange: true,
click: true,
keyboardInput: true,
http: {
instrumentFetch: true,
instrumentXhr: true,
Expand Down Expand Up @@ -85,6 +86,10 @@ export default function parse(options: Options): ParsedOptions {
),
flagChange: itemOrDefault(options.breadcrumbs?.flagChange, defaults.breadcrumbs.flagChange),
click: itemOrDefault(options.breadcrumbs?.click, defaults.breadcrumbs.click),
keyboardInput: itemOrDefault(
options.breadcrumbs?.keyboardInput,
defaults.breadcrumbs.keyboardInput,
),
http: parseHttp(options.breadcrumbs?.http, defaults.breadcrumbs.http),
},
stack: parseStack(options.stack, defaults.stack),
Expand Down Expand Up @@ -161,6 +166,11 @@ export interface ParsedOptions {
*/
click: boolean;

/**
* True to enable input breadcrumbs for keypresses. Defaults to true.
*/
keyboardInput?: boolean;

/**
* Settings for http instrumentation and breadcrumbs.
*/
Expand Down

0 comments on commit 3c79e47

Please sign in to comment.