Add in Sentry, WebRTC Statistics (#345)

Collect WebRTC Statistical Information

Add in some instrumentation to track the duration of the setup phase,
and set up a job to make periodic use of the WebRTC Statistics API
to collect information about the connection (specifically, each track),
including transport-level information, timing information and bandwidth
information.

Sentry isn't the best place for that information, but it'll work until we
can work out a good long-term solution for it.

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
This commit is contained in:
Paul Tagliamonte
2023-08-30 10:34:14 -04:00
committed by GitHub
parent 1fbd0ad675
commit 659e6d5b45
7 changed files with 277 additions and 4 deletions

View File

@ -1,9 +1,14 @@
import { SourceRange } from 'lang/executor'
import { Selections } from 'useStore'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import {
VITE_KC_API_WS_MODELING_URL,
VITE_KC_CONNECTION_TIMEOUT_MS,
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
} from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
interface ResultCommand {
type: 'result'
@ -116,6 +121,16 @@ export class EngineConnection {
// TODO(paultag): make this safe to call multiple times, and figure out
// when a connection is in progress (state: connecting or something).
// Information on the connect transaction
const webrtcMediaTransaction = Sentry.startTransaction({
name: 'webrtc-media',
})
const websocketSpan = webrtcMediaTransaction.startChild({ op: 'websocket' })
let mediaTrackSpan: Sentry.Span
let dataChannelSpan: Sentry.Span
let handshakeSpan: Sentry.Span
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
@ -129,6 +144,14 @@ export class EngineConnection {
})
this.websocket.addEventListener('open', (event) => {
// websocketSpan.setStatus(SpanStatus.OK)
websocketSpan.finish()
handshakeSpan = webrtcMediaTransaction.startChild({ op: 'handshake' })
dataChannelSpan = webrtcMediaTransaction.startChild({
op: 'data-channel',
})
mediaTrackSpan = webrtcMediaTransaction.startChild({ op: 'media-track' })
this.onWebsocketOpen(this)
})
@ -191,6 +214,11 @@ export class EngineConnection {
sdp: answer.sdp,
})
)
// When both ends have a local and remote SDP, we've been able to
// set up successfully. We'll still need to find the right ICE
// servers, but this is hand-shook.
handshakeSpan.finish()
}
} else if (resp.type === 'trickle_ice') {
let candidate = resp.data?.candidate
@ -274,6 +302,134 @@ export class EngineConnection {
this.pc.addEventListener('track', (event) => {
console.log('received track', event)
const mediaStream = event.streams[0]
mediaStream.getVideoTracks()[0].addEventListener('unmute', () => {
mediaTrackSpan.finish()
webrtcMediaTransaction.finish()
})
// Set up the background thread to keep an eye on statistical
// information about the WebRTC media stream from the server to
// us. We'll also eventually want more global statistical information,
// but this will give us a baseline.
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
setInterval(() => {
if (this.pc === undefined) {
return
}
console.log('Reporting statistics')
// Use the WebRTC Statistics API to collect statistical information
// about the WebRTC connection we're using to report to Sentry.
mediaStream.getVideoTracks().forEach((videoTrack) => {
let trackStats = new Map<string, any>()
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// Sentry only allows 10 metrics per transaction. We're going
// to have to pick carefully here, eventually send like a prom
// file or something to the peer.
const transaction = Sentry.startTransaction({
name: 'webrtc-stats',
})
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
// RTC Stream Info
// transaction.setMeasurement(
// 'mediaStreamTrack.framesDecoded',
// videoTrackReport.framesDecoded,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesDropped',
videoTrackReport.framesDropped,
''
)
// transaction.setMeasurement(
// 'mediaStreamTrack.framesReceived',
// videoTrackReport.framesReceived,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesPerSecond',
videoTrackReport.framesPerSecond,
'fps'
)
transaction.setMeasurement(
'rtcFreezeCount',
videoTrackReport.freezeCount,
''
)
transaction.setMeasurement(
'rtcJitter',
videoTrackReport.jitter,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferDelay',
// videoTrackReport.jitterBufferDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferEmittedCount',
// videoTrackReport.jitterBufferEmittedCount,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferMinimumDelay',
// videoTrackReport.jitterBufferMinimumDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferTargetDelay',
// videoTrackReport.jitterBufferTargetDelay,
// ''
// )
transaction.setMeasurement(
'rtcKeyFramesDecoded',
videoTrackReport.keyFramesDecoded,
''
)
transaction.setMeasurement(
'rtcTotalFreezesDuration',
videoTrackReport.totalFreezesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalInterFrameDelay',
// videoTrackReport.totalInterFrameDelay,
// ''
// )
transaction.setMeasurement(
'rtcTotalPausesDuration',
videoTrackReport.totalPausesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalProcessingDelay',
// videoTrackReport.totalProcessingDelay,
// 'second'
// )
} else if (videoTrackReport.type === 'transport') {
// // Bytes i/o
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesReceived',
// videoTrackReport.bytesReceived,
// 'byte'
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesSent',
// videoTrackReport.bytesSent,
// 'byte'
// )
}
})
transaction.finish()
})
})
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
}
this.onNewTrack({
conn: this,
mediaStream: mediaStream,
@ -290,11 +446,10 @@ export class EngineConnection {
console.log('accepted lossy data channel', event.channel.label)
this.lossyDataChannel.addEventListener('open', (event) => {
console.log('lossy data channel opened', event)
dataChannelSpan.finish()
this.onDataChannelOpen(this)
let timeToConnectMs = new Date().getTime() - connectionStarted.getTime()
console.log(`engine connection time to connect: ${timeToConnectMs}ms`)
this.onEngineConnectionOpen(this)
this.ready = true
})