Add video playback observability to Sentry (#11111)

This commit is contained in:
Spence Pope
2026-07-14 10:38:39 -04:00
committed by GitHub
parent 30e862bc26
commit a5adb0e97a
6 changed files with 324 additions and 6 deletions
@@ -0,0 +1,120 @@
import {createPlaybackTelemetry} from '../playbackTelemetry'
const mockSpan = {
setAttribute: jest.fn(),
end: jest.fn(),
}
jest.mock('#/logger/sentry/lib', () => ({
Sentry: {
startInactiveSpan: jest.fn(() => mockSpan),
},
}))
const {Sentry}: {Sentry: {startInactiveSpan: jest.Mock}} = jest.requireMock(
'#/logger/sentry/lib',
)
describe('createPlaybackTelemetry', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('starts a root span on activation with surface and presentation', () => {
const telemetry = createPlaybackTelemetry({
surface: 'feed',
presentation: 'video',
})
telemetry.activated()
expect(Sentry.startInactiveSpan).toHaveBeenCalledWith({
name: 'video.playback',
op: 'video.playback',
attributes: {surface: 'feed', presentation: 'video'},
})
})
it('does nothing before activation', () => {
const telemetry = createPlaybackTelemetry({
surface: 'feed',
presentation: 'video',
})
telemetry.ready()
telemetry.playing()
telemetry.deactivated()
expect(Sentry.startInactiveSpan).not.toHaveBeenCalled()
expect(mockSpan.end).not.toHaveBeenCalled()
})
it('ignores duplicate activations while a span is open', () => {
const telemetry = createPlaybackTelemetry({
surface: 'feed',
presentation: 'video',
})
telemetry.activated()
telemetry.activated()
expect(Sentry.startInactiveSpan).toHaveBeenCalledTimes(1)
})
it('records ready and playing times once', () => {
const telemetry = createPlaybackTelemetry({
surface: 'feed',
presentation: 'video',
})
telemetry.activated()
telemetry.ready()
telemetry.ready()
telemetry.playing()
telemetry.playing()
const calls = mockSpan.setAttribute.mock.calls as [string, unknown][]
const attrs = calls.map(c => c[0])
expect(attrs.filter(a => a === 'timeToReadyMs')).toHaveLength(1)
expect(attrs.filter(a => a === 'timeToFirstPlayMs')).toHaveLength(1)
})
it('ends the span with ok outcome on deactivation, idempotently', () => {
const telemetry = createPlaybackTelemetry({
surface: 'feed',
presentation: 'video',
})
telemetry.activated()
telemetry.deactivated()
telemetry.deactivated()
expect(mockSpan.setAttribute).toHaveBeenCalledWith('outcome', 'ok')
expect(mockSpan.end).toHaveBeenCalledTimes(1)
})
it('ends the span with error outcome and message', () => {
const telemetry = createPlaybackTelemetry({
surface: 'feed',
presentation: 'video',
})
telemetry.activated()
telemetry.error('AVFoundationErrorDomain -11850')
expect(mockSpan.setAttribute).toHaveBeenCalledWith(
'errorMessage',
'AVFoundationErrorDomain -11850',
)
expect(mockSpan.setAttribute).toHaveBeenCalledWith('outcome', 'error')
expect(mockSpan.end).toHaveBeenCalledTimes(1)
})
it('starts a new span for a new activation window', () => {
const telemetry = createPlaybackTelemetry({
surface: 'immersiveFeed',
presentation: 'video',
})
telemetry.activated({preloaded: true})
telemetry.deactivated()
telemetry.activated({preloaded: false})
expect(Sentry.startInactiveSpan).toHaveBeenCalledTimes(2)
expect(Sentry.startInactiveSpan).toHaveBeenLastCalledWith({
name: 'video.playback',
op: 'video.playback',
attributes: {
surface: 'immersiveFeed',
presentation: 'video',
preloaded: false,
},
})
})
})
+83
View File
@@ -0,0 +1,83 @@
import {Sentry} from '#/logger/sentry/lib'
/**
* Where the video is being played.
*/
export type PlaybackSurface = 'feed' | 'immersiveFeed'
export type PlaybackTelemetry = {
activated: (opts?: {preloaded?: boolean}) => void
ready: () => void
playing: () => void
error: (e: unknown) => void
deactivated: () => void
}
/**
* Sentry-only observability for video playback in feeds. Opens a root span
* per activation window (the video becomes the active, autoplaying one) and
* ends it on deactivation. Because these are root spans, the SDK attaches JS
* stall and slow/frozen frame measurements to them, capturing scroll
* smoothness while a video is on screen.
*
* Does not report to the analytics pipeline, only Sentry.
*/
export function createPlaybackTelemetry({
surface,
presentation,
}: {
surface: PlaybackSurface
presentation: 'video' | 'gif'
}): PlaybackTelemetry {
let span: ReturnType<typeof Sentry.startInactiveSpan> | undefined
let activatedAt = 0
let sawReady = false
let sawPlaying = false
function end(outcome: 'ok' | 'error') {
if (!span) return
span.setAttribute('outcome', outcome)
span.end()
span = undefined
}
return {
activated(opts) {
if (span) return
activatedAt = Date.now()
sawReady = false
sawPlaying = false
span = Sentry.startInactiveSpan({
name: 'video.playback',
op: 'video.playback',
attributes: {
surface,
presentation,
...(opts?.preloaded !== undefined && {preloaded: opts.preloaded}),
},
})
},
ready() {
if (!span || sawReady) return
sawReady = true
span.setAttribute('timeToReadyMs', Date.now() - activatedAt)
},
playing() {
if (!span || sawPlaying) return
sawPlaying = true
span.setAttribute('timeToFirstPlayMs', Date.now() - activatedAt)
},
error(e) {
if (!span) return
span.setAttribute('errorMessage', String(e).slice(0, 256))
end('error')
},
deactivated() {
end('ok')
},
}
}