Skip to content

Commit

Permalink
feat: Add registerDomEvents command (#111)
Browse files Browse the repository at this point in the history
Allow applications to dynamically update DOM event listeners.
  • Loading branch information
limhjgrace committed Mar 4, 2022
1 parent cb7de5f commit 2a67daa
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 3 deletions.
11 changes: 11 additions & 0 deletions app/dom_event.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
function enable() {
cwr('enable');
}

function registerDomEvents() {
cwr('registerDomEvents', [
{ event: 'click', cssLocator: '[label="label2"]' }
]);
}
</script>

<style>
Expand Down Expand Up @@ -62,6 +68,11 @@
<button id="button2" label="label1">Button Two</button>
<button id="button3" label="label1">Button Three</button>
<hr />
<button id="registerDomEvents" onclick="registerDomEvents()">
Update Plugin
</button>
<button id="button5" label="label2">Button Five</button>
<hr />
<button id="dispatch" onclick="dispatch()">Dispatch</button>
<button id="clearRequestResponse" onclick="clearRequestResponse()">
Clear
Expand Down
1 change: 1 addition & 0 deletions docs/cdn_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ Commands may be sent to the web client after the snippet has executed. In the fo
| enable | None | `cwr('enable');` | Start recording and dispatching RUM events.
| recordPageView | String | `cwr('recordPageView', '/home');` | Record a page view event.<br/><br/>By default, the web client records page views when (1) the page first loads and (2) the browser's [history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) is called. The page ID is `window.location.pathname`.<br/><br/>In some cases, the web client's instrumentation will not record the desired page ID. In this case, the web client's page view automation must be disabled using the `disableAutoPageView` configuration, and the application must be instrumented to record page views using this command.
| recordError | Error \|&nbsp;ErrorEvent \|&nbsp;String | `try {...} catch(e) { cwr('recordError', e); }` | Record a caught error.
| registerDomEvents | Array | `cwr('registerDomEvents', [{ event: 'click', cssLocator: '[label="label1"]' }]);` | Register target DOM events to record. The target DOM events will be added to existing target DOM events. The parameter type is equivalent to the `events` property type of the [interaction telemetry configuration](https://github.com/aws-observability/aws-rum-web/blob/main/docs/cdn_installation.md#interaction).
| setAwsCredentials | [Credentials](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html) \|&nbsp;[CredentialProvider](https://www.npmjs.com/package/@aws-sdk/credential-providers) | `cwr('setAwsCredentials', cred);` | Forward AWS credentials to the web client. The web client requires AWS credentials with permission to call the `PutRumEvents` API. If you have not set `identityPoolId` and `guestRoleArn` in the web client configuration, you must forward AWS credentials to the web client using this command.
3 changes: 3 additions & 0 deletions src/CommandQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export class CommandQueue {
recordError: (payload: any): void => {
this.orchestration.recordError(payload);
},
registerDomEvents: (payload: any): void => {
this.orchestration.registerDomEvents(payload);
},
dispatch: (): void => {
this.orchestration.dispatch();
},
Expand Down
14 changes: 13 additions & 1 deletion src/__tests__/CommandQueue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const setAwsCredentials = jest.fn();
const allowCookies = jest.fn();
const recordPageView = jest.fn();
const recordError = jest.fn();
const registerDomEvents = jest.fn();
jest.mock('../orchestration/Orchestration', () => ({
Orchestration: jest.fn().mockImplementation(() => ({
disable,
Expand All @@ -54,7 +55,8 @@ jest.mock('../orchestration/Orchestration', () => ({
setAwsCredentials,
allowCookies,
recordPageView,
recordError
recordError,
registerDomEvents
}))
}));

Expand Down Expand Up @@ -273,6 +275,16 @@ describe('CommandQueue tests', () => {
expect(recordError).toHaveBeenCalled();
});

test('registerDomEvents calls Orchestration.registerDomEvents', async () => {
const cq: CommandQueue = getCommandQueue();
await cq.push({
c: 'registerDomEvents',
p: false
});
expect(Orchestration).toHaveBeenCalled();
expect(registerDomEvents).toHaveBeenCalled();
});

test('allowCookies fails when paylod is non-boolean', async () => {
const cq: CommandQueue = getCommandQueue();
await cq
Expand Down
14 changes: 13 additions & 1 deletion src/orchestration/Orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Plugin, PluginContext } from '../plugins/Plugin';
import { Authentication } from '../dispatch/Authentication';
import { EnhancedAuthentication } from '../dispatch/EnhancedAuthentication';
import { PluginManager } from '../plugins/PluginManager';
import { DomEventPlugin } from '../plugins/event-plugins/DomEventPlugin';
import {
DomEventPlugin,
DOM_EVENT_PLUGIN_ID,
TargetDomEvent
} from '../plugins/event-plugins/DomEventPlugin';
import {
JsErrorPlugin,
JS_ERROR_EVENT_PLUGIN_ID
Expand Down Expand Up @@ -294,6 +298,14 @@ export class Orchestration {
this.pluginManager.record(JS_ERROR_EVENT_PLUGIN_ID, error);
}

/**
* Update DOM plugin to record the (additional) provided DOM events.
* @param pluginConfig Target DOM events.
*/
public registerDomEvents(events: TargetDomEvent[]) {
this.pluginManager.updatePlugin(DOM_EVENT_PLUGIN_ID, events);
}

private initEventCache(
applicationId: string,
applicationVersion: string
Expand Down
27 changes: 26 additions & 1 deletion src/orchestration/__tests__/Orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jest.mock('../../event-cache/EventCache', () => ({
}));

const addPlugin = jest.fn();
const updatePlugin = jest.fn();

const enablePlugins = jest.fn();
const disablePlugins = jest.fn();
Expand All @@ -36,7 +37,8 @@ jest.mock('../../plugins/PluginManager', () => ({
PluginManager: jest.fn().mockImplementation(() => ({
addPlugin: addPlugin,
enable: enablePlugins,
disable: disablePlugins
disable: disablePlugins,
updatePlugin: updatePlugin
}))
}));

Expand Down Expand Up @@ -328,4 +330,27 @@ describe('Orchestration tests', () => {

expect(actual.sort()).toEqual(expected.sort());
});

test('when an additional DOM event is provided then it is added to the DOM event plugin config', async () => {
// Init
const orchestration = new Orchestration('a', 'c', 'us-east-1', {
eventPluginsToLoad: [new DomEventPlugin()]
});

orchestration.registerDomEvents([
{ event: 'click', cssLocator: '[label="label1"]' }
]);

const expected = { event: 'click', cssLocator: '[label="label1"]' };
let actual;

// Assert
expect(updatePlugin).toHaveBeenCalledTimes(1);

updatePlugin.mock.calls.forEach((call) => {
actual = call[1][0];
});

expect(actual).toEqual(expected);
});
});
6 changes: 6 additions & 0 deletions src/plugins/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ export interface Plugin {
* @param data Data that the plugin will use to create an event.
*/
record?(data: any): void;

/**
* Update the plugin.
* @param config Data that the plugin will use to update its config.
*/
update?(config: object): void;
}
12 changes: 12 additions & 0 deletions src/plugins/PluginManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Plugin, PluginContext } from './Plugin';
import { DOM_EVENT_PLUGIN_ID } from '../plugins/event-plugins/DomEventPlugin';

/**
* The plugin manager maintains a list of plugins
Expand Down Expand Up @@ -31,6 +32,17 @@ export class PluginManager {
plugin.load(this.context);
}

/**
* Update an event plugin
* @param config The config to update the plugin with.
*/
public updatePlugin(pluginId: string, config: object) {
const plugin = this.plugins.get(pluginId);
if (plugin && plugin.update instanceof Function) {
plugin.update(config);
}
}

/**
* Enable all event plugins.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/plugins/event-plugins/DomEventPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export class DomEventPlugin implements Plugin {
return this.pluginId;
}

update(events: TargetDomEvent[]): void {
events.forEach((domEvent) => {
this.addEventHandler(domEvent);
this.config.events.push(domEvent);
});
}

private removeListeners() {
this.config.events.forEach((domEvent) =>
this.removeEventHandler(domEvent)
Expand Down
37 changes: 37 additions & 0 deletions src/plugins/event-plugins/__integ__/DomEventPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const link1: Selector = Selector(`a`);
const button2: Selector = Selector(`#button2`);
const button3: Selector = Selector(`#button3`);

const registerDomEvents: Selector = Selector(`#registerDomEvents`);
const button5: Selector = Selector(`#button5`);

const dispatch: Selector = Selector(`#dispatch`);
const clear: Selector = Selector(`#clearRequestResponse`);

Expand Down Expand Up @@ -260,3 +263,37 @@ test('when element ID and CSS selector are specified then only event for element
elementId: 'button1'
});
});

test('when new DOM events are registered and then a button is clicked, the event is recorded', async (t: TestController) => {
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
await t
.wait(300)
.click(registerDomEvents)
.click(button5)
.click(dispatch)
.expect(REQUEST_BODY.textContent)
.contains('BatchId');

const events = JSON.parse(await REQUEST_BODY.textContent).RumEvents.filter(
(e) =>
e.type === DOM_EVENT_TYPE &&
JSON.parse(e.details).cssLocator === '[label="label2"]'
);

for (let i = 0; i < events.length; i++) {
let eventType = events[i].type;
let eventDetails = JSON.parse(events[i].details);

await t
.expect(events.length)
.eql(1)
.expect(eventType)
.eql(DOM_EVENT_TYPE)
.expect(eventDetails)
.contains({
event: 'click',
cssLocator: '[label="label2"]'
});
}
});
27 changes: 27 additions & 0 deletions src/plugins/event-plugins/__tests__/DomEventPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,31 @@ describe('DomEventPlugin tests', () => {
})
);
});
test('when a DOM event is added at runtime then the DOM event is recorded', async () => {
// Init
document.body.innerHTML =
'<button id="button1" label="label1"></button>';
const plugin: DomEventPlugin = new DomEventPlugin();

// Run
plugin.load(context);

// Update plugin by registering new DOM event
plugin.update([{ event: 'click', cssLocator: '[label="label1"]' }]);

document.getElementById('button1').click();

plugin.disable();

// Assert
expect(record).toHaveBeenCalledTimes(1);
expect(record.mock.calls[0][0]).toEqual(DOM_EVENT_TYPE);
expect(record.mock.calls[0][1]).toMatchObject(
expect.objectContaining({
version: '1.0.0',
event: 'click',
cssLocator: '[label="label1"]'
})
);
});
});

0 comments on commit 2a67daa

Please sign in to comment.