Skip to content

Commit

Permalink
feat: User-defined logger config (#6409)
Browse files Browse the repository at this point in the history
## Summary

This PR adds a possibility to customize log level via the `level`
property (limit to just see errors or show errors and warnings) and
enable/disable strict mode (via `strict` property), which will be useful
for changes implemented in
[this](#6310)
PR to show invalid `.value` access warnings only in the strict mode.

## Example recording


https://github.com/user-attachments/assets/232d3b69-d99c-4114-989f-967d9d8c9e22


## Test plan

- Open `EmptyExample` (which will be reverted to the original
implementation before merging this PR),
- Change logger settings (e.g. the `level` property) via
`Animated.configureLogger`,

---------

Co-authored-by: Tomasz Żelawski <40713406+tjzel@users.noreply.github.com>
  • Loading branch information
MatiPl01 and tjzel authored Aug 28, 2024
1 parent 6933d14 commit 8a2749e
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 41 deletions.
17 changes: 17 additions & 0 deletions packages/react-native-reanimated/jest-setup.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
delete global.MessageChannel;
require('./src/jestUtils').setUpTests();

global.__reanimatedLoggerConfig = {
logFunction: (data) => {
switch (data.level) {
case 'warn':
console.warn(data.message.content);
break;
case 'error':
case 'fatal':
case 'syntax':
console.error(data.message.content);
break;
}
},
level: 'warn',
strict: false,
};
2 changes: 1 addition & 1 deletion packages/react-native-reanimated/plugin/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/react-native-reanimated/plugin/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const notCapturedIdentifiers = [
// Reanimated
'_WORKLET',
'ReanimatedError',
'__reanimatedLoggerConfig',
];

/**
Expand Down
11 changes: 10 additions & 1 deletion packages/react-native-reanimated/src/ConfigHelper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use strict';
import { PropsAllowlists } from './propsAllowlists';
import { jsiConfigureProps } from './core';
import { executeOnUIRuntimeSync, jsiConfigureProps } from './core';
import { ReanimatedError } from './errors';
import { updateLoggerConfig } from './logger';
import type { LoggerConfig } from './logger';

function assertNoOverlapInLists() {
for (const key in PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST) {
Expand Down Expand Up @@ -52,6 +54,13 @@ export function addWhitelistedUIProps(props: Record<string, boolean>): void {
}
}

export function configureReanimatedLogger(config: LoggerConfig) {
// Update the configuration object in the React runtime
updateLoggerConfig(config);
// Register the updated configuration in the UI runtime
executeOnUIRuntimeSync(updateLoggerConfig)(config);
}

const PROCESSED_VIEW_NAMES = new Set();

export interface ViewConfig {
Expand Down
7 changes: 0 additions & 7 deletions packages/react-native-reanimated/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ export const isReanimated3 = () => true;
*/
export const isConfigured = isReanimated3;

// this is for web implementation
if (SHOULD_BE_USE_WEB) {
global._WORKLET = false;
global._log = console.log;
global._getAnimationTimestamp = () => performance.now();
}

export function getViewProp<T>(
viewTag: number,
propName: string,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-reanimated/src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { UpdatePropsManager } from './UpdateProps';
import type { callGuardDEV } from './initializers';
import type { WorkletRuntime } from './runtimes';
import type { RNScreensTurboModuleType } from './screenTransition/commonTypes';
import type { LoggerConfigInternal } from './logger';

declare global {
var _REANIMATED_IS_REDUCED_MOTION: boolean | undefined;
Expand Down Expand Up @@ -112,4 +113,5 @@ declare global {
shadowNodeWrapper: ShadowNodeWrapper,
propName: string
) => string;
var __reanimatedLoggerConfig: LoggerConfigInternal;
}
2 changes: 2 additions & 0 deletions packages/react-native-reanimated/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import * as Animated from './Animated';

export default Animated;

export { configureReanimatedLogger } from './ConfigHelper';
export { LogLevel as ReanimatedLogLevel } from './logger';
export type { WorkletRuntime } from './core';
export {
runOnJS,
Expand Down
42 changes: 27 additions & 15 deletions packages/react-native-reanimated/src/initializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,46 @@ import {
executeOnUIRuntimeSync,
} from './threads';
import { mockedRequestAnimationFrame } from './mockedRequestAnimationFrame';
import type { LogData } from './logger';
import {
logger,
DEFAULT_LOGGER_CONFIG,
logToLogBoxAndConsole,
registerLoggerConfig,
replaceLoggerImplementation,
} from './logger';
import { makeShareableCloneRecursive } from './shareables';
import { shareableMappingCache } from './shareableMappingCache';

const IS_JEST = isJest();
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
const IS_CHROME_DEBUGGER = isChromeDebugger();

// Register ReanimatedError in the UI global scope.
// (we are using `executeOnUIRuntimeSync` here to make sure that the error is
// registered before any async operations are executed on the UI runtime)
if (!shouldBeUseWeb()) {
executeOnUIRuntimeSync(registerReanimatedError)();
}

// Override the logFunction implementation with the one that adds logs
// with better stack traces to the LogBox (need to override it after `runOnJS`
// is defined).
replaceLoggerImplementation((data: LogData) => {
function overrideLogFunctionImplementation() {
'worklet';
runOnJS(logToLogBoxAndConsole)(data);
});
shareableMappingCache.set(logger, makeShareableCloneRecursive(logger));
replaceLoggerImplementation((data) => {
'worklet';
runOnJS(logToLogBoxAndConsole)(data);
});
}

// Register logger config and replace the log function implementation in
// the React runtime global scope
registerLoggerConfig(DEFAULT_LOGGER_CONFIG);
overrideLogFunctionImplementation();

// this is for web implementation
if (SHOULD_BE_USE_WEB) {
global._WORKLET = false;
global._log = console.log;
global._getAnimationTimestamp = () => performance.now();
} else {
// Register ReanimatedError and logger config in the UI runtime global scope.
// (we are using `executeOnUIRuntimeSync` here to make sure that the changes
// are applied before any async operations are executed on the UI runtime)
executeOnUIRuntimeSync(registerReanimatedError)();
executeOnUIRuntimeSync(registerLoggerConfig)(DEFAULT_LOGGER_CONFIG);
executeOnUIRuntimeSync(overrideLogFunctionImplementation)();
}

// callGuard is only used with debug builds
export function callGuardDEV<Args extends unknown[], ReturnValue>(
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native-reanimated/src/logger/LogBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { LogBoxStatic } from 'react-native';
import { LogBox as RNLogBox } from 'react-native';

export type LogLevel = 'warn' | 'error' | 'fatal' | 'syntax';
export type LogBoxLogLevel = 'warn' | 'error' | 'fatal' | 'syntax';

type Message = {
content: string;
Expand All @@ -34,7 +34,7 @@ type ComponentStack = CodeFrame[];
type ComponentStackType = 'legacy' | 'stack';

export type LogData = {
level: LogLevel;
level: LogBoxLogLevel;
message: Message;
category: Category;
componentStack: ComponentStack;
Expand Down
100 changes: 85 additions & 15 deletions packages/react-native-reanimated/src/logger/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
'use strict';
import { addLogBoxLog } from './LogBox';
import type { LogLevel, LogData } from './LogBox';
import type { LogData, LogBoxLogLevel } from './LogBox';

type LogFunction = (data: LogData) => void;

export enum LogLevel {
warn = 1,
error = 2,
fatal = 3,
}

export type LoggerConfig = {
level?: LogLevel;
strict?: boolean;
};

export type LoggerConfigInternal = {
logFunction: LogFunction;
} & Required<LoggerConfig>;

function logToConsole(data: LogData) {
'worklet';
Expand All @@ -16,12 +33,18 @@ function logToConsole(data: LogData) {
}
}

export const DEFAULT_LOGGER_CONFIG: LoggerConfigInternal = {
logFunction: logToConsole,
level: LogLevel.warn,
strict: false,
};

function formatMessage(message: string) {
'worklet';
return `[Reanimated] ${message}`;
}

function createLog(level: LogLevel, message: string): LogData {
function createLog(level: LogBoxLogLevel, message: string): LogData {
'worklet';
const formattedMessage = formatMessage(message);

Expand All @@ -38,10 +61,6 @@ function createLog(level: LogLevel, message: string): LogData {
};
}

const loggerImpl = {
logFunction: logToConsole,
};

/**
* Function that logs to LogBox and console.
* Used to replace the default console logging with logging to LogBox
Expand All @@ -54,28 +73,79 @@ export function logToLogBoxAndConsole(data: LogData) {
logToConsole(data);
}

/**
* Registers the logger configuration.
* use it only for Worklet runtimes.
*
* @param config - The config to register.
*/
export function registerLoggerConfig(config: LoggerConfigInternal) {
'worklet';
global.__reanimatedLoggerConfig = config;
}

/**
* Replaces the default log function with a custom implementation.
*
* @param logFunction - The custom log function.
*/
export function replaceLoggerImplementation(
logFunction: (data: LogData) => void
export function replaceLoggerImplementation(logFunction: LogFunction) {
'worklet';
registerLoggerConfig({ ...__reanimatedLoggerConfig, logFunction });
}

/**
* Updates logger configuration.
*
* @param options - The new logger configuration to apply.
* - level: The minimum log level to display.
* - strict: Whether to log warnings and errors that are not strict.
* Defaults to false.
*/
export function updateLoggerConfig(options?: Partial<LoggerConfig>) {
'worklet';
registerLoggerConfig({
...__reanimatedLoggerConfig,
// Don't reuse previous level and strict values from the global config
level: options?.level ?? LogLevel.warn,
strict: options?.strict ?? false,
});
}

type LogOptions = {
strict?: boolean;
};

function handleLog(
level: Exclude<LogBoxLogLevel, 'syntax'>,
message: string,
options: LogOptions
) {
loggerImpl.logFunction = logFunction;
'worklet';
const config = __reanimatedLoggerConfig;
if (
// Don't log if the log is marked as strict-only and the config doesn't
// enable strict logging
(options.strict && !config.strict) ||
// Don't log if the log level is below the minimum configured level
LogLevel[level] < config.level
) {
return;
}
config.logFunction(createLog(level, message));
}

export const logger = {
warn(message: string) {
warn(message: string, options: LogOptions = {}) {
'worklet';
loggerImpl.logFunction(createLog('warn', message));
handleLog('warn', message, options);
},
error(message: string) {
error(message: string, options: LogOptions = {}) {
'worklet';
loggerImpl.logFunction(createLog('error', message));
handleLog('error', message, options);
},
fatal(message: string) {
fatal(message: string, options: LogOptions = {}) {
'worklet';
loggerImpl.logFunction(createLog('fatal', message));
handleLog('fatal', message, options);
},
};
5 changes: 5 additions & 0 deletions packages/react-native-reanimated/src/runtimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isWorkletFunction } from './commonTypes';
import type { WorkletFunction } from './commonTypes';
import { ReanimatedError, registerReanimatedError } from './errors';
import { setupCallGuard, setupConsole } from './initializers';
import { registerLoggerConfig } from './logger';
import NativeReanimatedModule from './NativeReanimated';
import { shouldBeUseWeb } from './PlatformChecker';
import {
Expand Down Expand Up @@ -35,11 +36,15 @@ export function createWorkletRuntime(
name: string,
initializer?: WorkletFunction<[], void>
): WorkletRuntime {
// Assign to a different variable as __reanimatedLoggerConfig is not a captured
// identifier in the Worklet runtime.
const config = __reanimatedLoggerConfig;
return NativeReanimatedModule.createWorkletRuntime(
name,
makeShareableCloneRecursive(() => {
'worklet';
registerReanimatedError();
registerLoggerConfig(config);
setupCallGuard();
setupConsole();
initializer?.();
Expand Down

0 comments on commit 8a2749e

Please sign in to comment.