Skip to content

Commit 3e16093

Browse files
Reno Seoqhanam
andauthored
feat: Add recordEvent API and expose Plugin to enable recording of custom events (#188)
* feat: Add recordEvent API and expose Plugin to enable recording of custom events * Add recordEvent to CommandFunction * Change payload parameters to type and data * Update docs/cdn_commands.md Co-authored-by: Quinn Hanam <[email protected]> Co-authored-by: Quinn Hanam <[email protected]>
1 parent ac7a2bb commit 3e16093

File tree

12 files changed

+497
-11
lines changed

12 files changed

+497
-11
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/events/*

app/custom_event.html

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>RUM Custom Event Integ Test</title>
5+
<script src="./loader_custom_events.js"></script>
6+
<link
7+
rel="icon"
8+
type="image/png"
9+
href="https://awsmedia.s3.amazonaws.com/favicon.ico"
10+
/>
11+
12+
<script>
13+
// Common to all test pages
14+
function dispatch() {
15+
cwr('dispatch');
16+
}
17+
18+
// Record custom event directly using API
19+
function recordEvent() {
20+
cwr('recordEvent', {
21+
type: 'custom_event_api',
22+
data: { customEventVersion: 255 }
23+
});
24+
}
25+
26+
function recordEmptyEvent() {
27+
cwr('recordEvent', {
28+
type: 'custom_event_api',
29+
data: {}
30+
});
31+
}
32+
33+
// Record event using plugin
34+
function pluginRecordEvent() {
35+
window.dispatchEvent(new Event('custom_events'));
36+
}
37+
38+
function pluginRecordEmptyEvent() {
39+
window.dispatchEvent(new Event('empty_custom_events'));
40+
}
41+
42+
function clearRequestResponse() {
43+
document.getElementById('request_url').innerText = '';
44+
document.getElementById('request_header').innerText = '';
45+
document.getElementById('request_body').innerText = '';
46+
47+
document.getElementById('response_status').innerText = '';
48+
document.getElementById('response_header').innerText = '';
49+
document.getElementById('response_body').innerText = '';
50+
}
51+
</script>
52+
53+
<style>
54+
table {
55+
border-collapse: collapse;
56+
margin-top: 10px;
57+
margin-bottom: 10px;
58+
}
59+
60+
td,
61+
th {
62+
border: 1px solid black;
63+
text-align: left;
64+
padding: 8px;
65+
}
66+
</style>
67+
</head>
68+
69+
<body>
70+
<p id="welcome">This application is used for RUM integ testing.</p>
71+
<hr />
72+
<button id="dispatch" onclick="dispatch()">Dispatch</button>
73+
<button id="clearRequestResponse" onclick="clearRequestResponse()">
74+
Clear
75+
</button>
76+
<button id="recordEventAPI" onclick="recordEvent()">
77+
RecordEvent API
78+
</button>
79+
<button id="recordEventAPIEmpty" onclick="recordEmptyEvent()">
80+
RecordEvent API - Empty
81+
</button>
82+
<button id="pluginRecord" onclick="pluginRecordEvent()">
83+
Record with Plugin
84+
</button>
85+
<button id="pluginRecordEmpty" onclick="pluginRecordEmptyEvent()">
86+
Record with Plugin - Empty
87+
</button>
88+
<hr />
89+
<span id="request"></span>
90+
<span id="response"></span>
91+
<table>
92+
<tr>
93+
<td>Request URL</td>
94+
<td id="request_url"></td>
95+
</tr>
96+
<tr>
97+
<td>Request Header</td>
98+
<td id="request_header"></td>
99+
</tr>
100+
<tr>
101+
<td>Request Body</td>
102+
<td id="request_body"></td>
103+
</tr>
104+
</table>
105+
<table>
106+
<tr>
107+
<td>Response Status Code</td>
108+
<td id="response_status"></td>
109+
</tr>
110+
<tr>
111+
<td>Response Header</td>
112+
<td id="response_header"></td>
113+
</tr>
114+
<tr>
115+
<td>Response Body</td>
116+
<td id="response_body"></td>
117+
</tr>
118+
</table>
119+
</body>
120+
</html>

docs/cdn_commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ if(awsCreds) awsRum.setAwsCredentials(credentialProvider);
6262
| recordError | Error \|&nbsp;ErrorEvent \|&nbsp;String | `try {...} catch(e) { cwr('recordError', e); }`<br/><br/>`try {...} catch(e) { awsRum.recordError(e); }` | Record a caught error.
6363
| registerDomEvents | Array | `cwr('registerDomEvents', [{ event: 'click', cssLocator: '[label="label1"]' }]);`<br/><br/>`awsRum.registerDomEvent([{ 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).
6464
| 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);`<br/><br/>`awsRum.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.
65+
| recordEvent | Object | `cwr('recordEvent', {type: 'your_event_type', data: {field1: 1, field2: 2}})` <br/><br/> `awsRum.recordEvent('your_event_type', {field1: 1, field2: 2})` | Record a custom event.
6566

6667
## PageView
6768
| Field Name | Type | Default | Example | Description |

src/CommandQueue.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CredentialProvider, Credentials } from '@aws-sdk/types';
2+
import { Plugin } from 'plugins/Plugin';
23
import { PartialConfig, Orchestration } from './orchestration/Orchestration';
34
import { getRemoteConfig } from './remote-config/remote-config';
45

@@ -10,6 +11,7 @@ interface CommandFunctions {
1011
recordPageView: CommandFunction;
1112
recordError: CommandFunction;
1213
registerDomEvents: CommandFunction;
14+
recordEvent: CommandFunction;
1315
dispatch: CommandFunction;
1416
dispatchBeacon: CommandFunction;
1517
enable: CommandFunction;
@@ -65,6 +67,17 @@ export class CommandQueue {
6567
registerDomEvents: (payload: any): void => {
6668
this.orchestration.registerDomEvents(payload);
6769
},
70+
recordEvent: (payload: any) => {
71+
if (
72+
typeof payload === 'object' &&
73+
typeof payload.type === 'string' &&
74+
typeof payload.data === 'object'
75+
) {
76+
this.orchestration.recordEvent(payload.type, payload.data);
77+
} else {
78+
throw new Error('IncorrectParametersException');
79+
}
80+
},
6881
dispatch: (): void => {
6982
this.orchestration.dispatch();
7083
},
@@ -102,7 +115,7 @@ export class CommandQueue {
102115
awsRum.c = config;
103116
this.initCwr(awsRum);
104117
} else {
105-
// Ther is no remote config file -- initialize CWR immediately.
118+
// There is no remote config file -- initialize CWR immediately.
106119
this.initCwr(awsRum);
107120
}
108121
}

src/__integ__/customEvents.test.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { Selector } from 'testcafe';
2+
import { REQUEST_BODY } from '../test-utils/integ-test-utils';
3+
4+
const dispatch: Selector = Selector(`#dispatch`);
5+
const recordEventAPI: Selector = Selector(`#recordEventAPI`);
6+
const recordEventApiEmptyEvent: Selector = Selector(`#recordEventAPIEmpty`);
7+
const pluginRecord: Selector = Selector(`#pluginRecord`);
8+
const pluginRecordEmptyEvent: Selector = Selector(`#pluginRecordEmpty`);
9+
10+
const API_EVENT_TYPE = 'custom_event_api';
11+
const PLUGIN_EVENT_TYPE = 'custom_event_plugin';
12+
const COUNT = 5;
13+
14+
fixture('Custom Events API & Plugin').page(
15+
'http://localhost:8080/custom_event.html'
16+
);
17+
18+
const removeUnwantedEvents = (json: any) => {
19+
const newEventsList = json.RumEvents.filter(
20+
(e) =>
21+
/(custom_event_api)/.test(e.type) ||
22+
/(custom_event_plugin)/.test(e.type)
23+
);
24+
25+
json.RumEvents = newEventsList;
26+
return json;
27+
};
28+
29+
test('when a recordEvent API is called then event is recorded', async (t: TestController) => {
30+
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
31+
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
32+
await t
33+
.wait(300)
34+
.click(recordEventAPI)
35+
.click(dispatch)
36+
.expect(REQUEST_BODY.textContent)
37+
.contains('BatchId');
38+
39+
const json = removeUnwantedEvents(
40+
JSON.parse(await REQUEST_BODY.textContent)
41+
);
42+
const eventType = json.RumEvents[0].type;
43+
const eventDetails = JSON.parse(json.RumEvents[0].details);
44+
45+
await t
46+
.expect(eventType)
47+
.eql(API_EVENT_TYPE)
48+
.expect(eventDetails.customEventVersion)
49+
.eql(255);
50+
});
51+
52+
test('when a recordEvent API is called x times then event is recorded x times', async (t: TestController) => {
53+
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
54+
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
55+
// Record event 5 times.
56+
await t.wait(300);
57+
for (let i = 0; i < COUNT; i++) {
58+
await t.click(recordEventAPI);
59+
}
60+
61+
await t
62+
.click(dispatch)
63+
.expect(REQUEST_BODY.textContent)
64+
.contains('BatchId');
65+
66+
const json = removeUnwantedEvents(
67+
JSON.parse(await REQUEST_BODY.textContent)
68+
);
69+
70+
await t.expect(json.RumEvents.length).eql(COUNT);
71+
json.RumEvents.forEach(async (item) => {
72+
const eventType = item.type;
73+
const eventDetails = JSON.parse(item.details);
74+
await t
75+
.expect(eventType)
76+
.eql(API_EVENT_TYPE)
77+
.expect(eventDetails.customEventVersion)
78+
.eql(255);
79+
});
80+
});
81+
82+
test('when a recordEvent API has empty event_data then RumEvent detail is empty', async (t: TestController) => {
83+
await t
84+
.wait(300)
85+
.click(recordEventApiEmptyEvent)
86+
.click(dispatch)
87+
.expect(REQUEST_BODY.textContent)
88+
.contains('BatchId');
89+
90+
const json = removeUnwantedEvents(
91+
JSON.parse(await REQUEST_BODY.textContent)
92+
);
93+
await t
94+
.expect(json.RumEvents.length)
95+
.eql(1)
96+
.expect(json.RumEvents[0].type)
97+
.eql(API_EVENT_TYPE)
98+
.expect(json.RumEvents[0].details)
99+
.eql('{}');
100+
});
101+
102+
test('when a plugin calls recordEvent then the event is recorded', async (t: TestController) => {
103+
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
104+
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
105+
await t
106+
.wait(300)
107+
.click(pluginRecord)
108+
.click(dispatch)
109+
.expect(REQUEST_BODY.textContent)
110+
.contains('BatchId');
111+
112+
const json = removeUnwantedEvents(
113+
JSON.parse(await REQUEST_BODY.textContent)
114+
);
115+
const eventType = json.RumEvents[0].type;
116+
const eventDetails = JSON.parse(json.RumEvents[0].details);
117+
118+
await t
119+
.expect(eventType)
120+
.eql(PLUGIN_EVENT_TYPE)
121+
.expect(eventDetails.intField)
122+
.eql(1)
123+
.expect(eventDetails.stringField)
124+
.eql('string')
125+
.expect(eventDetails.nestedField)
126+
.eql({ subfield: 1 });
127+
});
128+
129+
test('when a plugin calls recordEvent x times then event is recorded x times', async (t: TestController) => {
130+
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
131+
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
132+
// Record event 5 times.
133+
await t.wait(300);
134+
for (let i = 0; i < COUNT; i++) {
135+
await t.click(pluginRecord);
136+
}
137+
await t
138+
.click(dispatch)
139+
.expect(REQUEST_BODY.textContent)
140+
.contains('BatchId');
141+
142+
const json = removeUnwantedEvents(
143+
JSON.parse(await REQUEST_BODY.textContent)
144+
);
145+
146+
await t.expect(json.RumEvents.length).eql(COUNT);
147+
json.RumEvents.forEach(async (item) => {
148+
const eventType = item.type;
149+
const eventDetails = JSON.parse(item.details);
150+
await t
151+
.expect(eventType)
152+
.eql(PLUGIN_EVENT_TYPE)
153+
.expect(eventDetails.intField)
154+
.eql(1)
155+
.expect(eventDetails.stringField)
156+
.eql('string')
157+
.expect(eventDetails.nestedField)
158+
.eql({ subfield: 1 });
159+
});
160+
});
161+
162+
test('when plugin recordEvent has empty event_data then RumEvent details is empty', async (t: TestController) => {
163+
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
164+
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
165+
await t.wait(300);
166+
167+
await t
168+
.click(pluginRecordEmptyEvent)
169+
.click(dispatch)
170+
.expect(REQUEST_BODY.textContent)
171+
.contains('BatchId');
172+
173+
const json = removeUnwantedEvents(
174+
JSON.parse(await REQUEST_BODY.textContent)
175+
);
176+
await t
177+
.expect(json.RumEvents.length)
178+
.eql(1)
179+
.expect(json.RumEvents[0].type)
180+
.eql(PLUGIN_EVENT_TYPE)
181+
.expect(json.RumEvents[0].details)
182+
.eql('{}');
183+
});

0 commit comments

Comments
 (0)