Skip to content

Commit 2a67daa

Browse files
authored
feat: Add registerDomEvents command (#111)
Allow applications to dynamically update DOM event listeners.
1 parent cb7de5f commit 2a67daa

File tree

11 files changed

+156
-3
lines changed

11 files changed

+156
-3
lines changed

app/dom_event.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
function enable() {
3434
cwr('enable');
3535
}
36+
37+
function registerDomEvents() {
38+
cwr('registerDomEvents', [
39+
{ event: 'click', cssLocator: '[label="label2"]' }
40+
]);
41+
}
3642
</script>
3743

3844
<style>
@@ -62,6 +68,11 @@
6268
<button id="button2" label="label1">Button Two</button>
6369
<button id="button3" label="label1">Button Three</button>
6470
<hr />
71+
<button id="registerDomEvents" onclick="registerDomEvents()">
72+
Update Plugin
73+
</button>
74+
<button id="button5" label="label2">Button Five</button>
75+
<hr />
6576
<button id="dispatch" onclick="dispatch()">Dispatch</button>
6677
<button id="clearRequestResponse" onclick="clearRequestResponse()">
6778
Clear

docs/cdn_commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ Commands may be sent to the web client after the snippet has executed. In the fo
3232
| enable | None | `cwr('enable');` | Start recording and dispatching RUM events.
3333
| 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.
3434
| recordError | Error \|&nbsp;ErrorEvent \|&nbsp;String | `try {...} catch(e) { cwr('recordError', e); }` | Record a caught error.
35+
| 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).
3536
| 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.

src/CommandQueue.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export class CommandQueue {
4444
recordError: (payload: any): void => {
4545
this.orchestration.recordError(payload);
4646
},
47+
registerDomEvents: (payload: any): void => {
48+
this.orchestration.registerDomEvents(payload);
49+
},
4750
dispatch: (): void => {
4851
this.orchestration.dispatch();
4952
},

src/__tests__/CommandQueue.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const setAwsCredentials = jest.fn();
4545
const allowCookies = jest.fn();
4646
const recordPageView = jest.fn();
4747
const recordError = jest.fn();
48+
const registerDomEvents = jest.fn();
4849
jest.mock('../orchestration/Orchestration', () => ({
4950
Orchestration: jest.fn().mockImplementation(() => ({
5051
disable,
@@ -54,7 +55,8 @@ jest.mock('../orchestration/Orchestration', () => ({
5455
setAwsCredentials,
5556
allowCookies,
5657
recordPageView,
57-
recordError
58+
recordError,
59+
registerDomEvents
5860
}))
5961
}));
6062

@@ -273,6 +275,16 @@ describe('CommandQueue tests', () => {
273275
expect(recordError).toHaveBeenCalled();
274276
});
275277

278+
test('registerDomEvents calls Orchestration.registerDomEvents', async () => {
279+
const cq: CommandQueue = getCommandQueue();
280+
await cq.push({
281+
c: 'registerDomEvents',
282+
p: false
283+
});
284+
expect(Orchestration).toHaveBeenCalled();
285+
expect(registerDomEvents).toHaveBeenCalled();
286+
});
287+
276288
test('allowCookies fails when paylod is non-boolean', async () => {
277289
const cq: CommandQueue = getCommandQueue();
278290
await cq

src/orchestration/Orchestration.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { Plugin, PluginContext } from '../plugins/Plugin';
22
import { Authentication } from '../dispatch/Authentication';
33
import { EnhancedAuthentication } from '../dispatch/EnhancedAuthentication';
44
import { PluginManager } from '../plugins/PluginManager';
5-
import { DomEventPlugin } from '../plugins/event-plugins/DomEventPlugin';
5+
import {
6+
DomEventPlugin,
7+
DOM_EVENT_PLUGIN_ID,
8+
TargetDomEvent
9+
} from '../plugins/event-plugins/DomEventPlugin';
610
import {
711
JsErrorPlugin,
812
JS_ERROR_EVENT_PLUGIN_ID
@@ -294,6 +298,14 @@ export class Orchestration {
294298
this.pluginManager.record(JS_ERROR_EVENT_PLUGIN_ID, error);
295299
}
296300

301+
/**
302+
* Update DOM plugin to record the (additional) provided DOM events.
303+
* @param pluginConfig Target DOM events.
304+
*/
305+
public registerDomEvents(events: TargetDomEvent[]) {
306+
this.pluginManager.updatePlugin(DOM_EVENT_PLUGIN_ID, events);
307+
}
308+
297309
private initEventCache(
298310
applicationId: string,
299311
applicationVersion: string

src/orchestration/__tests__/Orchestration.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jest.mock('../../event-cache/EventCache', () => ({
2828
}));
2929

3030
const addPlugin = jest.fn();
31+
const updatePlugin = jest.fn();
3132

3233
const enablePlugins = jest.fn();
3334
const disablePlugins = jest.fn();
@@ -36,7 +37,8 @@ jest.mock('../../plugins/PluginManager', () => ({
3637
PluginManager: jest.fn().mockImplementation(() => ({
3738
addPlugin: addPlugin,
3839
enable: enablePlugins,
39-
disable: disablePlugins
40+
disable: disablePlugins,
41+
updatePlugin: updatePlugin
4042
}))
4143
}));
4244

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

329331
expect(actual.sort()).toEqual(expected.sort());
330332
});
333+
334+
test('when an additional DOM event is provided then it is added to the DOM event plugin config', async () => {
335+
// Init
336+
const orchestration = new Orchestration('a', 'c', 'us-east-1', {
337+
eventPluginsToLoad: [new DomEventPlugin()]
338+
});
339+
340+
orchestration.registerDomEvents([
341+
{ event: 'click', cssLocator: '[label="label1"]' }
342+
]);
343+
344+
const expected = { event: 'click', cssLocator: '[label="label1"]' };
345+
let actual;
346+
347+
// Assert
348+
expect(updatePlugin).toHaveBeenCalledTimes(1);
349+
350+
updatePlugin.mock.calls.forEach((call) => {
351+
actual = call[1][0];
352+
});
353+
354+
expect(actual).toEqual(expected);
355+
});
331356
});

src/plugins/Plugin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,10 @@ export interface Plugin {
4343
* @param data Data that the plugin will use to create an event.
4444
*/
4545
record?(data: any): void;
46+
47+
/**
48+
* Update the plugin.
49+
* @param config Data that the plugin will use to update its config.
50+
*/
51+
update?(config: object): void;
4652
}

src/plugins/PluginManager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Plugin, PluginContext } from './Plugin';
2+
import { DOM_EVENT_PLUGIN_ID } from '../plugins/event-plugins/DomEventPlugin';
23

34
/**
45
* The plugin manager maintains a list of plugins
@@ -31,6 +32,17 @@ export class PluginManager {
3132
plugin.load(this.context);
3233
}
3334

35+
/**
36+
* Update an event plugin
37+
* @param config The config to update the plugin with.
38+
*/
39+
public updatePlugin(pluginId: string, config: object) {
40+
const plugin = this.plugins.get(pluginId);
41+
if (plugin && plugin.update instanceof Function) {
42+
plugin.update(config);
43+
}
44+
}
45+
3446
/**
3547
* Enable all event plugins.
3648
*/

src/plugins/event-plugins/DomEventPlugin.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ export class DomEventPlugin implements Plugin {
7777
return this.pluginId;
7878
}
7979

80+
update(events: TargetDomEvent[]): void {
81+
events.forEach((domEvent) => {
82+
this.addEventHandler(domEvent);
83+
this.config.events.push(domEvent);
84+
});
85+
}
86+
8087
private removeListeners() {
8188
this.config.events.forEach((domEvent) =>
8289
this.removeEventHandler(domEvent)

src/plugins/event-plugins/__integ__/DomEventPlugin.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const link1: Selector = Selector(`a`);
1414
const button2: Selector = Selector(`#button2`);
1515
const button3: Selector = Selector(`#button3`);
1616

17+
const registerDomEvents: Selector = Selector(`#registerDomEvents`);
18+
const button5: Selector = Selector(`#button5`);
19+
1720
const dispatch: Selector = Selector(`#dispatch`);
1821
const clear: Selector = Selector(`#clearRequestResponse`);
1922

@@ -260,3 +263,37 @@ test('when element ID and CSS selector are specified then only event for element
260263
elementId: 'button1'
261264
});
262265
});
266+
267+
test('when new DOM events are registered and then a button is clicked, the event is recorded', async (t: TestController) => {
268+
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
269+
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
270+
await t
271+
.wait(300)
272+
.click(registerDomEvents)
273+
.click(button5)
274+
.click(dispatch)
275+
.expect(REQUEST_BODY.textContent)
276+
.contains('BatchId');
277+
278+
const events = JSON.parse(await REQUEST_BODY.textContent).RumEvents.filter(
279+
(e) =>
280+
e.type === DOM_EVENT_TYPE &&
281+
JSON.parse(e.details).cssLocator === '[label="label2"]'
282+
);
283+
284+
for (let i = 0; i < events.length; i++) {
285+
let eventType = events[i].type;
286+
let eventDetails = JSON.parse(events[i].details);
287+
288+
await t
289+
.expect(events.length)
290+
.eql(1)
291+
.expect(eventType)
292+
.eql(DOM_EVENT_TYPE)
293+
.expect(eventDetails)
294+
.contains({
295+
event: 'click',
296+
cssLocator: '[label="label2"]'
297+
});
298+
}
299+
});

0 commit comments

Comments
 (0)