/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable class-methods-use-this */ /* eslint-disable valid-jsdoc */ // @ts-ignore import {WebexPlugin} from '@webex/webex-core'; import CallDiagnosticMetrics from './call-diagnostic/call-diagnostic-metrics'; import BehavioralMetrics from './behavioral-metrics'; import OperationalMetrics from './operational-metrics'; import BusinessMetrics from './business-metrics'; import PreLoginMetrics from './prelogin-metrics'; import PreLoginMetricsBatcher from './prelogin-metrics-batcher'; import { RecursivePartial, MetricEventProduct, MetricEventAgent, MetricEventVerb, ClientEvent, FeatureEvent, EventPayload, OperationalEvent, MediaQualityEvent, InternalEvent, SubmitClientEventOptions, Table, DelayedClientEvent, DelayedClientFeatureEvent, } from './metrics.types'; import CallDiagnosticLatencies from './call-diagnostic/call-diagnostic-metrics-latencies'; import {setMetricTimings} from './call-diagnostic/call-diagnostic-metrics.util'; import {generateCommonErrorMetadata} from './utils'; /** * Metrics plugin to centralize all types of metrics. * https://confluence-eng-gpk2.cisco.com/conf/pages/viewpage.action?pageId=231011379 * @class */ class Metrics extends WebexPlugin { // eslint-disable-next-line no-use-before-define static instance: Metrics; // Call Diagnostic latencies callDiagnosticLatencies: CallDiagnosticLatencies; // Helper classes to handle the different types of metrics callDiagnosticMetrics: CallDiagnosticMetrics; behavioralMetrics: BehavioralMetrics; operationalMetrics: OperationalMetrics; businessMetrics: BusinessMetrics; preLoginMetrics: PreLoginMetrics; isReady = false; /** * Whether or not to delay the submission of client events. */ delaySubmitClientEvents = false; /** * Whether or not to delay the submission of feature events. */ delaySubmitClientFeatureEvents = false; /** * Overrides for delayed client events. E.g. if you want to override the correlationId for all delayed client events, you can set this to { correlationId: 'newCorrelationId' } */ delayedClientEventsOverrides: Partial = {}; delayedClientFeatureEventsOverrides: Partial = {}; /** * Constructor * @param args * @constructor * @private * @returns */ constructor(...args) { super(...args); // @ts-ignore this.callDiagnosticLatencies = new CallDiagnosticLatencies({}, {parent: this.webex}); this.onReady(); } /** * On Ready */ private onReady() { // @ts-ignore this.webex.once('ready', () => { // @ts-ignore this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex}); this.preLoginMetrics = new PreLoginMetrics( // @ts-ignore new PreLoginMetricsBatcher({}, {parent: this.webex}), {}, // @ts-ignore {parent: this.webex} ); this.isReady = true; this.setDelaySubmitClientEvents({ shouldDelay: this.delaySubmitClientEvents, overrides: this.delayedClientEventsOverrides, }); }); } /** * Used for internal purposes only * @param args */ submitInternalEvent({ name, payload, options, }: { name: InternalEvent['name']; payload?: RecursivePartial; options?: any; }) { if (name === 'internal.reset.join.latencies') { this.callDiagnosticLatencies.clearTimestamps(); } else { this.callDiagnosticLatencies.saveTimestamp({key: name}); } } /** * if webex metrics is ready, build behavioral metric backend if not already done. */ private lazyBuildBehavioralMetrics() { if (this.isReady && !this.behavioralMetrics) { // @ts-ignore this.behavioralMetrics = new BehavioralMetrics({}, {parent: this.webex}); } } /** * if webex metrics is ready, build operational metric backend if not already done. */ private lazyBuildOperationalMetrics() { if (this.isReady && !this.operationalMetrics) { // @ts-ignore this.operationalMetrics = new OperationalMetrics({}, {parent: this.webex}); } } /** * if webex metrics is ready, build business metric backend if not already done. */ private lazyBuildBusinessMetrics() { if (this.isReady && !this.businessMetrics) { // @ts-ignore this.businessMetrics = new BusinessMetrics({}, {parent: this.webex}); } } /** * @returns true once we have the deviceId we need to submit behavioral events to Amplitude */ isReadyToSubmitBehavioralEvents() { this.lazyBuildBehavioralMetrics(); return this.behavioralMetrics?.isReadyToSubmitEvents() ?? false; } /** * @returns true once we have the deviceId we need to submit operational events */ isReadyToSubmitOperationalEvents() { this.lazyBuildOperationalMetrics(); return this.operationalMetrics?.isReadyToSubmitEvents() ?? false; } /** * @returns true once we have the deviceId we need to submit business events */ isReadyToSubmitBusinessEvents() { this.lazyBuildBusinessMetrics(); return this.businessMetrics?.isReadyToSubmitEvents() ?? false; } /** * Behavioral event * @param args */ submitBehavioralEvent({ product, agent, target, verb, payload, }: { product: MetricEventProduct; agent: MetricEventAgent; target: string; verb: MetricEventVerb; payload?: EventPayload; }) { if (!this.isReady) { // @ts-ignore this.webex.logger.log( `NewMetrics: @submitBehavioralEvent. Attempted to submit before webex.ready: ${product}.${agent}.${target}.${verb}` ); return Promise.resolve(); } this.lazyBuildBehavioralMetrics(); return this.behavioralMetrics.submitBehavioralEvent({product, agent, target, verb, payload}); } /** * Operational event * @param args */ submitOperationalEvent({name, payload}: {name: string; payload?: EventPayload}) { if (!this.isReady) { // @ts-ignore this.webex.logger.log( `NewMetrics: @submitOperationalEvent. Attempted to submit before webex.ready: ${name}` ); return Promise.resolve(); } this.lazyBuildOperationalMetrics(); return this.operationalMetrics.submitOperationalEvent({name, payload}); } /** * Business event * @param args */ submitBusinessEvent({ name, payload, table, metadata, }: { name: string; payload: EventPayload; table?: Table; metadata?: EventPayload; }) { if (!this.isReady) { // @ts-ignore this.webex.logger.log( `NewMetrics: @submitBusinessEvent. Attempted to submit before webex.ready: ${name}` ); return Promise.resolve(); } this.lazyBuildBusinessMetrics(); return this.businessMetrics.submitBusinessEvent({name, payload, table, metadata}); } /** * Call Analyzer: Pre-Login Event * @param args */ submitPreLoginEvent({ name, preLoginId, payload, metadata, }: { name: string; preLoginId: string; payload: EventPayload; metadata?: EventPayload; }): Promise { if (!this.isReady) { // @ts-ignore this.webex.logger.log( `NewMetrics: @submitPreLoginEvent. Attempted to submit before webex.ready: ${name}` ); return Promise.resolve(); } return this.preLoginMetrics.submitPreLoginEvent({ name, preLoginId, payload, metadata, }); } /** * Call Analyzer: Media Quality Event * @param args */ submitMQE({ name, payload, options, }: { name: MediaQualityEvent['name']; payload: RecursivePartial & { intervals: MediaQualityEvent['payload']['intervals']; }; options: any; }) { this.callDiagnosticLatencies.saveTimestamp({key: name}); this.callDiagnosticMetrics.submitMQE({name, payload, options}); } /** * Call Analyzer: Feature Usage Event * @param args */ submitFeatureEvent({ name, payload, options, }: { name: FeatureEvent['name']; payload?: RecursivePartial; options: any; }) { if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) { // @ts-ignore this.webex.logger.log( `NewMetrics: @submitFeatureEvent. Attempted to submit before webex.ready. Event name: ${name}` ); return Promise.resolve(); } this.callDiagnosticLatencies.saveTimestamp({ key: name, options: {meetingId: options?.meetingId}, }); return this.callDiagnosticMetrics.submitFeatureEvent({ name, payload, options, delaySubmitEvent: this.delaySubmitClientFeatureEvents, }); } /** * Call Analyzer: Client Event * @public * @param args */ public submitClientEvent({ name, payload, options, }: { name: ClientEvent['name']; payload?: RecursivePartial; options?: SubmitClientEventOptions; }): Promise { if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) { // @ts-ignore this.webex.logger.log( `NewMetrics: @submitClientEvent. Attempted to submit before webex.ready. Event name: ${name}` ); return Promise.resolve(); } this.callDiagnosticLatencies.saveTimestamp({ key: name, options: {meetingId: options?.meetingId}, }); return this.callDiagnosticMetrics.submitClientEvent({ name, payload, options, delaySubmitEvent: this.delaySubmitClientEvents, }); } /** * Issue request to alias a user's pre-login ID with their CI UUID * @param {string} preLoginId * @returns {Object} HttpResponse object */ public clientMetricsAliasUser(preLoginId: string) { // @ts-ignore return this.webex .request({ method: 'POST', api: 'metrics', resource: 'clientmetrics', headers: { 'x-prelogin-userid': preLoginId, }, body: {}, qs: { alias: true, }, }) .then((res) => { // @ts-ignore this.webex.logger.log(`NewMetrics: @clientMetricsAliasUser. Request successful.`); return res; }) .catch((err) => { // @ts-ignore this.logger.error( `NewMetrics: @clientMetricsAliasUser. Request failed:`, `err: ${generateCommonErrorMetadata(err)}` ); return Promise.reject(err); }); } /** * Returns a promise that will resolve to fetch options for submitting a metric. * * This is to support quickly submitting metrics when the browser/tab is closing. * Calling submitClientEvent will not work because there some async steps that will * not complete before the browser is closed. Instead, we pre-gather all the * information/options needed for the request(s), and then simply and quickly * fire the fetch(es) when beforeUnload is triggered. * * We must use fetch instead of request because fetch has a keepalive option that * allows the request it to outlive the page. * * Note: the timings values will be wrong, but setMetricTimingsAndFetch() will * properly adjust them before submitting. * * @public * @param {Object} arg * @param {String} arg.name - event name * @param {Object} arg.payload - event payload * @param {Object} arg.options - other options * @returns {Promise} promise that resolves to options to be used with fetch */ public async buildClientEventFetchRequestOptions({ name, payload, options, }: { name: ClientEvent['name']; payload?: RecursivePartial; options?: SubmitClientEventOptions; }): Promise { return this.callDiagnosticMetrics.buildClientEventFetchRequestOptions({ name, payload, options, }); } /** * Submits a metric from pre-built request options via the fetch API. Updates * the "$timings" and "originTime" values to Date.now() since the existing times * were set when the options were built (not submitted). * @param {any} options - the pre-built request options for submitting a metric * @returns {Promise} promise that resolves to the response object */ public setMetricTimingsAndFetch(options: any): Promise { // @ts-ignore return this.webex.setTimingsAndFetch(setMetricTimings(options)); } /** * Returns true if the specified serviceErrorCode maps to an expected error. * @param {number} serviceErrorCode the service error code * @returns {boolean} */ public isServiceErrorExpected(serviceErrorCode: number): boolean { return this.callDiagnosticMetrics.isServiceErrorExpected(serviceErrorCode); } /** * Sets the value of delaySubmitClientEvents. If set to true, client events will be delayed until submitDelayedClientEvents is called. If * set to false, delayed client events will be submitted. * * @param {object} options - {shouldDelay: A boolean value indicating whether to delay the submission of client events, overrides: An object containing overrides for the client events} */ public setDelaySubmitClientEvents({ shouldDelay, overrides, }: { shouldDelay: boolean; overrides?: Partial; }) { this.delaySubmitClientEvents = shouldDelay; this.delayedClientEventsOverrides = overrides || {}; if (this.isReady && !shouldDelay) { return this.callDiagnosticMetrics.submitDelayedClientEvents(overrides); } return Promise.resolve(); } /** * Sets the value of setDelaySubmitClientFeatureEvents. * If set to true, feature events will be delayed until submitDelayedClientFeatureEvents is called. * If set to false, delayed feature events will be submitted. * * @param {object} options - {shouldDelay: A boolean value indicating whether to delay the submission of feature events, * overrides: An object containing overrides for the feature events} */ public setDelaySubmitClientFeatureEvents({ shouldDelay, overrides, }: { shouldDelay: boolean; overrides?: Partial; }) { this.delaySubmitClientFeatureEvents = shouldDelay; this.delayedClientFeatureEventsOverrides = overrides || {}; if (this.isReady && !shouldDelay) { return this.callDiagnosticMetrics.submitDelayedClientFeatureEvents(overrides); } return Promise.resolve(); } } export default Metrics;