Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions packages/core/src/DdSdkReactNative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,26 @@ export class DdSdkReactNative {
});
}
if (configuration.initializationMode === InitializationMode.ASYNC) {
return InteractionManager.runAfterInteractions(() => {
return DdSdkReactNative.initializeNativeSDK(configuration, {
const initNative = () =>
DdSdkReactNative.initializeNativeSDK(configuration, {
initializationModeForTelemetry: 'ASYNC'
});

// We rely on requestIdleCallback for RN >= 0.76 to make sure that the SDK initialization
// happens after rendering has finished
if ((globalThis as Record<string, unknown>).requestIdleCallback) {
return new Promise<void>((resolve, reject) => {
((globalThis as unknown) as {
requestIdleCallback: (cb: () => void) => void;
}).requestIdleCallback(() => {
Comment thread
sbarrio marked this conversation as resolved.
initNative().then(resolve, reject);
});
});
}

// On RN below 0.76 we use runAfterInteractions, which initializes the SDK after animations are done
return InteractionManager.runAfterInteractions(() => {
return initNative();
});
}
// TODO: Remove when DdSdkReactNativeConfiguration is deprecated
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/config/DatadogProviderConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { InitializationMode } from './types';
*/
export class DatadogProviderConfiguration extends CoreConfiguration {
/**
* If set to ASYNC, the initialization will be delayed until all animations are completed.
* If set to ASYNC, the native initialization will run when the
* main thread is idle (via `requestIdleCallback` on RN >= 0.76, or
* `InteractionManager.runAfterInteractions` on earlier versions)
*/
public initializationMode: InitializationMode = InitializationMode.SYNC;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { render } from '@testing-library/react-native';
import { Animated, Button, InteractionManager, Text, View } from 'react-native';
import { Animated, Button, Text, View } from 'react-native';
import React, { useState } from 'react';

import { DatadogProviderConfiguration } from '../../../../config/DatadogProviderConfiguration';
Expand Down Expand Up @@ -109,14 +109,31 @@ export const renderWithProvider = (params?: {
};

/**
* Mocks an animation for InteractionManager.runAfterInteractions. Returns
* a function to be called to finish the animation
* Mocks requestIdleCallback so that scheduled callbacks only run when
* {@link flushIdleCallbacks} is called. Restores the original value on
* {@link restore}.
*/
export const mockAnimation = () => {
const fakeAnimationHandle = InteractionManager.createInteractionHandle();
export const mockIdleCallback = () => {
const callbacks: Array<() => void> = [];
const original = (globalThis as Record<string, unknown>)
.requestIdleCallback;

(globalThis as Record<string, unknown>).requestIdleCallback = (
cb: () => void
) => {
callbacks.push(cb);
return callbacks.length;
};

return {
finishAnimation: () =>
InteractionManager.clearInteractionHandle(fakeAnimationHandle)
flushIdleCallbacks: () => {
callbacks.splice(0).forEach(cb => cb());
},
restore: () => {
(globalThis as Record<
string,
unknown
>).requestIdleCallback = original;
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {

import {
getDefaultConfiguration,
mockAnimation,
mockIdleCallback,
renderWithProvider
} from './__utils__/renderWithProvider';

Expand Down Expand Up @@ -190,20 +190,24 @@ describe('DatadogProvider', () => {

it('runs after initialization when ASYNC initialization', async () => {
const onInitialization = jest.fn();
const { finishAnimation } = mockAnimation();
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.ASYNC;
const { getByText } = renderWithProvider({
onInitialization,
configuration
});
getByText('I am a test application');
await flushPromises();
expect(onInitialization).not.toHaveBeenCalled();

finishAnimation();
await flushPromises();
expect(onInitialization).toHaveBeenCalledTimes(1);
const idle = mockIdleCallback();
try {
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.ASYNC;
const { getByText } = renderWithProvider({
onInitialization,
configuration
});
getByText('I am a test application');
await flushPromises();
expect(onInitialization).not.toHaveBeenCalled();

idle.flushIdleCallbacks();
await flushPromises();
expect(onInitialization).toHaveBeenCalledTimes(1);
} finally {
idle.restore();
}
});
it('runs after initialization when partial initialization', async () => {
const onInitialization = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { fireEvent } from '@testing-library/react-native';
import { NativeModules } from 'react-native';
import { InteractionManager, NativeModules } from 'react-native';

import { DdSdkReactNative } from '../../../DdSdkReactNative';
import { InitializationMode } from '../../../config/types';
Expand All @@ -22,7 +22,7 @@ import {

import {
getDefaultConfiguration,
mockAnimation,
mockIdleCallback,
renderWithProvider,
renderWithProviderAndAnimation
} from './__utils__/renderWithProvider';
Expand Down Expand Up @@ -70,70 +70,101 @@ describe('DatadogProvider', () => {

expect(NativeModules.DdRum.addAction).toHaveBeenCalledTimes(1);
});
it('initializes the SDK before animations are done', async () => {
const { finishAnimation } = mockAnimation();
renderWithProvider();
it('initializes the SDK without waiting for idle callback', async () => {
const idle = mockIdleCallback();
try {
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.SYNC;
renderWithProvider({ configuration });

await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);
finishAnimation();
await flushPromises();

expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);

idle.flushIdleCallbacks();
await flushPromises();

expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);
} finally {
idle.restore();
}
});
});

describe('initializationMode ASYNC', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('starts auto-instrumentation after animations are done (with real Animation)', async () => {
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.ASYNC;

const { getByText } = renderWithProviderAndAnimation({
configuration
});
await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(700);
await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);
const button = getByText('test button');
fireEvent(button, 'press', {
_targetInst: {
props: {
'dd-action-name': 'press button'
it('initializes the SDK when the idle callback fires', async () => {
const idle = mockIdleCallback();
try {
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.ASYNC;

const { getByText } = renderWithProvider({ configuration });
await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(0);

idle.flushIdleCallbacks();
await flushPromises();

expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);
const button = getByText('test button');
fireEvent(button, 'press', {
_targetInst: {
props: {
'dd-action-name': 'press button'
}
}
}
});
expect(NativeModules.DdRum.addAction).toHaveBeenCalledTimes(1);
});
expect(NativeModules.DdRum.addAction).toHaveBeenCalledTimes(1);
} finally {
idle.restore();
}
});

it('starts auto-instrumentation after animations are done (with InteractionManager)', async () => {
jest.useRealTimers();
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.ASYNC;
it('defers initialization while animations are running', async () => {
const idle = mockIdleCallback();
try {
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.ASYNC;

const { finishAnimation } = mockAnimation();
const { getByText } = renderWithProvider({ configuration });
renderWithProviderAndAnimation({ configuration });
await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(0);

await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(0);
idle.flushIdleCallbacks();
await flushPromises();

finishAnimation();
await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);
} finally {
idle.restore();
}
});

expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);
const button = getByText('test button');
fireEvent(button, 'press', {
_targetInst: {
props: {
'dd-action-name': 'press button'
}
}
});
expect(NativeModules.DdRum.addAction).toHaveBeenCalledTimes(1);
it('falls back to InteractionManager when requestIdleCallback is unavailable', async () => {
const original = (globalThis as Record<string, unknown>)
.requestIdleCallback;
(globalThis as Record<
string,
unknown
>).requestIdleCallback = undefined;
try {
const configuration = getDefaultConfiguration();
configuration.initializationMode = InitializationMode.ASYNC;

const handle = InteractionManager.createInteractionHandle();
renderWithProvider({ configuration });
await flushPromises();
expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(0);

InteractionManager.clearInteractionHandle(handle);
await flushPromises();

expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1);
} finally {
(globalThis as Record<
string,
unknown
>).requestIdleCallback = original;
}
});
});

Expand Down
Loading