diff --git a/packages/rum/src/boot/recorderApi.spec.ts b/packages/rum/src/boot/recorderApi.spec.ts index 5e8cee779d..eab0deb310 100644 --- a/packages/rum/src/boot/recorderApi.spec.ts +++ b/packages/rum/src/boot/recorderApi.spec.ts @@ -1,11 +1,12 @@ -import { isIE, noop } from '@datadog/browser-core' +import { display, isIE } from '@datadog/browser-core' import type { RecorderApi, ViewContexts, LifeCycle, RumConfiguration } from '@datadog/browser-rum-core' import { LifeCycleEventType } from '@datadog/browser-rum-core' import { deleteEventBridgeStub, initEventBridgeStub, createNewEvent } from '@datadog/browser-core/test' import type { RumSessionManagerMock, TestSetupBuilder } from '../../../rum-core/test' import { createRumSessionManagerMock, setup } from '../../../rum-core/test' -import type { DeflateWorker, startDeflateWorker } from '../domain/deflate' +import type { CreateDeflateWorker } from '../domain/deflate' import { MockWorker } from '../../test' +import { resetDeflateWorkerState } from '../domain/deflate' import type { StartRecording } from './recorderApi' import { makeRecorderApi } from './recorderApi' @@ -14,23 +15,8 @@ describe('makeRecorderApi', () => { let recorderApi: RecorderApi let startRecordingSpy: jasmine.Spy let stopRecordingSpy: jasmine.Spy<() => void> - let startDeflateWorkerSpy: jasmine.Spy - const FAKE_WORKER = new MockWorker() - - function startDeflateWorkerWith(worker?: DeflateWorker) { - if (!startDeflateWorkerSpy) { - startDeflateWorkerSpy = jasmine.createSpy('startDeflateWorker') - } - startDeflateWorkerSpy.and.callFake((_, callback) => callback(worker)) - } - - function callLastRegisteredInitialisationCallback() { - startDeflateWorkerSpy.calls.mostRecent().args[1](FAKE_WORKER) - } - - function stopDeflateWorker() { - startDeflateWorkerSpy.and.callFake(noop) - } + let mockWorker: MockWorker + let createDeflateWorkerSpy: jasmine.Spy let rumInit: () => void @@ -38,14 +24,17 @@ describe('makeRecorderApi', () => { if (isIE()) { pending('IE not supported') } + mockWorker = new MockWorker() + createDeflateWorkerSpy = jasmine.createSpy('createDeflateWorkerSpy').and.callFake(() => mockWorker) + spyOn(display, 'error') + setupBuilder = setup().beforeBuild(({ lifeCycle, sessionManager }) => { stopRecordingSpy = jasmine.createSpy('stopRecording') startRecordingSpy = jasmine.createSpy('startRecording').and.callFake(() => ({ stop: stopRecordingSpy, })) - startDeflateWorkerWith(FAKE_WORKER) - recorderApi = makeRecorderApi(startRecordingSpy, startDeflateWorkerSpy) + recorderApi = makeRecorderApi(startRecordingSpy, createDeflateWorkerSpy) rumInit = () => { recorderApi.onRumStart(lifeCycle, {} as RumConfiguration, sessionManager, {} as ViewContexts) } @@ -54,6 +43,7 @@ describe('makeRecorderApi', () => { afterEach(() => { setupBuilder.cleanup() + resetDeflateWorkerState() }) describe('boot', () => { @@ -116,25 +106,22 @@ describe('makeRecorderApi', () => { expect(startRecordingSpy).not.toHaveBeenCalled() }) - it('do not start recording if worker fails to be instantiated', () => { + it('do not start recording if worker creation fails', () => { setupBuilder.build() rumInit() - startDeflateWorkerWith(undefined) + createDeflateWorkerSpy.and.throwError('Crash') recorderApi.start() expect(startRecordingSpy).not.toHaveBeenCalled() }) - it('does not start recording multiple times if restarted before worker is initialized', () => { + it('stops recording if worker initialization fails', () => { setupBuilder.build() rumInit() - stopDeflateWorker() recorderApi.start() - recorderApi.stop() - callLastRegisteredInitialisationCallback() - recorderApi.start() - callLastRegisteredInitialisationCallback() - expect(startRecordingSpy).toHaveBeenCalledTimes(1) + mockWorker.dispatchErrorEvent() + + expect(stopRecordingSpy).toHaveBeenCalled() }) describe('if event bridge present', () => { @@ -395,24 +382,54 @@ describe('makeRecorderApi', () => { }) describe('isRecording', () => { - it('is true only if recording', () => { + it('is false when recording has not been started', () => { setupBuilder.build() rumInit() + expect(recorderApi.isRecording()).toBeFalse() + }) + + it('is false when the worker is not yet initialized', () => { + setupBuilder.build() + rumInit() + recorderApi.start() - expect(recorderApi.isRecording()).toBeTrue() - recorderApi.stop() expect(recorderApi.isRecording()).toBeFalse() }) + it('is false when the worker failed to initialize', () => { + setupBuilder.build() + rumInit() + + recorderApi.start() + mockWorker.dispatchErrorEvent() + + expect(recorderApi.isRecording()).toBeFalse() + }) + + it('is true when recording is started and the worker is initialized', () => { + setupBuilder.build() + rumInit() + + recorderApi.start() + mockWorker.processAllMessages() + + expect(recorderApi.isRecording()).toBeTrue() + }) + it('is false before the DOM is loaded', () => { setupBuilder.build() const { triggerOnDomLoaded } = mockDocumentReadyState() rumInit() - expect(recorderApi.isRecording()).toBeFalse() + recorderApi.start() + mockWorker.processAllMessages() + expect(recorderApi.isRecording()).toBeFalse() + triggerOnDomLoaded() + mockWorker.processAllMessages() + expect(recorderApi.isRecording()).toBeTrue() }) }) diff --git a/packages/rum/src/boot/recorderApi.ts b/packages/rum/src/boot/recorderApi.ts index 5d8e6e7bb8..98ab7206da 100644 --- a/packages/rum/src/boot/recorderApi.ts +++ b/packages/rum/src/boot/recorderApi.ts @@ -10,7 +10,14 @@ import type { import { LifeCycleEventType } from '@datadog/browser-rum-core' import { getReplayStats } from '../domain/replayStats' import { getSessionReplayLink } from '../domain/getSessionReplayLink' -import { DeflateEncoderStreamId, createDeflateEncoder, startDeflateWorker } from '../domain/deflate' +import type { CreateDeflateWorker } from '../domain/deflate' +import { + DeflateEncoderStreamId, + createDeflateEncoder, + startDeflateWorker, + DeflateWorkerStatus, + getDeflateWorkerStatus, +} from '../domain/deflate' import { getSerializedNodeId } from '../domain/record' import type { startRecording } from './startRecording' @@ -46,7 +53,7 @@ type RecorderState = export function makeRecorderApi( startRecordingImpl: StartRecording, - startDeflateWorkerImpl = startDeflateWorker + createDeflateWorkerImpl?: CreateDeflateWorker ): RecorderApi { const recorderStartObservable = new Observable() @@ -76,7 +83,6 @@ export function makeRecorderApi( return { start: () => startStrategy(), stop: () => stopStrategy(), - getReplayStats, getSessionReplayLink: (configuration, sessionManager, viewContexts) => getSessionReplayLink(configuration, sessionManager, viewContexts, state.status !== RecorderStatus.Stopped), recorderStartObservable, @@ -118,31 +124,33 @@ export function makeRecorderApi( return } - startDeflateWorkerImpl(configuration, (worker) => { - if (state.status !== RecorderStatus.Starting) { - return - } - - if (!worker) { - state = { - status: RecorderStatus.Stopped, - } - return - } + const worker = startDeflateWorker( + configuration, + () => { + stopStrategy() + }, + createDeflateWorkerImpl + ) - const { stop: stopRecording } = startRecordingImpl( - lifeCycle, - configuration, - sessionManager, - viewContexts, - createDeflateEncoder(configuration, worker, DeflateEncoderStreamId.REPLAY) - ) - recorderStartObservable.notify(relativeNow()) + if (!worker) { state = { - status: RecorderStatus.Started, - stopRecording, + status: RecorderStatus.Stopped, } - }) + return + } + + const { stop: stopRecording } = startRecordingImpl( + lifeCycle, + configuration, + sessionManager, + viewContexts, + createDeflateEncoder(configuration, worker, DeflateEncoderStreamId.REPLAY) + ) + recorderStartObservable.notify(relativeNow()) + state = { + status: RecorderStatus.Started, + stopRecording, + } }) } @@ -165,6 +173,32 @@ export function makeRecorderApi( } }, - isRecording: () => state.status === RecorderStatus.Started, + isRecording: () => + // The worker is started optimistically, meaning we could have started to record but its + // initialization fails a bit later. This could happen when: + // * the worker URL (blob or plain URL) is blocked by CSP in Firefox only (Chromium and Safari + // throw an exception when instantiating the worker, and IE doesn't care about CSP) + // * the browser fails to load the worker in case the workerUrl is used + // * an unexpected error occurs in the Worker before initialization, ex: + // * a runtime exception collected by monitor() + // * a syntax error notified by the browser via an error event + // * the worker is unresponsive for some reason and timeouts + // + // It is not expected to happen often. Nonetheless, the "replayable" status on RUM events is + // an important part of the Datadog App: + // * If we have a false positive (we set has_replay: true even if no replay data is present), + // we might display broken links to the Session Replay player. + // * If we have a false negative (we don't set has_replay: true even if replay data is + // available), it is less noticeable because no link will be displayed. + // + // Thus, it is better to have false negative, so let's make sure the worker is correctly + // initialized before advertizing that we are recording. + // + // In the future, when the compression worker will also be used for RUM data, this will be + // less important since no RUM event will be sent when the worker fails to initialize. + getDeflateWorkerStatus() === DeflateWorkerStatus.Initialized && state.status === RecorderStatus.Started, + + getReplayStats: (viewId) => + getDeflateWorkerStatus() === DeflateWorkerStatus.Initialized ? getReplayStats(viewId) : undefined, } } diff --git a/packages/rum/src/boot/startRecording.spec.ts b/packages/rum/src/boot/startRecording.spec.ts index 01b96d4641..1a2d9149db 100644 --- a/packages/rum/src/boot/startRecording.spec.ts +++ b/packages/rum/src/boot/startRecording.spec.ts @@ -28,7 +28,7 @@ describe('startRecording', () => { let clock: Clock | undefined let configuration: RumConfiguration - beforeEach((done) => { + beforeEach(() => { if (isIE()) { pending('IE not supported') } @@ -42,37 +42,36 @@ describe('startRecording', () => { textField = document.createElement('input') sandbox.appendChild(textField) - startDeflateWorker(configuration, (worker) => { - setupBuilder = setup() - .withViewContexts({ - findView() { - return { id: viewId, startClocks: {} as ClocksState } - }, - }) - .withSessionManager(sessionManager) - .withConfiguration({ - defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, - }) - .beforeBuild(({ lifeCycle, configuration, viewContexts, sessionManager }) => { - requestSendSpy = jasmine.createSpy() - const httpRequest = { - send: requestSendSpy, - sendOnExit: requestSendSpy, - } - - const recording = startRecording( - lifeCycle, - configuration, - sessionManager, - viewContexts, - createDeflateEncoder(configuration, worker!, DeflateEncoderStreamId.REPLAY), - httpRequest - ) - stopRecording = recording ? recording.stop : noop - return { stop: stopRecording } - }) - done() - }) + const worker = startDeflateWorker(configuration, noop)! + + setupBuilder = setup() + .withViewContexts({ + findView() { + return { id: viewId, startClocks: {} as ClocksState } + }, + }) + .withSessionManager(sessionManager) + .withConfiguration({ + defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, + }) + .beforeBuild(({ lifeCycle, configuration, viewContexts, sessionManager }) => { + requestSendSpy = jasmine.createSpy() + const httpRequest = { + send: requestSendSpy, + sendOnExit: requestSendSpy, + } + + const recording = startRecording( + lifeCycle, + configuration, + sessionManager, + viewContexts, + createDeflateEncoder(configuration, worker, DeflateEncoderStreamId.REPLAY), + httpRequest + ) + stopRecording = recording ? recording.stop : noop + return { stop: stopRecording } + }) }) afterEach(() => { diff --git a/packages/rum/src/domain/deflate/deflateWorker.spec.ts b/packages/rum/src/domain/deflate/deflateWorker.spec.ts index d67b4b12c2..5a27737a3d 100644 --- a/packages/rum/src/domain/deflate/deflateWorker.spec.ts +++ b/packages/rum/src/domain/deflate/deflateWorker.spec.ts @@ -4,62 +4,55 @@ import type { RumConfiguration } from '@datadog/browser-rum-core' import type { Clock } from '@datadog/browser-core/test' import { mockClock } from '@datadog/browser-core/test' import { MockWorker } from '../../../test' -import type { createDeflateWorker } from './deflateWorker' +import type { CreateDeflateWorker } from './deflateWorker' import { startDeflateWorker, resetDeflateWorkerState, INITIALIZATION_TIME_OUT_DELAY } from './deflateWorker' // Arbitrary stream ids used for tests const TEST_STREAM_ID = 5 describe('startDeflateWorker', () => { - let deflateWorker: MockWorker - let createDeflateWorkerSpy: jasmine.Spy - let callbackSpy: jasmine.Spy + let mockWorker: MockWorker + let createDeflateWorkerSpy: jasmine.Spy + let onInitializationFailureSpy: jasmine.Spy<() => void> let configuration: RumConfiguration beforeEach(() => { configuration = {} as RumConfiguration - deflateWorker = new MockWorker() - callbackSpy = jasmine.createSpy('callbackSpy') - createDeflateWorkerSpy = jasmine.createSpy('createDeflateWorkerSpy').and.callFake(() => deflateWorker) + mockWorker = new MockWorker() + onInitializationFailureSpy = jasmine.createSpy('onInitializationFailureSpy') + createDeflateWorkerSpy = jasmine.createSpy('createDeflateWorkerSpy').and.callFake(() => mockWorker) }) afterEach(() => { resetDeflateWorkerState() }) - it('creates a deflate worker and call callback when initialized', () => { - startDeflateWorker(configuration, callbackSpy, createDeflateWorkerSpy) + it('creates a deflate worker', () => { + const worker = startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy) expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1) - deflateWorker.processAllMessages() - expect(callbackSpy).toHaveBeenCalledOnceWith(deflateWorker) - }) + expect(worker).toBe(mockWorker) - it('uses the previously created worker', () => { - startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - deflateWorker.processAllMessages() + mockWorker.processAllMessages() + expect(onInitializationFailureSpy).not.toHaveBeenCalled() + }) - startDeflateWorker(configuration, callbackSpy, createDeflateWorkerSpy) + it('uses the previously created worker during loading', () => { + const worker1 = startDeflateWorker(configuration, noop, createDeflateWorkerSpy) + const worker2 = startDeflateWorker(configuration, noop, createDeflateWorkerSpy) expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1) - deflateWorker.processAllMessages() - expect(callbackSpy).toHaveBeenCalledOnceWith(deflateWorker) + expect(worker1).toBe(worker2) }) - describe('loading state', () => { - it('does not create multiple workers when called multiple times while the worker is loading', () => { - startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1) - }) + it('uses the previously created worker once initialized', () => { + const worker1 = startDeflateWorker(configuration, noop, createDeflateWorkerSpy) + mockWorker.processAllMessages() - it('calls all registered callbacks when the worker is initialized', () => { - const callbackSpy1 = jasmine.createSpy() - const callbackSpy2 = jasmine.createSpy() - startDeflateWorker(configuration, callbackSpy1, createDeflateWorkerSpy) - startDeflateWorker(configuration, callbackSpy2, createDeflateWorkerSpy) - deflateWorker.processAllMessages() - expect(callbackSpy1).toHaveBeenCalledOnceWith(deflateWorker) - expect(callbackSpy2).toHaveBeenCalledOnceWith(deflateWorker) - }) + const worker2 = startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy) + expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1) + expect(worker1).toBe(worker2) + + mockWorker.processAllMessages() + expect(onInitializationFailureSpy).not.toHaveBeenCalled() }) describe('worker CSP error', () => { @@ -85,49 +78,84 @@ describe('startDeflateWorker', () => { resetTelemetry() }) - it('displays CSP instructions when the worker creation throws a CSP error', () => { - startDeflateWorker(configuration, noop, () => { - throw CSP_ERROR + 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 + }) + expect(worker).toBeUndefined() }) - 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 + it('displays CSP instructions when the worker creation throws a CSP error', () => { + startDeflateWorker(configuration, noop, () => { + throw CSP_ERROR + }) + expect(displaySpy).toHaveBeenCalledWith( + jasmine.stringContaining('Please make sure CSP is correctly configured') + ) }) - expect(telemetryEvents).toEqual([]) - }) - it('displays ErrorEvent as CSP error', () => { - startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - deflateWorker.dispatchErrorEvent() - 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 + }) + expect(telemetryEvents).toEqual([]) + }) - it('calls the callback without argument in case of an error occurs during loading', () => { - startDeflateWorker(configuration, callbackSpy, createDeflateWorkerSpy) - deflateWorker.dispatchErrorEvent() - expect(callbackSpy).toHaveBeenCalledOnceWith() + it('does not try to create a worker again after the creation failed', () => { + startDeflateWorker(configuration, noop, () => { + throw CSP_ERROR + }) + startDeflateWorker(configuration, noop, createDeflateWorkerSpy) + expect(createDeflateWorkerSpy).not.toHaveBeenCalled() + }) }) - it('calls the callback without argument in case of an error occurred in a previous loading', () => { - startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - deflateWorker.dispatchErrorEvent() + describe('Firefox behavior: error during worker loading', () => { + it('displays ErrorEvent as CSP error', () => { + startDeflateWorker(configuration, noop, createDeflateWorkerSpy) + mockWorker.dispatchErrorEvent() + expect(displaySpy).toHaveBeenCalledWith( + jasmine.stringContaining('Please make sure CSP is correctly configured') + ) + }) - startDeflateWorker(configuration, callbackSpy, createDeflateWorkerSpy) - expect(callbackSpy).toHaveBeenCalledOnceWith() - }) + it('calls the initialization failure callback in case of an error occurs during loading', () => { + startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy) + mockWorker.dispatchErrorEvent() + expect(onInitializationFailureSpy).toHaveBeenCalledTimes(1) + }) - it('adjusts the error message when a workerUrl is set', () => { - configuration.workerUrl = '/worker.js' - startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - deflateWorker.dispatchErrorEvent() - expect(displaySpy).toHaveBeenCalledWith( - jasmine.stringContaining( - 'Please make sure the Worker URL /worker.js is correct and CSP is correctly configured.' + it('returns undefined if an error occurred in a previous loading', () => { + startDeflateWorker(configuration, noop, createDeflateWorkerSpy) + mockWorker.dispatchErrorEvent() + + const worker = startDeflateWorker(configuration, onInitializationFailureSpy, createDeflateWorkerSpy) + + 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) + mockWorker.dispatchErrorEvent() + expect(displaySpy).toHaveBeenCalledWith( + jasmine.stringContaining( + 'Please make sure the Worker URL /worker.js is correct and CSP is correctly configured.' + ) ) - ) + }) + + 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) + mockWorker.dispatchErrorEvent() + expect(onInitializationFailureSpy1).toHaveBeenCalledTimes(1) + expect(onInitializationFailureSpy2).toHaveBeenCalledTimes(1) + }) }) }) @@ -203,15 +231,15 @@ describe('startDeflateWorker', () => { it('does not display error messages as CSP error', () => { startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - deflateWorker.dispatchErrorMessage('foo') + mockWorker.dispatchErrorMessage('foo') expect(displaySpy).not.toHaveBeenCalledWith(jasmine.stringContaining('CSP')) }) it('reports errors occurring after loading to telemetry', () => { startDeflateWorker(configuration, noop, createDeflateWorkerSpy) - deflateWorker.processAllMessages() + mockWorker.processAllMessages() - deflateWorker.dispatchErrorMessage('boom', TEST_STREAM_ID) + mockWorker.dispatchErrorMessage('boom', TEST_STREAM_ID) expect(telemetryEvents).toEqual([ { type: 'log', diff --git a/packages/rum/src/domain/deflate/deflateWorker.ts b/packages/rum/src/domain/deflate/deflateWorker.ts index 82fe465665..f09ec2cc90 100644 --- a/packages/rum/src/domain/deflate/deflateWorker.ts +++ b/packages/rum/src/domain/deflate/deflateWorker.ts @@ -10,7 +10,7 @@ export const INITIALIZATION_TIME_OUT_DELAY = 10 * ONE_SECOND * initialization messages, making the creation asynchronous. * These worker lifecycle states handle this case. */ -const enum DeflateWorkerStatus { +export const enum DeflateWorkerStatus { Nil, Loading, Error, @@ -23,7 +23,8 @@ type DeflateWorkerState = } | { status: DeflateWorkerStatus.Loading - callbacks: Array<(worker?: DeflateWorker) => void> + worker: DeflateWorker + initializationFailureCallbacks: Array<() => void> } | { status: DeflateWorkerStatus.Error @@ -38,41 +39,29 @@ export interface DeflateWorker extends Worker { postMessage(message: DeflateWorkerAction): void } -let workerBlobUrl: string | undefined +export type CreateDeflateWorker = typeof createDeflateWorker -function createWorkerBlobUrl() { - // Lazily compute the worker URL to allow importing the SDK in NodeJS - if (!workerBlobUrl) { - workerBlobUrl = URL.createObjectURL(new Blob([workerString])) - } - return workerBlobUrl -} - -export function createDeflateWorker(configuration: RumConfiguration): DeflateWorker { - return new Worker(configuration.workerUrl || createWorkerBlobUrl()) +function createDeflateWorker(configuration: RumConfiguration): DeflateWorker { + return new Worker(configuration.workerUrl || URL.createObjectURL(new Blob([workerString]))) } let state: DeflateWorkerState = { status: DeflateWorkerStatus.Nil } export function startDeflateWorker( configuration: RumConfiguration, - callback: (worker?: DeflateWorker) => void, + onInitializationFailure: () => void, createDeflateWorkerImpl = createDeflateWorker ) { + if (state.status === DeflateWorkerStatus.Nil) { + doStartDeflateWorker(configuration, createDeflateWorkerImpl) + } + switch (state.status) { - case DeflateWorkerStatus.Nil: - state = { status: DeflateWorkerStatus.Loading, callbacks: [callback] } - doStartDeflateWorker(configuration, createDeflateWorkerImpl) - break case DeflateWorkerStatus.Loading: - state.callbacks.push(callback) - break - case DeflateWorkerStatus.Error: - callback() - break + state.initializationFailureCallbacks.push(onInitializationFailure) + return state.worker case DeflateWorkerStatus.Initialized: - callback(state.worker) - break + return state.worker } } @@ -80,6 +69,10 @@ export function resetDeflateWorkerState() { state = { status: DeflateWorkerStatus.Nil } } +export function getDeflateWorkerStatus() { + return state.status +} + /** * Starts the deflate worker and handle messages and errors * @@ -99,12 +92,12 @@ export function doStartDeflateWorker(configuration: RumConfiguration, createDefl if (data.type === 'errored') { onError(configuration, data.error, data.streamId) } else if (data.type === 'initialized') { - onInitialized(worker, data.version) + onInitialized(data.version) } }) worker.postMessage({ action: 'init' }) setTimeout(onTimeout, INITIALIZATION_TIME_OUT_DELAY) - return worker + state = { status: DeflateWorkerStatus.Loading, worker, initializationFailureCallbacks: [] } } catch (error) { onError(configuration, error) } @@ -113,20 +106,19 @@ export function doStartDeflateWorker(configuration: RumConfiguration, createDefl function onTimeout() { if (state.status === DeflateWorkerStatus.Loading) { display.error('Session Replay recording failed to start: a timeout occurred while initializing the Worker') - state.callbacks.forEach((callback) => callback()) + state.initializationFailureCallbacks.forEach((callback) => callback()) state = { status: DeflateWorkerStatus.Error } } } -function onInitialized(worker: DeflateWorker, version: string) { +function onInitialized(version: string) { if (state.status === DeflateWorkerStatus.Loading) { - state.callbacks.forEach((callback) => callback(worker)) - state = { status: DeflateWorkerStatus.Initialized, worker, version } + state = { status: DeflateWorkerStatus.Initialized, worker: state.worker, version } } } function onError(configuration: RumConfiguration, error: unknown, streamId?: number) { - if (state.status === DeflateWorkerStatus.Loading) { + 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) if (error instanceof Event || (error instanceof Error && isMessageCspRelated(error.message))) { let baseMessage @@ -141,7 +133,9 @@ function onError(configuration: RumConfiguration, error: unknown, streamId?: num } else { addTelemetryError(error) } - state.callbacks.forEach((callback) => callback()) + if (state.status === DeflateWorkerStatus.Loading) { + state.initializationFailureCallbacks.forEach((callback) => callback()) + } state = { status: DeflateWorkerStatus.Error } } else { addTelemetryError(error, { diff --git a/packages/rum/src/domain/deflate/index.ts b/packages/rum/src/domain/deflate/index.ts index 2cb7917bce..fb2d904535 100644 --- a/packages/rum/src/domain/deflate/index.ts +++ b/packages/rum/src/domain/deflate/index.ts @@ -1,2 +1,9 @@ export { DeflateEncoderStreamId, DeflateEncoder, createDeflateEncoder } from './deflateEncoder' -export { DeflateWorker, startDeflateWorker } from './deflateWorker' +export { + DeflateWorker, + startDeflateWorker, + DeflateWorkerStatus, + getDeflateWorkerStatus, + resetDeflateWorkerState, + CreateDeflateWorker, +} from './deflateWorker'