/react-native-redux-file-logger

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).

Primary LanguageTypeScriptMIT LicenseMIT

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.

Motivation

3rd party libraries like 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

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
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;
}
  1. Create the store and a middleware injector. We can't just pass the middleware to configureStore(), because it's created asynchronously.
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;
  1. Create Redux file logger middleware and inject it to the store.
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
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);
  1. Configure navigation state listener
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);
}
  1. Pass ref to NavigationContainer
import {navigationRef} from 'path/to/file'

return (
  <NavigationContainer ref={navigationRef} >
    ...
  </NavigationContainer>
)
  1. Use logger inside navigation state listener
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).

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

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

type InclusionPredicate<TState> = (action: AnyAction, getState: () => TState) => boolean;

FileConfig

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

enum SupportedIosRootDirsEnum {
  Downloads = 'Downloads',
  Documents = 'Documents',
  AppSupportFiles = 'AppSupportFiles',
  Cache = 'Cache',
}

SupportedAndroidRootDirsEnum

Dirs taken from ReactApplicationContext

enum SupportedAndroidRootDirsEnum {
  Cache = 'Cache',
  Files = 'Files',
}

Functions

createReduxFileLoggerMiddleware

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

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

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

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

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

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

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


Made with create-react-native-library