UNPKG

react-native-redux-file-logger

Version:

This tool allows you to log Redux actions + state to files. It also provides a convenient API for file logging, so that you can add your own loggers (e.g. navigation state).

396 lines (305 loc) 12 kB
# File Logger for Redux This tool allows you to log Redux actions + state to files. It also provides a convenient API for file logging, so that you can add your own loggers (e.g. navigation state). **Use case:** QA team can easily create independent logs for each issue, which makes it much easier to understand the root-cause. <img src='docs/img/logs-example.PNG' width='800'> ## Motivation 3rd party libraries like [react-native-fs](https://github.com/itinance/react-native-fs) allows you to write data to files, but each write operation opens & closes a new output stream. The idea of this library is to use the standard output functions and redirect the output stream to a file, so that the stream remains open. In this case the logging process doesn't affect the app performance - [Installation](#Installation) - [Usage](#Usage) - [Creating Redux file logger middleware](#creating-redux-file-logger-middleware) - [Creating file logger](#creating-file-logger) - [Creating archive](#creating-archive) - [API](#API) - [Types](#Types) - [LoggerOptions](#LoggerOptions) - [InclusionPredicate](#InclusionPredicate) - [FileConfig](#FileConfig) - [Constants](#Constants) - [SupportedIosRootDirsEnum](#SupportedIosRootDirsEnum) - [SupportedAndroidRootDirsEnum](#SupportedAndroidRootDirsEnum) - [Functions](#Functions) - [createReduxFileLoggerMiddleware](#createReduxFileLoggerMiddleware) - [createLoggerMiddleware](#createLoggerMiddleware) - [addFileLogger](#addFileLogger) - [getFileLogger](#getFileLogger) - [archive](#archive) - [Utils](#Utils) - [createMiddlewareInjector](#createMiddlewareInjector) - [Recipes](#Recipes) - [Pulling files from Android emulators](#pulling-files-from-android-emulators) - [Browsing files on iOS emulators](#browsing-files-on-ios-emulators) - [Thanks](#Thanks) - [License](#License) ## Installation ```sh npm install react-native-redux-file-logger npx pod-install ``` ## Usage ### Creating Redux file logger middleware 1. Create a configurator for `createReduxFileLoggerMiddleware()` that returns middleware, so that later it can be injected to the `store` ```ts import type { Action, AnyAction } from 'redux'; import type { ThunkMiddleware } from 'redux-thunk'; import type { LoggerOptions } from 'react-native-redux-file-logger'; import { Platform } from 'react-native'; export async function configureReduxFileLoggerMiddleware< State = any, BasicAction extends Action = AnyAction, >(): Promise<ThunkMiddleware<State, BasicAction, LoggerOptions<State>> | null> { if (process.env.NODE_ENV === `development`) { const { createReduxFileLoggerMiddleware, SupportedIosRootDirsEnum, SupportedAndroidRootDirsEnum, } = require('react-native-redux-file-logger'); try { const rootDir = Platform.OS === 'android' ? SupportedAndroidRootDirsEnum.Files : SupportedIosRootDirsEnum.Cache; return await createReduxFileLoggerMiddleware( 'redux-action', { rootDir, nestedDir: 'logs', fileName: 'time-travel.json', }, { showDiff: true, shouldLogPrevState: false, shouldLogNextState: true, }, ); } catch (e) { console.error(e); } } return null; } ``` 2. Create the store and a middleware injector. We can't just pass the middleware to `configureStore()`, because it's created asynchronously. ```ts import { configureStore } from '@reduxjs/toolkit'; import { createMiddlewareInjector } from 'react-native-redux-file-logger'; import counterReducer from './features/counter/slice'; export const store = configureStore({ reducer: { counter: counterReducer }, middleware: (getDefaultMiddleware) => getDefaultMiddleware(), }); export const middlewareInjector = createMiddlewareInjector<RootState, AppDispatch>(store); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; ``` 3. Create Redux file logger middleware and inject it to the `store`. ```tsx import * as React from 'react'; import { Provider } from 'react-redux'; import { store, middlewareInjector } from './store'; import { configureReduxFileLoggerMiddleware } from 'react-native-redux-file-logger'; export default function App() { useEffect(() => { (async () => { const rflMiddleware = await configureReduxFileLoggerMiddleware(); if (rflMiddleware) { middlewareInjector(rflMiddleware); } })(); }, []); return ( <Provider store={store}> ... </Provider> ); } ``` ### Creating file logger Let's consider an example of a file logger for navigation state changes 1. Create file logger for navigation ```ts import { Platform } from 'react-native'; import { addFileLogger, getFileLogger, SupportedAndroidRootDirsEnum, SupportedIosRootDirsEnum } from 'react-native-redux-file-logger'; const rootDir = Platform.OS === 'android' ? SupportedAndroidRootDirsEnum.Files : SupportedIosRootDirsEnum.Cache await addFileLogger('navigation-state', { rootDir, nestedDir: 'logs', fileName: 'navigation.json' }); export const navigationStateLogger = getFileLogger(tag); ``` 2. Configure navigation state listener ```ts import {createNavigationContainerRef} from '@react-navigation/native'; import {EventArg, EventListenerCallback, EventMapCore} from '@react-navigation/core'; export const navigationRef = createNavigationContainerRef(); export type StateListenerCallbackType = EventListenerCallback<EventMapCore<any>, 'state'>; export function addNavigationStateListener(listener: StateListenerCallbackType): void { navigationRef.addListener('state', listener); } ``` 3. Pass `ref` to `NavigationContainer` ```tsx import {navigationRef} from 'path/to/file' return ( <NavigationContainer ref={navigationRef} > ... </NavigationContainer> ) ``` 4. Use logger inside navigation state listener ```ts import {addStateListener, StateListenerCallbackType} from 'path/to/file' import {navigationStateLogger} from 'path/to/file' const stateListener: StateListenerCallbackType = e => { if (this.isInitialized && e.data.state && e.type) { navigationStateLogger.log(e.data.state); } }; addNavigationStateListener(stateListener); ``` ## Creating archive Archiving logs from all file logger instances to a specified file. If you need to archive logs for a single instance, pass the `tag` as a second parameter (see API section). ```ts import { Platform } from 'react-native'; import { archive, SupportedAndroidRootDirsEnum, SupportedIosRootDirsEnum } from 'react-native-redux-file-logger'; const zipFilePath = await archive({ rootDir: Platform.OS === 'android' ? SupportedAndroidRootDirsEnum.Files : SupportedIosRootDirsEnum.Cache, fileName: 'logs.zip', }); ``` ## API ### Types #### LoggerOptions ```ts type LoggerOptions<TState = any, TLogger extends {log: (message: string) => void} = Logger> = { actionInclusionPredicate?: InclusionPredicate<TState>; diffInclusionPredicate?: InclusionPredicate<TState>; shouldLogPrevState?: boolean; shouldLogNextState?: boolean; showDiff?: boolean; stateTransformer?: (state: any) => any; logger: TLogger; }; ``` - **actionInclusionPredicate** - actions filtering function, called before middleware logic execution. If returns false, the middleware won't be applied - **actionInclusionPredicate** - diffs filtering function - **shouldLogPrevState** - whether to add previous state to the file - **shouldLogNextState** - whether to add next state to the file - **showDiff** - whether to add diff(prev to next) state to the file - **stateTransformer** - accepts prev & next state and applies its logic to is - **logger** - logger instance, that implements `log(message: string) => void` method #### InclusionPredicate ```ts type InclusionPredicate<TState> = (action: AnyAction, getState: () => TState) => boolean; ``` #### FileConfig ```ts type FileConfig = { fileName: string; nestedDir?: string; rootDir: SupportedIosRootDirsEnum | SupportedAndroidRootDirsEnum | string; } ``` Example: - rootDir: `/storage/emulated/0/Android/data/com.reduxfileloggerexample/files/` (i.e. SupportedAndroidRootDirsEnum.Files) - nestedDir: `logs` - fileName: `time-travel.json` - Resulting path: `/storage/emulated/0/Android/data/com.reduxfileloggerexample/files/logs/time-travel.json` ### Constants #### SupportedIosRootDirsEnum Dirs that correspond to `FileManager.SearchPathDirectory` in `Foundation` ```ts enum SupportedIosRootDirsEnum { Downloads = 'Downloads', Documents = 'Documents', AppSupportFiles = 'AppSupportFiles', Cache = 'Cache', } ``` #### SupportedAndroidRootDirsEnum Dirs taken from `ReactApplicationContext` ```ts enum SupportedAndroidRootDirsEnum { Cache = 'Cache', Files = 'Files', } ``` ### Functions #### createReduxFileLoggerMiddleware ```ts async function createReduxFileLoggerMiddleware< State = any, BasicAction extends Action = AnyAction, ExtraThunkArg = undefined >( tag: string, fileConfig: FileConfig, loggerOptions: Omit<LoggerOptions<State>, 'logger'> ): Promise<ThunkMiddleware<State, BasicAction, ExtraThunkArg>> {} ``` Creates a Redux file logger middleware. Notice, that it doesn't accept `logger`, because it's encapsulated - **tag** - unique logger identifier - **fileConfig** - determines the file path (see above) - **loggerOptions** - logger options (see above) #### createLoggerMiddleware ```ts function createLoggerMiddleware< State = any, BasicAction extends Action = AnyAction, ExtraThunkArg = undefined >(options: LoggerOptions<State>): ThunkMiddleware<State, BasicAction, ExtraThunkArg> {} ``` Creates a logger middleware. Unlike `createReduxFileLoggerMiddleware()`, it accepts a `logger` instance, so you can provide your own implementation. - **options** - logger options (see above) #### addFileLogger ```ts const addFileLogger = async (tag: string, fileConfig: FileConfig) => Promise<void> ``` Creates a unique file logger instance and stores in a map. Use it when you need to add file logger in addition to Redux (e.g. navigation state change, see example above). - **tag** - unique logger identifier - **fileConfig** - determines the file path (see above) #### getFileLogger ```ts interface Logger { log: (message: string) => void; } const getFileLogger = (tag: string) => Logger | undefined ``` Gets a logger instance from map by `tag`. - **tag** - unique logger identifier #### archive ```ts async function archive(fileConfig: FileConfig, tag?: string): Promise<string> {} ``` Archive logs from all logger instances (or for a specific instance if `tag` is provided) to a file. Supports only `zip` format for Android. After a successful archive creation the logs are emptied. - **tag** - unique logger identifier - **fileConfig** - determines the file path (see above) ### Utils #### createMiddlewareInjector ```ts function createMiddlewareInjector<S = any, D extends Dispatch = Dispatch>(store: MiddlewareAPI<D, S>) { return function inject(middleware: Middleware) { store.dispatch = middleware(store)(store.dispatch); }; } ``` Create an injector that can be used to add middlewares. ### Recipes #### Pulling files from Android emulators ```shell adb root adb pull /storage/emulated/0/Android/data/com.reduxfileloggerexample/files/example/time-travel.json /Users/{$user}/Desktop ``` #### Browsing files on iOS emulators 1. Copy `archive` result to the clipboard 2. Finder --> Go --> Go to folder 3. Paste the value & hit enter ## License MIT ## Thanks Inspired by [Oleg Titaev](https://github.com/yadormad ) --- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)