Skip to content

Commit 7363c6b

Browse files
committed
feat: add a new client-metrics-web package
Work in progress. Release-as: 0.1.0
1 parent dc16323 commit 7363c6b

File tree

9 files changed

+249
-0
lines changed

9 files changed

+249
-0
lines changed

.github/dependabot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ updates:
66
- /
77
- /resources/logos
88
- /packages/app-info
9+
- /packages/client-metrics-web
910
- /packages/crash-handler
1011
- /packages/errors
1112
- /packages/eslint-config

.release-please-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"packages/app-info": "4.1.0",
3+
"packages/client-metrics-web": "0.0.0",
34
"packages/crash-handler": "5.0.2",
45
"packages/errors": "4.0.0",
56
"packages/eslint-config": "4.0.0",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CHANGELOG.md
2+
docs
3+
test

packages/client-metrics-web/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
# @dotcom-reliability-kit/client-metrics-web
3+
4+
A client for sending metrics to AWS CloudWatch RUM from the web. This module is part of [FT.com Reliability Kit](https://github.com/Financial-Times/dotcom-reliability-kit#readme).
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
const { AwsRum } = require('aws-rum-web');
2+
3+
/**
4+
* @import { AwsRumConfig } from 'aws-rum-web'
5+
* @import { MetricsClientOptions, MetricsClient as MetricsClientType, MetricsEvent } from '@dotcom-reliability-kit/client-metrics-web'
6+
*/
7+
8+
const cpClientMetrics = {
9+
appMonitorDomain: /\.ft\.com$/,
10+
appMonitorEndpoint: 'https://dataplane.rum.eu-west-1.amazonaws.com',
11+
appMonitorId: '9d83365c-91d0-4d9b-ac46-d73a7d1b574f',
12+
appMonitorRegion: 'eu-west-1',
13+
identityPoolId: 'eu-west-1:51b0ae4e-c11d-4047-b281-7357c61002c5'
14+
};
15+
16+
exports.MetricsClient = class MetricsClient {
17+
/** @type {null | AwsRum} */
18+
#rum = null;
19+
20+
/** @type {boolean} */
21+
#isAvailable = false;
22+
23+
/** @type {boolean} */
24+
#isEnabled = false;
25+
26+
/**
27+
* @param {MetricsClientOptions} options
28+
*/
29+
constructor(options) {
30+
try {
31+
const {
32+
awsAppMonitorEndpoint,
33+
awsAppMonitorId,
34+
awsAppMonitorRegion,
35+
awsIdentityPoolId,
36+
samplePercentage,
37+
systemCode,
38+
systemVersion
39+
} = MetricsClient.#defaultOptions(options);
40+
41+
// Convert percentage-based sample rate to a decimal
42+
const sessionSampleRate =
43+
Math.round(Math.min(Math.max(samplePercentage, 0), 100)) / 100;
44+
45+
/** @type {AwsRumConfig} */
46+
const awsRumConfig = {
47+
sessionSampleRate,
48+
identityPoolId: awsIdentityPoolId,
49+
endpoint: awsAppMonitorEndpoint,
50+
telemetries: ['errors'],
51+
allowCookies: false,
52+
enableXRay: false,
53+
disableAutoPageView: true,
54+
sessionAttributes: { systemCode }
55+
};
56+
57+
this.#rum = new AwsRum(
58+
awsAppMonitorId,
59+
systemVersion,
60+
awsAppMonitorRegion,
61+
awsRumConfig
62+
);
63+
this.#isAvailable = true;
64+
} catch (/** @type {any} */ error) {
65+
this.#isAvailable = false;
66+
// eslint-disable-next-line no-console
67+
console.warn(`RUM client could not be initialised: ${error.message}`);
68+
}
69+
70+
this.#handleMetricsEvent = this.#handleMetricsEvent.bind(this);
71+
this.enable();
72+
}
73+
74+
/** @type {MetricsClientType['enable']} */
75+
enable() {
76+
if (this.#isAvailable && !this.#isEnabled) {
77+
this.#rum?.enable();
78+
window.addEventListener('ft.clientMetric', this.#handleMetricsEvent);
79+
}
80+
}
81+
82+
/** @type {MetricsClientType['disable']} */
83+
disable() {
84+
if (this.#isAvailable && this.#isEnabled) {
85+
this.#rum?.disable();
86+
window.removeEventListener('ft.clientMetric', this.#handleMetricsEvent);
87+
}
88+
}
89+
90+
/**
91+
* @param {Event} event
92+
*/
93+
#handleMetricsEvent = (event) => {
94+
try {
95+
if (event instanceof CustomEvent) {
96+
const { namespace, ...data } = MetricsClient.#resolveEventDetail(
97+
event.detail
98+
);
99+
this.recordEvent(namespace, data);
100+
}
101+
} catch (/** @type {any} */ error) {
102+
// eslint-disable-next-line no-console
103+
console.warn(`Metrics event could not be sent: ${error.message}`);
104+
}
105+
};
106+
107+
/** @type {MetricsClientType['recordError']} */
108+
recordError(error) {
109+
return this.#rum?.recordError(error);
110+
}
111+
112+
/** @type {MetricsClientType['recordEvent']} */
113+
recordEvent(namespace, eventData = {}) {
114+
this.#rum?.recordEvent(
115+
MetricsClient.#resolveNamespace(namespace),
116+
eventData
117+
);
118+
}
119+
120+
/**
121+
* @param {MetricsClientOptions} options
122+
* @returns {Required<MetricsClientOptions>}
123+
*/
124+
static #defaultOptions(options) {
125+
/** @type {Required<MetricsClientOptions>} */
126+
const defaultedOptions = Object.assign(
127+
{
128+
awsAppMonitorDomainPattern: cpClientMetrics.appMonitorDomain,
129+
awsAppMonitorEndpoint: cpClientMetrics.appMonitorEndpoint,
130+
awsAppMonitorId: cpClientMetrics.appMonitorId,
131+
awsAppMonitorRegion: cpClientMetrics.appMonitorRegion,
132+
awsIdentityPoolId: cpClientMetrics.identityPoolId,
133+
samplePercentage: 10,
134+
systemCode: null,
135+
systemVersion: '0.0.0'
136+
},
137+
options
138+
);
139+
this.#assertValidOptions(defaultedOptions);
140+
return defaultedOptions;
141+
}
142+
143+
/**
144+
* @param {Required<MetricsClientOptions>} options
145+
* @returns {void}
146+
*/
147+
static #assertValidOptions({ awsAppMonitorDomainPattern, systemCode }) {
148+
// No point trying to send RUM events when we're not running on an allowed domain
149+
const hostname = window.location.hostname;
150+
if (!awsAppMonitorDomainPattern.test(hostname)) {
151+
throw new Error(`Client errors cannot be handled on ${hostname}`);
152+
}
153+
154+
if (typeof systemCode !== 'string') {
155+
throw new TypeError('Option systemCode must be a string');
156+
}
157+
}
158+
159+
/**
160+
* @param {string} namespace
161+
* @returns {string}
162+
*/
163+
static #resolveNamespace(namespace) {
164+
return `com.ft.${namespace}`;
165+
}
166+
167+
/**
168+
* @param {any} detail
169+
* @returns {MetricsEvent}
170+
*/
171+
static #resolveEventDetail(detail) {
172+
if (
173+
typeof detail !== 'object' ||
174+
detail === null ||
175+
Array.isArray(detail)
176+
) {
177+
throw new TypeError(`Event detail is not an object`);
178+
}
179+
if (typeof detail.namespace !== 'string') {
180+
throw new TypeError(`Event detail.namespace is not a string`);
181+
}
182+
return detail;
183+
}
184+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@dotcom-reliability-kit/client-metrics-web",
3+
"version": "0.0.0",
4+
"description": "A client for sending metrics to AWS CloudWatch RUM from the web",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/Financial-Times/dotcom-reliability-kit.git",
8+
"directory": "packages/client-metrics-web"
9+
},
10+
"homepage": "https://github.com/Financial-Times/dotcom-reliability-kit/tree/main/packages/client-metrics-web#readme",
11+
"bugs": "https://github.com/Financial-Times/dotcom-reliability-kit/issues?q=label:\"package: client-metrics-web\"",
12+
"license": "MIT",
13+
"engines": {
14+
"node": "20.x || 22.x"
15+
},
16+
"main": "lib/index.js",
17+
"types": "types/index.d.ts",
18+
"dependencies": {
19+
"aws-rum-web": "^1.21.0"
20+
}
21+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe('@dotcom-reliability-kit/client-metrics-web', () => {
2+
it('has some tests', () => {
3+
throw new Error('Please write some tests');
4+
});
5+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
declare module '@dotcom-reliability-kit/client-metrics-web' {
2+
export type MetricsClientOptions = {
3+
awsAppMonitorDomainPattern?: RegExp;
4+
awsAppMonitorEndpoint?: string;
5+
awsAppMonitorId?: string;
6+
awsAppMonitorRegion?: string;
7+
awsIdentityPoolId?: string;
8+
hostname?: string;
9+
samplePercentage?: number;
10+
systemCode: string;
11+
systemVersion?: string;
12+
};
13+
14+
export class MetricsClient {
15+
constructor(options: Options): MetricsClient;
16+
public enable(): void;
17+
public disable(): void;
18+
public recordError(error: unknown): void;
19+
public recordEvent(
20+
namespace: string,
21+
eventData?: Record<string, any>
22+
): void;
23+
}
24+
25+
export type MetricsEvent = {
26+
namespace: string;
27+
[key: string]: any;
28+
};
29+
}

release-please-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
],
3232
"packages": {
3333
"packages/app-info": {},
34+
"packages/client-metrics-web": {},
3435
"packages/crash-handler": {},
3536
"packages/errors": {},
3637
"packages/eslint-config": {},

0 commit comments

Comments
 (0)