0.2.0 • Published 1 year ago

react-native-redux-file-logger v0.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

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