Skip to content

Commit

Permalink
🔊 [RUM-253] customize deflate worker failure logs
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitZugmeyer committed Sep 11, 2023
1 parent 50b7549 commit 4fa98c7
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 66 deletions.
1 change: 1 addition & 0 deletions packages/rum/src/boot/recorderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function makeRecorderApi(

const worker = startDeflateWorker(
configuration,
'Datadog Session Replay',
() => {
stopStrategy()
},
Expand Down
2 changes: 1 addition & 1 deletion packages/rum/src/boot/startRecording.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('startRecording', () => {
textField = document.createElement('input')
sandbox.appendChild(textField)

const worker = startDeflateWorker(configuration, noop)
const worker = startDeflateWorker(configuration, 'Datadog Session Replay', noop)

setupBuilder = setup()
.withViewContexts({
Expand Down
129 changes: 74 additions & 55 deletions packages/rum/src/domain/deflate/deflateWorker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RawTelemetryEvent } from '@datadog/browser-core'
import { display, isIE, noop, resetTelemetry, startFakeTelemetry } from '@datadog/browser-core'
import { display, isIE, resetTelemetry, startFakeTelemetry } from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'
import type { Clock } from '@datadog/browser-core/test'
import { mockClock } from '@datadog/browser-core/test'
Expand All @@ -14,10 +14,23 @@ describe('startDeflateWorker', () => {
let mockWorker: MockWorker
let createDeflateWorkerSpy: jasmine.Spy<CreateDeflateWorker>
let onInitializationFailureSpy: jasmine.Spy<() => void>
let configuration: RumConfiguration

function startDeflateWorkerWithDefaults({
configuration = {},
source = 'Datadog Session Replay',
}: {
configuration?: Partial<RumConfiguration>
source?: string
} = {}) {
return startDeflateWorker(
configuration as RumConfiguration,
source,
onInitializationFailureSpy,
createDeflateWorkerSpy
)
}

beforeEach(() => {
configuration = {} as RumConfiguration
mockWorker = new MockWorker()
onInitializationFailureSpy = jasmine.createSpy('onInitializationFailureSpy')
createDeflateWorkerSpy = jasmine.createSpy('createDeflateWorkerSpy').and.callFake(() => mockWorker)
Expand All @@ -28,7 +41,7 @@ describe('startDeflateWorker', () => {
})

it('creates a deflate worker', () => {
const worker = startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy)
const worker = startDeflateWorkerWithDefaults()
expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1)
expect(worker).toBe(mockWorker)

Expand All @@ -37,17 +50,17 @@ describe('startDeflateWorker', () => {
})

it('uses the previously created worker during loading', () => {
const worker1 = startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
const worker2 = startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
const worker1 = startDeflateWorkerWithDefaults()
const worker2 = startDeflateWorkerWithDefaults()
expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1)
expect(worker1).toBe(worker2)
})

it('uses the previously created worker once initialized', () => {
const worker1 = startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
const worker1 = startDeflateWorkerWithDefaults()
mockWorker.processAllMessages()

const worker2 = startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy)
const worker2 = startDeflateWorkerWithDefaults()
expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1)
expect(worker1).toBe(worker2)

Expand All @@ -60,13 +73,11 @@ describe('startDeflateWorker', () => {
// mimic Chrome behavior
let CSP_ERROR: DOMException
let displaySpy: jasmine.Spy
let configuration: RumConfiguration

beforeEach(() => {
if (isIE()) {
pending('IE does not support CSP blocking worker creation')
}
configuration = {} as RumConfiguration
displaySpy = spyOn(display, 'error')
telemetryEvents = startFakeTelemetry()
CSP_ERROR = new DOMException(
Expand All @@ -80,65 +91,66 @@ describe('startDeflateWorker', () => {

describe('Chrome and Safari behavior: exception during worker creation', () => {
it('returns undefined when the worker creation throws an exception', () => {
const worker = startDeflateWorker(configuration, noop, () => {
throw CSP_ERROR
})
createDeflateWorkerSpy.and.throwError(CSP_ERROR)
const worker = startDeflateWorkerWithDefaults()
expect(worker).toBeUndefined()
})

it('displays CSP instructions when the worker creation throws a CSP error', () => {
startDeflateWorker(configuration, noop, () => {
throw CSP_ERROR
})
createDeflateWorkerSpy.and.throwError(CSP_ERROR)
startDeflateWorkerWithDefaults()
expect(displaySpy).toHaveBeenCalledWith(
jasmine.stringContaining('Please make sure CSP is correctly configured')
)
})

it('does not report CSP errors to telemetry', () => {
startDeflateWorker(configuration, noop, () => {
throw CSP_ERROR
})
createDeflateWorkerSpy.and.throwError(CSP_ERROR)
startDeflateWorkerWithDefaults()
expect(telemetryEvents).toEqual([])
})

it('does not try to create a worker again after the creation failed', () => {
startDeflateWorker(configuration, noop, () => {
throw CSP_ERROR
})
startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
createDeflateWorkerSpy.and.throwError(CSP_ERROR)
startDeflateWorkerWithDefaults()
createDeflateWorkerSpy.calls.reset()
startDeflateWorkerWithDefaults()
expect(createDeflateWorkerSpy).not.toHaveBeenCalled()
})
})

describe('Firefox behavior: error during worker loading', () => {
it('displays ErrorEvent as CSP error', () => {
startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
startDeflateWorkerWithDefaults()
mockWorker.dispatchErrorEvent()
expect(displaySpy).toHaveBeenCalledWith(
jasmine.stringContaining('Please make sure CSP is correctly configured')
)
})

it('calls the initialization failure callback when of an error occurs during loading', () => {
startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy)
startDeflateWorkerWithDefaults()
mockWorker.dispatchErrorEvent()
expect(onInitializationFailureSpy).toHaveBeenCalledTimes(1)
})

it('returns undefined if an error occurred in a previous loading', () => {
startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
startDeflateWorkerWithDefaults()
mockWorker.dispatchErrorEvent()
onInitializationFailureSpy.calls.reset()

const worker = startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy)
const worker = startDeflateWorkerWithDefaults()

expect(worker).toBeUndefined()
expect(onInitializationFailureSpy).not.toHaveBeenCalled()
})

it('adjusts the error message when a workerUrl is set', () => {
configuration.workerUrl = '/worker.js'
startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
startDeflateWorkerWithDefaults({
configuration: {
workerUrl: '/worker.js',
},
})
mockWorker.dispatchErrorEvent()
expect(displaySpy).toHaveBeenCalledWith(
jasmine.stringContaining(
Expand All @@ -148,25 +160,25 @@ describe('startDeflateWorker', () => {
})

it('calls all registered callbacks when the worker initialization fails', () => {
const onInitializationFailureSpy1 = jasmine.createSpy()
const onInitializationFailureSpy2 = jasmine.createSpy()
startDeflateWorker(configuration, onInitializationFailureSpy1, createDeflateWorkerSpy)
startDeflateWorker(configuration, onInitializationFailureSpy2, createDeflateWorkerSpy)
startDeflateWorkerWithDefaults()
startDeflateWorkerWithDefaults()
mockWorker.dispatchErrorEvent()
expect(onInitializationFailureSpy1).toHaveBeenCalledTimes(1)
expect(onInitializationFailureSpy2).toHaveBeenCalledTimes(1)
expect(onInitializationFailureSpy).toHaveBeenCalledTimes(2)
})
})
})

describe('initialization timeout', () => {
let displaySpy: jasmine.Spy
let configuration: RumConfiguration
let clock: Clock

beforeEach(() => {
configuration = {} as RumConfiguration
displaySpy = spyOn(display, 'error')
createDeflateWorkerSpy.and.callFake(
() =>
// Creates a worker that does nothing
new Worker(URL.createObjectURL(new Blob([''])))
)
clock = mockClock()
})

Expand All @@ -175,16 +187,18 @@ describe('startDeflateWorker', () => {
})

it('displays an error message when the worker does not respond to the init action', () => {
startDeflateWorker(
configuration,
noop,
() =>
// Creates a worker that does nothing
new Worker(URL.createObjectURL(new Blob([''])))
startDeflateWorkerWithDefaults()
clock.tick(INITIALIZATION_TIME_OUT_DELAY)
expect(displaySpy).toHaveBeenCalledOnceWith(
'Datadog Session Replay failed to start: a timeout occurred while initializing the Worker'
)
})

it('displays a customized error message', () => {
startDeflateWorkerWithDefaults({ source: 'Foo' })
clock.tick(INITIALIZATION_TIME_OUT_DELAY)
expect(displaySpy).toHaveBeenCalledOnceWith(
'Session Replay recording failed to start: a timeout occurred while initializing the Worker'
'Foo failed to start: a timeout occurred while initializing the Worker'
)
})
})
Expand All @@ -193,10 +207,8 @@ describe('startDeflateWorker', () => {
let telemetryEvents: RawTelemetryEvent[]
const UNKNOWN_ERROR = new Error('boom')
let displaySpy: jasmine.Spy
let configuration: RumConfiguration

beforeEach(() => {
configuration = {} as RumConfiguration
displaySpy = spyOn(display, 'error')
telemetryEvents = startFakeTelemetry()
})
Expand All @@ -206,19 +218,26 @@ describe('startDeflateWorker', () => {
})

it('displays an error message when the worker creation throws an unknown error', () => {
startDeflateWorker(configuration, noop, () => {
throw UNKNOWN_ERROR
})
createDeflateWorkerSpy.and.throwError(UNKNOWN_ERROR)
startDeflateWorkerWithDefaults()
expect(displaySpy).toHaveBeenCalledOnceWith(
'Session Replay recording failed to start: an error occurred while creating the Worker:',
'Datadog Session Replay failed to start: an error occurred while creating the Worker:',
UNKNOWN_ERROR
)
})

it('displays a customized error message', () => {
createDeflateWorkerSpy.and.throwError(UNKNOWN_ERROR)
startDeflateWorkerWithDefaults({ source: 'Foo' })
expect(displaySpy).toHaveBeenCalledOnceWith(
'Foo failed to start: an error occurred while creating the Worker:',
UNKNOWN_ERROR
)
})

it('reports unknown errors to telemetry', () => {
startDeflateWorker(configuration, noop, () => {
throw UNKNOWN_ERROR
})
createDeflateWorkerSpy.and.throwError(UNKNOWN_ERROR)
startDeflateWorkerWithDefaults()
expect(telemetryEvents).toEqual([
{
type: 'log',
Expand All @@ -230,13 +249,13 @@ describe('startDeflateWorker', () => {
})

it('does not display error messages as CSP error', () => {
startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
startDeflateWorkerWithDefaults()
mockWorker.dispatchErrorMessage('foo')
expect(displaySpy).not.toHaveBeenCalledWith(jasmine.stringContaining('CSP'))
})

it('reports errors occurring after loading to telemetry', () => {
startDeflateWorker(configuration, noop, createDeflateWorkerSpy)
startDeflateWorkerWithDefaults()
mockWorker.processAllMessages()

mockWorker.dispatchErrorMessage('boom', TEST_STREAM_ID)
Expand Down
25 changes: 15 additions & 10 deletions packages/rum/src/domain/deflate/deflateWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ let state: DeflateWorkerState = { status: DeflateWorkerStatus.Nil }

export function startDeflateWorker(
configuration: RumConfiguration,
source: string,
onInitializationFailure: () => void,
createDeflateWorkerImpl = createDeflateWorker
) {
if (state.status === DeflateWorkerStatus.Nil) {
// doStartDeflateWorker updates the state to "loading" or "error"
doStartDeflateWorker(configuration, createDeflateWorkerImpl)
doStartDeflateWorker(configuration, source, createDeflateWorkerImpl)
}

switch (state.status) {
Expand Down Expand Up @@ -89,40 +90,44 @@ export function getDeflateWorkerStatus() {
*
* more details: https://bugzilla.mozilla.org/show_bug.cgi?id=1736865#c2
*/
export function doStartDeflateWorker(configuration: RumConfiguration, createDeflateWorkerImpl = createDeflateWorker) {
export function doStartDeflateWorker(
configuration: RumConfiguration,
source: string,
createDeflateWorkerImpl = createDeflateWorker
) {
try {
const worker = createDeflateWorkerImpl(configuration)
const { stop: removeErrorListener } = addEventListener(configuration, worker, 'error', (error) => {
onError(configuration, error)
onError(configuration, source, error)
})
const { stop: removeMessageListener } = addEventListener(
configuration,
worker,
'message',
({ data }: MessageEvent<DeflateWorkerResponse>) => {
if (data.type === 'errored') {
onError(configuration, data.error, data.streamId)
onError(configuration, source, data.error, data.streamId)
} else if (data.type === 'initialized') {
onInitialized(data.version)
}
}
)
worker.postMessage({ action: 'init' })
setTimeout(onTimeout, INITIALIZATION_TIME_OUT_DELAY)
setTimeout(() => onTimeout(source), INITIALIZATION_TIME_OUT_DELAY)
const stop = () => {
removeErrorListener()
removeMessageListener()
}

state = { status: DeflateWorkerStatus.Loading, worker, stop, initializationFailureCallbacks: [] }
} catch (error) {
onError(configuration, error)
onError(configuration, source, error)
}
}

function onTimeout() {
function onTimeout(source: string) {
if (state.status === DeflateWorkerStatus.Loading) {
display.error('Session Replay recording failed to start: a timeout occurred while initializing the Worker')
display.error(`${source} failed to start: a timeout occurred while initializing the Worker`)
state.initializationFailureCallbacks.forEach((callback) => callback())
state = { status: DeflateWorkerStatus.Error }
}
Expand All @@ -134,9 +139,9 @@ function onInitialized(version: string) {
}
}

function onError(configuration: RumConfiguration, error: unknown, streamId?: number) {
function onError(configuration: RumConfiguration, source: string, error: unknown, streamId?: number) {
if (state.status === DeflateWorkerStatus.Loading || state.status === DeflateWorkerStatus.Nil) {
display.error('Session Replay recording failed to start: an error occurred while creating the Worker:', error)
display.error(`${source} failed to start: an error occurred while creating the Worker:`, error)
if (error instanceof Event || (error instanceof Error && isMessageCspRelated(error.message))) {
let baseMessage
if (configuration.workerUrl) {
Expand Down

0 comments on commit 4fa98c7

Please sign in to comment.