UNPKG

@observertc/observer-js

Version:

Server Side NodeJS Library for processing ObserveRTC Samples

637 lines (636 loc) 31.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObservedClient = void 0; const events_1 = require("events"); const ObservedPeerConnection_1 = require("./ObservedPeerConnection"); const logger_1 = require("./common/logger"); const ClientEventTypes_1 = require("./schema/ClientEventTypes"); const ClientMetaTypes_1 = require("./schema/ClientMetaTypes"); const utils_1 = require("./common/utils"); const Detectors_1 = require("./detectors/Detectors"); const logger = (0, logger_1.createLogger)('ObservedClient'); class ObservedClient extends events_1.EventEmitter { call; detectors; clientId; observedPeerConnections = new Map(); calculatedScore = { weight: 1, value: undefined, }; appData; attachments; updated = Date.now(); acceptedSamples = 0; closed = false; joinedAt; leftAt; closedAt; lastSampleTimestamp; // the timestamp of the CLIENT_JOINED event operationSystem; engine; platform; browser; mediaConstraints = []; usingTURN = false; usingTCP = false; availableOutgoingBitrate = 0; availableIncomingBitrate = 0; totalInboundPacketsLost = 0; totalInboundPacketsReceived = 0; totalOutboundPacketsSent = 0; totalDataChannelBytesSent = 0; totalDataChannelBytesReceived = 0; totalDataChannelMessagesSent = 0; totalDataChannelMessagesReceived = 0; totalReceivedAudioBytes = 0; totalReceivedVideoBytes = 0; totalSentAudioBytes = 0; totalSentVideoBytes = 0; totalSentBytes = 0; totalReceivedBytes = 0; deltaReceivedAudioBytes = 0; deltaReceivedVideoBytes = 0; deltaSentAudioBytes = 0; deltaSentVideoBytes = 0; deltaDataChannelBytesSent = 0; deltaDataChannelBytesReceived = 0; deltaDataChannelMessagesSent = 0; deltaDataChannelMessagesReceived = 0; deltaInboundPacketsLost = 0; deltaInboundPacketsReceived = 0; deltaOutboundPacketsSent = 0; deltaTransportSentBytes = 0; deltaTransportReceivedBytes = 0; deltaRttLt50Measurements = 0; deltaRttLt150Measurements = 0; deltaRttLt300Measurements = 0; deltaRttGtOrEq300Measurements = 0; currentMaxRttInMs; currentMinRttInMs; currentAvgRttInMs; sendingAudioBitrate = 0; sendingVideoBitrate = 0; receivingAudioBitrate = 0; receivingVideoBitrate = 0; numberOfInboundRtpStreams = 0; numberOfInbundTracks = 0; numberOfOutboundRtpStreams = 0; numberOfOutboundTracks = 0; numberOfDataChannels = 0; totalRttLt50Measurements = 0; totalRttLt150Measurements = 0; totalRttLt300Measurements = 0; totalRttGtOrEq300Measurements = 0; deltaNumberOfIssues = 0; totalScoreSum = 0; numberOfScoreMeasurements = 0; totalNumberOfIssues = 0; mediaDevices = []; issues = []; _injections = {}; constructor(settings, call) { super(); this.call = call; this.setMaxListeners(Infinity); this.clientId = settings.clientId; this.appData = settings.appData ?? {}; this.detectors = new Detectors_1.Detectors(); } get numberOfPeerConnections() { return this.observedPeerConnections.size; } get score() { return this.calculatedScore.value; } close() { if (this.closed) return; this.closed = true; this._injections.clientEvents?.forEach((clientEvent) => this._processClientEvent(clientEvent)); this._injections.clientIssues?.forEach((clientIssue) => this.addIssue(clientIssue)); this._injections.extensionStats?.forEach((extensionStat) => this.addExtensionStats(extensionStat)); this._injections.clientMetaItems?.forEach((clientMetaItem) => this.addMetadata(clientMetaItem)); Array.from(this.observedPeerConnections.values()).forEach((peerConnection) => peerConnection.close()); if (!this.leftAt) { this.leftAt = this.lastSampleTimestamp; if (this.leftAt) { this.emit('left'); } } this.closedAt = Date.now(); this.emit('close'); } accept(sample) { if (this.closed) throw new Error(`Client ${this.clientId} is closed`); const now = Date.now(); const elapsedInMs = now - this.updated; const elapsedInSeconds = elapsedInMs / 1000; let sumOfRtts = 0; let numberOfRttMeasurements = 0; ++this.acceptedSamples; this.availableIncomingBitrate = 0; this.availableOutgoingBitrate = 0; this.deltaDataChannelBytesReceived = 0; this.deltaDataChannelBytesSent = 0; this.deltaDataChannelMessagesReceived = 0; this.deltaDataChannelMessagesSent = 0; this.deltaInboundPacketsLost = 0; this.deltaInboundPacketsReceived = 0; this.deltaOutboundPacketsSent = 0; this.deltaReceivedAudioBytes = 0; this.deltaReceivedVideoBytes = 0; this.deltaSentAudioBytes = 0; this.deltaSentVideoBytes = 0; this.deltaTransportReceivedBytes = 0; this.deltaTransportSentBytes = 0; this.deltaRttLt50Measurements = 0; this.deltaRttLt150Measurements = 0; this.deltaRttLt300Measurements = 0; this.deltaRttGtOrEq300Measurements = 0; this.deltaNumberOfIssues = 0; this.numberOfDataChannels = 0; this.numberOfInboundRtpStreams = 0; this.numberOfInbundTracks = 0; this.numberOfOutboundRtpStreams = 0; this.numberOfOutboundTracks = 0; this.usingTURN = false; this.usingTCP = false; this.currentMinRttInMs = undefined; this.currentMaxRttInMs = undefined; this._mergeInjections(sample); const clientEventsPostBuffer = []; for (const clientEvent of sample.clientEvents ?? []) { this._processClientEvent(clientEvent, clientEventsPostBuffer); this.call.observer.emit('client-event', this, clientEvent); } for (const metaData of sample.clientMetaItems ?? []) { this.addMetadata(metaData); } for (const issue of sample.clientIssues ?? []) { this.addIssue(issue); ++this.deltaNumberOfIssues; } for (const extensionStat of sample.extensionStats ?? []) { this.addExtensionStats(extensionStat); } for (const pcSample of sample.peerConnections ?? []) { const observedPeerConnection = this._updatePeerConnection(pcSample); if (!observedPeerConnection) continue; this.deltaDataChannelBytesReceived += observedPeerConnection.deltaDataChannelBytesReceived; this.deltaDataChannelBytesSent += observedPeerConnection.deltaDataChannelBytesSent; this.deltaDataChannelMessagesReceived += observedPeerConnection.deltaDataChannelMessagesReceived; this.deltaDataChannelMessagesSent += observedPeerConnection.deltaDataChannelMessagesSent; this.deltaInboundPacketsLost += observedPeerConnection.deltaInboundPacketsLost; this.deltaInboundPacketsReceived += observedPeerConnection.deltaInboundPacketsReceived; this.deltaOutboundPacketsSent += observedPeerConnection.deltaOutboundPacketsSent; this.deltaReceivedAudioBytes += observedPeerConnection.deltaReceivedAudioBytes; this.deltaReceivedVideoBytes += observedPeerConnection.deltaReceivedVideoBytes; this.deltaSentAudioBytes += observedPeerConnection.deltaSentAudioBytes; this.deltaSentVideoBytes += observedPeerConnection.deltaSentVideoBytes; this.deltaTransportReceivedBytes += observedPeerConnection.deltaTransportReceivedBytes; this.deltaTransportSentBytes += observedPeerConnection.deltaTransportSentBytes; this.availableIncomingBitrate += observedPeerConnection.availableIncomingBitrate; this.availableOutgoingBitrate += observedPeerConnection.availableOutgoingBitrate; this.numberOfDataChannels += observedPeerConnection.observedDataChannels.size; this.numberOfInbundTracks += observedPeerConnection.observedInboundTracks.size; this.numberOfOutboundRtpStreams += observedPeerConnection.observedOutboundRtps.size; this.numberOfOutboundTracks += observedPeerConnection.observedOutboundTracks.size; this.numberOfInboundRtpStreams += observedPeerConnection.observedInboundRtps.size; if (observedPeerConnection.usingTURN) { this.usingTURN = true; } if (observedPeerConnection.usingTCP) { this.usingTCP = true; } if (observedPeerConnection.currentRttInMs) { if (this.currentMinRttInMs === undefined || observedPeerConnection.currentRttInMs < this.currentMinRttInMs) { this.currentMinRttInMs = observedPeerConnection.currentRttInMs; } if (this.currentMaxRttInMs === undefined || observedPeerConnection.currentRttInMs > this.currentMaxRttInMs) { this.currentMaxRttInMs = observedPeerConnection.currentRttInMs; } if (observedPeerConnection.currentRttInMs < 50) { this.deltaRttLt50Measurements += 1; } else if (observedPeerConnection.currentRttInMs < 150) { this.deltaRttLt150Measurements += 1; } else if (observedPeerConnection.currentRttInMs < 300) { this.deltaRttLt300Measurements += 1; } else if (300 <= observedPeerConnection.currentRttInMs) { this.deltaRttGtOrEq300Measurements += 1; } sumOfRtts += observedPeerConnection.currentRttInMs; ++numberOfRttMeasurements; } } for (const clientEvent of clientEventsPostBuffer) { this._processClientEvent(clientEvent); } // emit new attachments? this.attachments = sample.attachments; this.totalDataChannelBytesReceived += this.deltaDataChannelBytesReceived; this.totalDataChannelBytesSent += this.deltaDataChannelBytesSent; this.totalDataChannelMessagesReceived += this.deltaDataChannelMessagesReceived; this.totalDataChannelMessagesSent += this.deltaDataChannelMessagesSent; this.totalInboundPacketsLost += this.deltaInboundPacketsLost; this.totalInboundPacketsReceived += this.deltaInboundPacketsReceived; this.totalOutboundPacketsSent += this.deltaOutboundPacketsSent; this.totalReceivedAudioBytes += this.deltaReceivedAudioBytes; this.totalReceivedVideoBytes += this.deltaReceivedVideoBytes; this.totalSentAudioBytes += this.deltaSentAudioBytes; this.totalSentVideoBytes += this.deltaSentVideoBytes; this.totalReceivedBytes += this.deltaTransportReceivedBytes; this.totalSentBytes += this.deltaTransportSentBytes; this.totalRttLt50Measurements += this.deltaRttLt50Measurements; this.totalRttLt150Measurements += this.deltaRttLt150Measurements; this.totalRttLt300Measurements += this.deltaRttLt300Measurements; this.totalRttGtOrEq300Measurements += this.deltaRttGtOrEq300Measurements; this.totalNumberOfIssues += this.deltaNumberOfIssues; this.receivingAudioBitrate = (this.deltaReceivedAudioBytes * 8) / (elapsedInSeconds); this.receivingVideoBitrate = (this.totalReceivedVideoBytes * 8) / (elapsedInSeconds); this.sendingAudioBitrate = (this.deltaSentAudioBytes * 8) / (elapsedInSeconds); this.sendingVideoBitrate = (this.deltaSentVideoBytes * 8) / (elapsedInSeconds); this.currentAvgRttInMs = 0 < numberOfRttMeasurements ? sumOfRtts / numberOfRttMeasurements : undefined; this.calculatedScore.value = sample.score; this.detectors.update(); this.lastSampleTimestamp = sample.timestamp; // emit update this.emit('update', sample, now - this.updated); this.updated = now; // if result changed after update if (this.calculatedScore.value) { this.totalScoreSum += this.calculatedScore.value; ++this.numberOfScoreMeasurements; } } _processClientEvent(event, postBuffer) { // eslint-disable-next-line no-console // console.warn('ClientEvent', event); switch (event.type) { case ClientEventTypes_1.ClientEventTypes.CLIENT_JOINED: { if (event.timestamp) { if (!this.joinedAt) { this.joinedAt = event.timestamp; this.emit('joined'); } else if (this.joinedAt < event.timestamp) { this.emit('rejoined', event.timestamp); } else { this.joinedAt = event.timestamp; logger.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`); } } logger.debug('Client %s joined at %o', this.clientId, event); break; } case ClientEventTypes_1.ClientEventTypes.CLIENT_LEFT: { if (event.timestamp) { if (!this.leftAt) { this.leftAt = event.timestamp; this.emit('left'); } else { logger.warn(`Client ${this.clientId} leftAt timestamp was already set`); } } logger.debug('Client %s left at %o', this.clientId, event); break; } case ClientEventTypes_1.ClientEventTypes.PEER_CONNECTION_OPENED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); if (observedPeerConnection) { observedPeerConnection.openedAt = event.timestamp; } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.PEER_CONNECTION_CLOSED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); if (observedPeerConnection) { observedPeerConnection.closedAt = event.timestamp; } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.MEDIA_TRACK_ADDED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); const observedTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId) ?? observedPeerConnection?.observedOutboundTracks.get(payload.trackId); if (observedTrack) { observedTrack.addedAt = event.timestamp; } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.MEDIA_TRACK_REMOVED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); const observedTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId) ?? observedPeerConnection?.observedOutboundTracks.get(payload.trackId); if (observedTrack) { observedTrack.removedAt = event.timestamp; } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.DATA_CHANNEL_OPEN: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.dataChannelId && typeof payload.dataChannelId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); const observedDataChannel = observedPeerConnection?.observedDataChannels.get(payload.dataChannelId); if (observedDataChannel) { observedDataChannel.addedAt = event.timestamp; } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.DATA_CHANNEL_CLOSED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.dataChannelId && typeof payload.dataChannelId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); const observedDataChannel = observedPeerConnection?.observedDataChannels.get(payload.dataChannelId); if (observedDataChannel) { observedDataChannel.removedAt = event.timestamp; } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.MEDIA_TRACK_MUTED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); const observedInboundTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId); const observedOutboundTrack = observedPeerConnection?.observedOutboundTracks.get(payload.trackId); if (observedPeerConnection) { if (observedInboundTrack) { observedInboundTrack.muted = true; observedPeerConnection?.emit('muted-inbound-track', observedInboundTrack); } else if (observedOutboundTrack) { observedOutboundTrack.muted = true; observedPeerConnection?.emit('muted-outbound-track', observedOutboundTrack); } } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.MEDIA_TRACK_UNMUTED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); const observedInboundTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId); const observedOutboundTrack = observedPeerConnection?.observedOutboundTracks.get(payload.trackId); if (observedPeerConnection) { if (observedInboundTrack) { observedInboundTrack.muted = false; observedPeerConnection?.emit('unmuted-inbound-track', observedInboundTrack); } else if (observedOutboundTrack) { observedOutboundTrack.muted = false; observedPeerConnection?.emit('unmuted-outbound-track', observedOutboundTrack); } } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.ICE_CONNECTION_STATE_CHANGED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.iceConnectionState && typeof payload.iceConnectionState === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); if (observedPeerConnection) { observedPeerConnection.iceConnectionState = payload.iceConnectionState; observedPeerConnection.emit('iceconnectionstatechange', { state: payload.iceConnectionState, }); } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.ICE_GATHERING_STATE_CHANGED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.iceGatheringState && typeof payload.iceGatheringState === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); if (observedPeerConnection) { observedPeerConnection.iceGatheringState = payload.iceGatheringState; observedPeerConnection.emit('icegatheringstatechange', { state: payload.iceGatheringState, }); } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event); } } break; } case ClientEventTypes_1.ClientEventTypes.PEER_CONNECTION_STATE_CHANGED: { const payload = (0, utils_1.parseJsonAs)(event.payload); if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.peerConnectionState && typeof payload.peerConnectionState === 'string') { const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId); if (observedPeerConnection) { observedPeerConnection.connectionState = payload.peerConnectionState; observedPeerConnection.emit('connectionstatechange', { state: payload.peerConnectionState, }); } else if (postBuffer) { postBuffer.push(event); } else { logger.warn('Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event); } } break; } } this.emit('clientEvent', event); } injectMetaData(metaData) { if (this.closed) return; if (!this._injections.clientMetaItems) this._injections.clientMetaItems = []; this._injections.clientMetaItems.push(metaData); } injectEvent(event) { if (this.closed) return; if (!this._injections.clientEvents) this._injections.clientEvents = []; this._injections.clientEvents.push(event); } injectIssue(issue) { if (this.closed) return; if (!this._injections.clientIssues) this._injections.clientIssues = []; this._injections.clientIssues.push(issue); } injectExtensionStat(stat) { if (this.closed) return; if (!this._injections.extensionStats) this._injections.extensionStats = []; this._injections.extensionStats.push(stat); } injectAttachment(key, value) { if (this.closed) return; if (!this._injections.attachments) this._injections.attachments = {}; this._injections.attachments[key] = value; } addMetadata(metadata) { if (this.closed) return; switch (metadata.type) { case ClientMetaTypes_1.ClientMetaTypes.BROWSER: { this.browser = (0, utils_1.parseJsonAs)(metadata.payload); break; } case ClientMetaTypes_1.ClientMetaTypes.ENGINE: { this.engine = (0, utils_1.parseJsonAs)(metadata.payload); break; } case ClientMetaTypes_1.ClientMetaTypes.PLATFORM: { this.platform = (0, utils_1.parseJsonAs)(metadata.payload); break; } case ClientMetaTypes_1.ClientMetaTypes.OPERATION_SYSTEM: { this.operationSystem = (0, utils_1.parseJsonAs)(metadata.payload); break; } } this.call.observer.emit('client-metadata', this, metadata); } addIssue(issue) { if (this.closed) return; this.emit('issue', issue); this.call.observer.emit('client-issue', this, issue); } addExtensionStats(stats) { this.call.observer.emit('client-extension-stats', this, stats); this.emit('extensionStats', stats); } _updatePeerConnection(sample) { let observedPeerConnection = this.observedPeerConnections.get(sample.peerConnectionId); if (!observedPeerConnection) { if (!sample.peerConnectionId) { return (logger.warn(`ObservedClient received an invalid PeerConnectionSample (missing peerConnectionId field). ClientId: ${this.clientId}, CallId: ${this.call.callId}`, sample), void 0); } observedPeerConnection = new ObservedPeerConnection_1.ObservedPeerConnection(sample.peerConnectionId, this); observedPeerConnection.once('close', () => { this.observedPeerConnections.delete(sample.peerConnectionId); }); this.observedPeerConnections.set(sample.peerConnectionId, observedPeerConnection); this.emit('newpeerconnection', observedPeerConnection); } observedPeerConnection.accept(sample); return observedPeerConnection; } _mergeInjections(sample) { if (this.closed) return sample; if (this._injections.clientEvents) { if (!sample.clientEvents) sample.clientEvents = []; sample.clientEvents.push(...this._injections.clientEvents); this._injections.clientEvents = undefined; } if (this._injections.clientIssues) { if (!sample.clientIssues) sample.clientIssues = []; sample.clientIssues.push(...this._injections.clientIssues); this._injections.clientIssues = undefined; } if (this._injections.extensionStats) { if (!sample.extensionStats) sample.extensionStats = []; sample.extensionStats.push(...this._injections.extensionStats); this._injections.extensionStats = undefined; } if (this._injections.attachments) { if (!sample.attachments) sample.attachments = {}; Object.assign(sample.attachments, this._injections.attachments); this._injections.attachments = undefined; } if (this._injections.clientMetaItems) { if (!sample.clientMetaItems) sample.clientMetaItems = []; sample.clientMetaItems.push(...this._injections.clientMetaItems); this._injections.clientMetaItems = undefined; } return sample; } } exports.ObservedClient = ObservedClient;