|
| 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 | +}; |
0 commit comments