├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── spec ├── diff.spec.js ├── helpers.spec.js └── index.spec.js └── src ├── core.js ├── defaults.js ├── diff.js ├── helpers.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | docker: 5 | - image: circleci/node:8 6 | 7 | jobs: 8 | build: 9 | <<: *defaults 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | keys: 14 | - v1-dependencies-{{ checksum "package.json" }} 15 | - v1-dependencies- 16 | - run: npm install 17 | - save_cache: 18 | paths: 19 | - node_modules 20 | key: v1-dependencies-{{ checksum "package.json" }} 21 | - run: npm run lint 22 | - run: npm test 23 | - persist_to_workspace: 24 | root: ~/project 25 | paths: 26 | - dist/ 27 | - .npmrc 28 | - node_modules/ 29 | 30 | workflows: 31 | version: 2 32 | build-: 33 | jobs: 34 | - build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | npm-debug.log 5 | 6 | .nyc_output 7 | coverage 8 | *.lcov 9 | 10 | example/build 11 | dist 12 | lib 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Eugene Rodionov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logger for Redux 2 | [![npm](https://img.shields.io/npm/v/redux-logger.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/redux-logger) 3 | [![npm](https://img.shields.io/npm/dm/redux-logger.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/redux-logger) 4 | [![Build Status](https://circleci.com/gh/LogRocket/redux-logger/tree/master.svg?style=svg)](https://circleci.com/gh/LogRocket/redux-logger/tree/master) 5 | 6 | ![redux-logger](http://i.imgur.com/CgAuHlE.png) 7 | 8 | Now maintained by [LogRocket](https://logrocket.com?cid=github_redux)! 9 | 10 | [![](https://i.imgur.com/Yp5mUx2.png)](https://logrocket.com?cid=github_redux) 11 | 12 | > LogRocket is a production Redux logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay Redux actions + state, network requests, console logs, and see a video of what the user saw. 13 | 14 | For more informatiom about the future of redux-logger, check out the [discussion here](https://github.com/LogRocket/redux-logger/issues/314). 15 | 16 | ## Table of contents 17 | * [Install](#install) 18 | * [Usage](#usage) 19 | * [Options](#options) 20 | * [Recipes](#recipes) 21 | * [Log only in development](#log-only-in-development) 22 | * [Log everything except actions with certain type](#log-everything-except-actions-with-certain-type) 23 | * [Collapse actions with certain type](#collapse-actions-with-certain-type) 24 | * [Transform Immutable (without `combineReducers`)](#transform-immutable-without-combinereducers) 25 | * [Transform Immutable (with `combineReducers`)](#transform-immutable-with-combinereducers) 26 | * [Log batched actions](#log-batched-actions) 27 | * [To Do](#to-do) 28 | * [Known issues](#known-issues) (with `react-native` only at this moment) 29 | * [License](#license) 30 | 31 | ## Install 32 | `npm i --save redux-logger` 33 | 34 | Typescript types are also available, via [DefinitelyTyped](https://www.npmjs.com/package/@types/redux-logger): 35 | 36 | `npm i @types/redux-logger` 37 | 38 | ## Usage 39 | ```javascript 40 | import { applyMiddleware, createStore } from 'redux'; 41 | 42 | // Logger with default options 43 | import logger from 'redux-logger' 44 | const store = createStore( 45 | reducer, 46 | applyMiddleware(logger) 47 | ) 48 | 49 | // Note passing middleware as the third argument requires redux@>=3.1.0 50 | ``` 51 | 52 | Or you can create your own logger with custom [options](https://github.com/LogRocket/redux-logger#options): 53 | ```javascript 54 | import { applyMiddleware, createStore } from 'redux'; 55 | import { createLogger } from 'redux-logger' 56 | 57 | const logger = createLogger({ 58 | // ...options 59 | }); 60 | 61 | const store = createStore( 62 | reducer, 63 | applyMiddleware(logger) 64 | ); 65 | ``` 66 | 67 | Note: logger **must be** the last middleware in chain, otherwise it will log thunk and promise, not actual actions ([#20](https://github.com/LogRocket/redux-logger/issues/20)). 68 | 69 | ## Options 70 | ```javascript 71 | { 72 | predicate, // if specified this function will be called before each action is processed with this middleware. 73 | collapsed, // takes a Boolean or optionally a Function that receives `getState` function for accessing current store state and `action` object as parameters. Returns `true` if the log group should be collapsed, `false` otherwise. 74 | duration = false: Boolean, // print the duration of each action? 75 | timestamp = true: Boolean, // print the timestamp with each action? 76 | 77 | level = 'log': 'log' | 'console' | 'warn' | 'error' | 'info', // console's level 78 | colors: ColorsObject, // colors for title, prev state, action and next state: https://github.com/LogRocket/redux-logger/blob/master/src/defaults.js#L12-L18 79 | titleFormatter, // Format the title used when logging actions. 80 | 81 | stateTransformer, // Transform state before print. Eg. convert Immutable object to plain JSON. 82 | actionTransformer, // Transform action before print. Eg. convert Immutable object to plain JSON. 83 | errorTransformer, // Transform error before print. Eg. convert Immutable object to plain JSON. 84 | 85 | logger = console: LoggerObject, // implementation of the `console` API. 86 | logErrors = true: Boolean, // should the logger catch, log, and re-throw errors? 87 | 88 | diff = false: Boolean, // (alpha) show diff between states? 89 | diffPredicate // (alpha) filter function for showing states diff, similar to `predicate` 90 | } 91 | ``` 92 | 93 | ### Options description 94 | 95 | #### __level (String | Function | Object)__ 96 | Level of `console`. `warn`, `error`, `info` or [else](https://developer.mozilla.org/en/docs/Web/API/console). 97 | 98 | It can be a function `(action: Object) => level: String`. 99 | 100 | It can be an object with level string for: `prevState`, `action`, `nextState`, `error` 101 | 102 | It can be an object with getter functions: `prevState`, `action`, `nextState`, `error`. Useful if you want to print 103 | message based on specific state or action. Set any of them to `false` if you want to hide it. 104 | 105 | * `prevState(prevState: Object) => level: String` 106 | * `action(action: Object) => level: String` 107 | * `nextState(nextState: Object) => level: String` 108 | * `error(error: Any, prevState: Object) => level: String` 109 | 110 | *Default: `log`* 111 | 112 | #### __duration (Boolean)__ 113 | Print duration of each action? 114 | 115 | *Default: `false`* 116 | 117 | #### __timestamp (Boolean)__ 118 | Print timestamp with each action? 119 | 120 | *Default: `true`* 121 | 122 | #### __colors (Object)__ 123 | Object with color getter functions: `title`, `prevState`, `action`, `nextState`, `error`. Useful if you want to paint 124 | message based on specific state or action. Set any of them to `false` if you want to show plain message without colors. 125 | 126 | * `title(action: Object) => color: String` 127 | * `prevState(prevState: Object) => color: String` 128 | * `action(action: Object) => color: String` 129 | * `nextState(nextState: Object) => color: String` 130 | * `error(error: Any, prevState: Object) => color: String` 131 | 132 | #### __logger (Object)__ 133 | Implementation of the `console` API. Useful if you are using a custom, wrapped version of `console`. 134 | 135 | *Default: `console`* 136 | 137 | #### __logErrors (Boolean)__ 138 | Should the logger catch, log, and re-throw errors? This makes it clear which action triggered the error but makes "break 139 | on error" in dev tools harder to use, as it breaks on re-throw rather than the original throw location. 140 | 141 | *Default: `true`* 142 | 143 | #### __collapsed = (getState: Function, action: Object, logEntry: Object) => Boolean__ 144 | Takes a boolean or optionally a function that receives `getState` function for accessing current store state and `action` object as parameters. Returns `true` if the log group should be collapsed, `false` otherwise. 145 | 146 | *Default: `false`* 147 | 148 | #### __predicate = (getState: Function, action: Object) => Boolean__ 149 | If specified this function will be called before each action is processed with this middleware. 150 | Receives `getState` function for accessing current store state and `action` object as parameters. Returns `true` if action should be logged, `false` otherwise. 151 | 152 | *Default: `null` (always log)* 153 | 154 | #### __stateTransformer = (state: Object) => state__ 155 | Transform state before print. Eg. convert Immutable object to plain JSON. 156 | 157 | *Default: identity function* 158 | 159 | #### __actionTransformer = (action: Object) => action__ 160 | Transform action before print. Eg. convert Immutable object to plain JSON. 161 | 162 | *Default: identity function* 163 | 164 | #### __errorTransformer = (error: Any) => error__ 165 | Transform error before print. 166 | 167 | *Default: identity function* 168 | 169 | #### __titleFormatter = (action: Object, time: String?, took: Number?) => title__ 170 | Format the title used for each action. 171 | 172 | *Default: prints something like `action @ ${time} ${action.type} (in ${took.toFixed(2)} ms)`* 173 | 174 | #### __diff (Boolean)__ 175 | Show states diff. 176 | 177 | *Default: `false`* 178 | 179 | #### __diffPredicate = (getState: Function, action: Object) => Boolean__ 180 | Filter states diff for certain cases. 181 | 182 | *Default: `undefined`* 183 | 184 | ## Recipes 185 | ### Log only in development 186 | ```javascript 187 | const middlewares = []; 188 | 189 | if (process.env.NODE_ENV === `development`) { 190 | const { logger } = require(`redux-logger`); 191 | 192 | middlewares.push(logger); 193 | } 194 | 195 | const store = compose(applyMiddleware(...middlewares))(createStore)(reducer); 196 | ``` 197 | 198 | ### Log everything except actions with certain type 199 | ```javascript 200 | createLogger({ 201 | predicate: (getState, action) => action.type !== AUTH_REMOVE_TOKEN 202 | }); 203 | ``` 204 | 205 | ### Collapse actions with certain type 206 | ```javascript 207 | createLogger({ 208 | collapsed: (getState, action) => action.type === FORM_CHANGE 209 | }); 210 | ``` 211 | 212 | ### Collapse actions that don't have errors 213 | ```javascript 214 | createLogger({ 215 | collapsed: (getState, action, logEntry) => !logEntry.error 216 | }); 217 | ``` 218 | 219 | ### Transform Immutable (without `combineReducers`) 220 | ```javascript 221 | import { Iterable } from 'immutable'; 222 | 223 | const stateTransformer = (state) => { 224 | if (Iterable.isIterable(state)) return state.toJS(); 225 | else return state; 226 | }; 227 | 228 | const logger = createLogger({ 229 | stateTransformer, 230 | }); 231 | ``` 232 | 233 | ### Transform Immutable (with `combineReducers`) 234 | ```javascript 235 | const logger = createLogger({ 236 | stateTransformer: (state) => { 237 | let newState = {}; 238 | 239 | for (var i of Object.keys(state)) { 240 | if (Immutable.Iterable.isIterable(state[i])) { 241 | newState[i] = state[i].toJS(); 242 | } else { 243 | newState[i] = state[i]; 244 | } 245 | }; 246 | 247 | return newState; 248 | } 249 | }); 250 | ``` 251 | 252 | ### Log batched actions 253 | Thanks to [@smashercosmo](https://github.com/smashercosmo) 254 | ```javascript 255 | import { createLogger } from 'redux-logger'; 256 | 257 | const actionTransformer = action => { 258 | if (action.type === 'BATCHING_REDUCER.BATCH') { 259 | action.payload.type = action.payload.map(next => next.type).join(' => '); 260 | return action.payload; 261 | } 262 | 263 | return action; 264 | }; 265 | 266 | const level = 'info'; 267 | 268 | const logger = {}; 269 | 270 | for (const method in console) { 271 | if (typeof console[method] === 'function') { 272 | logger[method] = console[method].bind(console); 273 | } 274 | } 275 | 276 | logger[level] = function levelFn(...args) { 277 | const lastArg = args.pop(); 278 | 279 | if (Array.isArray(lastArg)) { 280 | return lastArg.forEach(item => { 281 | console[level].apply(console, [...args, item]); 282 | }); 283 | } 284 | 285 | console[level].apply(console, arguments); 286 | }; 287 | 288 | export default createLogger({ 289 | level, 290 | actionTransformer, 291 | logger 292 | }); 293 | ``` 294 | 295 | ## To Do 296 | - [x] Update eslint config to [airbnb's](https://www.npmjs.com/package/eslint-config-airbnb) 297 | - [ ] Clean up code, because it's very messy, to be honest 298 | - [ ] Write tests 299 | - [ ] Node.js support 300 | - [ ] React-native support 301 | 302 | Feel free to create PR for any of those tasks! 303 | 304 | ## Known issues 305 | * Performance issues in react-native ([#32](https://github.com/LogRocket/redux-logger/issues/32)) 306 | 307 | ## License 308 | MIT 309 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-logger", 3 | "version": "4.0.0", 4 | "description": "Logger for Redux", 5 | "main": "dist/redux-logger.js", 6 | "module": "dist/redux-logger.es.js", 7 | "jsnext:main": "dist/redux-logger.es.js", 8 | "scripts": { 9 | "lint": "eslint src", 10 | "test": "npm run lint && npm run spec", 11 | "spec": "nyc --all --silent --require babel-core/register mocha --plugins transform-inline-environment-variables --recursive spec/*.spec.js", 12 | "spec:watch": "npm run spec -- --watch", 13 | "coverage": "nyc report", 14 | "coverage:html": "nyc report --reporter=html && http-server -p 8077 ./coverage -o", 15 | "coverage:production": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 16 | "clean": "rimraf dist", 17 | "uglify": "uglifyjs dist/redux-logger.js -cm -o dist/redux-logger.js", 18 | "build": "rollup -c && npm run uglify", 19 | "precommit": "npm test && npm run lint", 20 | "prepublish": "npm run clean && npm test && npm run lint && npm run build" 21 | }, 22 | "eslintConfig": { 23 | "extends": "airbnb", 24 | "rules": { 25 | "no-console": "off" 26 | }, 27 | "env": { 28 | "browser": true, 29 | "mocha": true 30 | } 31 | }, 32 | "nyc": { 33 | "exclude": [ 34 | "node_modules", 35 | "spec", 36 | "example", 37 | "lib", 38 | "dist", 39 | "coverage", 40 | "rollup.config.js" 41 | ] 42 | }, 43 | "files": [ 44 | "dist", 45 | "src" 46 | ], 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/LogRocket/redux-logger.git" 50 | }, 51 | "keywords": [ 52 | "redux", 53 | "logger", 54 | "redux-logger", 55 | "middleware" 56 | ], 57 | "author": "Eugene Rodionov (https://github.com/evgenyrodionov)", 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/LogRocket/redux-logger/issues" 61 | }, 62 | "homepage": "https://github.com/LogRocket/redux-logger#readme", 63 | "devDependencies": { 64 | "babel-core": "^6.24.0", 65 | "babel-plugin-external-helpers": "^6.22.0", 66 | "babel-plugin-transform-inline-environment-variables": "6.8.0", 67 | "babel-preset-es2015": "^6.24.0", 68 | "chai": "3.5.0", 69 | "codecov": "1.0.1", 70 | "eslint": "^3.19.0", 71 | "eslint-config-airbnb": "^14.1.0", 72 | "eslint-plugin-import": "^2.2.0", 73 | "eslint-plugin-jsx-a11y": "^4.0.0", 74 | "eslint-plugin-react": "^6.10.3", 75 | "http-server": "0.9.0", 76 | "husky": "^0.13.2", 77 | "mocha": "3.1.2", 78 | "nyc": "9.0.1", 79 | "redux": "^3.6.0", 80 | "rimraf": "^2.6.1", 81 | "rollup": "^0.41.6", 82 | "rollup-plugin-babel": "^2.7.1", 83 | "rollup-plugin-commonjs": "^8.0.2", 84 | "rollup-plugin-node-resolve": "^3.0.0", 85 | "sinon": "^1.17.7", 86 | "uglify-js": "^3.0.8" 87 | }, 88 | "dependencies": { 89 | "deep-diff": "^0.3.5" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | 5 | export default { 6 | entry: 'src/index.js', 7 | exports: 'named', 8 | plugins: [ 9 | babel({ 10 | babelrc: false, 11 | presets: [ 12 | [ 13 | 'es2015', 14 | { 15 | modules: false, 16 | }, 17 | ], 18 | ], 19 | plugins: ['external-helpers'], 20 | }), 21 | commonjs({ 22 | include: 'node_modules/**', 23 | }), 24 | nodeResolve({ 25 | jsnext: true, 26 | main: true, 27 | browser: true, 28 | }) 29 | ], 30 | targets: [ 31 | { 32 | format: 'umd', 33 | moduleName: 'reduxLogger', 34 | dest: 'dist/redux-logger.js', 35 | }, 36 | { 37 | format: 'es', 38 | dest: 'dist/redux-logger.es.js' 39 | } 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /spec/diff.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import { expect } from 'chai'; 3 | import { style, render, default as diffLogger } from '../src/diff'; 4 | 5 | context('Diff', () => { 6 | describe('style', () => { 7 | it('return css rules for the given kind of diff changes', () => { 8 | expect(style('E')).to.equal('color: #2196F3; font-weight: bold'); 9 | expect(style('N')).to.equal('color: #4CAF50; font-weight: bold'); 10 | expect(style('D')).to.equal('color: #F44336; font-weight: bold'); 11 | expect(style('A')).to.equal('color: #2196F3; font-weight: bold'); 12 | }); 13 | }); 14 | 15 | describe('render', () => { 16 | it('should return an array indicating the changes', () => { 17 | expect(render({ 18 | kind: 'E', 19 | path: ['capitain', 'name'], 20 | lhs: 'kirk', 21 | rhs: 'picard', 22 | })).to.eql(['capitain.name', 'kirk', '→', 'picard']); 23 | }); 24 | 25 | it('should return an array indicating an added property/element', () => { 26 | expect(render({ 27 | kind: 'N', 28 | path: ['crew', 'engineer'], 29 | rhs: 'geordi', 30 | })).to.eql(['crew.engineer', 'geordi']); 31 | }); 32 | 33 | it('should return an array indicating a removed property/element', () => { 34 | expect(render({ 35 | kind: 'D', 36 | path: ['crew', 'security'], 37 | })).to.eql(['crew.security']); 38 | }); 39 | 40 | it('should return an array indicating a changed index', () => { 41 | expect(render({ 42 | kind: 'A', 43 | path: ['crew'], 44 | index: 2, 45 | item: { 46 | kind: 'N', 47 | rhs: 'after', 48 | }, 49 | })).to.eql(['crew[2]', { 50 | kind: 'N', 51 | rhs: 'after', 52 | }]); 53 | }); 54 | 55 | it('should return an empty array', () => { 56 | expect(render({})).to.eql([]); 57 | }); 58 | }); 59 | 60 | describe('diffLogger', () => { 61 | let logger 62 | 63 | beforeEach(() => { 64 | logger = { 65 | log: sinon.spy(), 66 | groupCollapsed: sinon.spy(), 67 | groupEnd: sinon.spy(), 68 | group: sinon.spy(), 69 | }; 70 | }); 71 | 72 | it('should show no diff with group collapsed', () => { 73 | diffLogger({}, {}, logger, true); 74 | 75 | expect(logger.group.calledOnce).to.be.false; 76 | expect(logger.groupCollapsed.calledOnce).to.be.true; 77 | expect(logger.groupEnd.calledOnce).to.be.true; 78 | expect(logger.log.calledOnce).to.be.true; 79 | expect(logger.log.calledWith('—— no diff ——')).to.be.true; 80 | }); 81 | 82 | it('should show no diff with group not collapsed', () => { 83 | diffLogger({}, {}, logger, false); 84 | 85 | expect(logger.group.calledOnce).to.be.true; 86 | expect(logger.groupCollapsed.calledOnce).to.be.false; 87 | expect(logger.groupEnd.calledOnce).to.be.true; 88 | expect(logger.log.calledOnce).to.be.true; 89 | expect(logger.log.calledWith('—— no diff ——')).to.be.true; 90 | }); 91 | 92 | it('should log no diff without group', () => { 93 | const loggerWithNoGroupCollapsed = Object.assign({}, logger, { 94 | groupCollapsed: () => { 95 | throw new Error() 96 | }, 97 | groupEnd: () => { 98 | throw new Error() 99 | }, 100 | }); 101 | 102 | diffLogger({}, {}, loggerWithNoGroupCollapsed, true); 103 | 104 | expect(loggerWithNoGroupCollapsed.log.calledWith('diff')).to.be.true; 105 | expect(loggerWithNoGroupCollapsed.log.calledWith('—— no diff ——')).to.be.true; 106 | expect(loggerWithNoGroupCollapsed.log.calledWith('—— diff end —— ')).to.be.true; 107 | }); 108 | 109 | it('should log the diffs', () => { 110 | diffLogger({name: 'kirk'}, {name: 'picard'}, logger, false); 111 | 112 | expect(logger.log.calledWithExactly('%c CHANGED:', 'color: #2196F3; font-weight: bold', 'name', 'kirk', '→', 'picard')).to.be.true; 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /spec/helpers.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { repeat, pad, formatTime } from '../src/helpers'; 3 | 4 | context('Helpers', () => { 5 | describe('repeat', () => { 6 | it('should repeat a string the number of indicated times', () => { 7 | expect(repeat('teacher', 3)).to.equal('teacherteacherteacher'); 8 | }); 9 | }); 10 | 11 | describe('pad', () => { 12 | it('should add leading zeros to a number given a maximun length', () => { 13 | expect(pad(56, 4)).to.equal('0056'); 14 | }); 15 | }); 16 | 17 | describe('formatTime', () => { 18 | it('should format a time given a Date object', () => { 19 | const time = new Date('December 25, 1995 23:15:30'); 20 | expect(formatTime(time)).to.equal('23:15:30.000'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import { applyMiddleware, createStore } from 'redux'; 3 | import { default as logger, createLogger } from '../src'; 4 | 5 | context('default logger', () => { 6 | describe('init', () => { 7 | beforeEach(() => { 8 | sinon.spy(console, 'error'); 9 | }); 10 | 11 | afterEach(() => { 12 | console.error.restore(); 13 | }); 14 | 15 | it('should be ok', () => { 16 | const store = createStore(() => ({}), applyMiddleware(logger)); 17 | 18 | store.dispatch({ type: 'foo' }); 19 | sinon.assert.notCalled(console.error); 20 | }); 21 | }); 22 | }); 23 | 24 | context('createLogger', () => { 25 | describe('init', () => { 26 | beforeEach(() => { 27 | sinon.spy(console, 'error'); 28 | }); 29 | 30 | afterEach(() => { 31 | console.error.restore(); 32 | }); 33 | 34 | it('should throw error if passed direct to applyMiddleware', () => { 35 | const store = createStore(() => ({}), applyMiddleware(createLogger)); 36 | 37 | store.dispatch({ type: 'foo' }); 38 | sinon.assert.calledOnce(console.error); 39 | }); 40 | 41 | it('should be ok', () => { 42 | const store = createStore(() => ({}), applyMiddleware(createLogger())); 43 | 44 | store.dispatch({ type: 'foo' }); 45 | sinon.assert.notCalled(console.error); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from './helpers'; 2 | import diffLogger from './diff'; 3 | 4 | /** 5 | * Get log level string based on supplied params 6 | * 7 | * @param {string | function | object} level - console[level] 8 | * @param {object} action - selected action 9 | * @param {array} payload - selected payload 10 | * @param {string} type - log entry type 11 | * 12 | * @returns {string} level 13 | */ 14 | function getLogLevel(level, action, payload, type) { 15 | switch (typeof level) { 16 | case 'object': 17 | return typeof level[type] === 'function' ? level[type](...payload) : level[type]; 18 | case 'function': 19 | return level(action); 20 | default: 21 | return level; 22 | } 23 | } 24 | 25 | function defaultTitleFormatter(options) { 26 | const { timestamp, duration } = options; 27 | 28 | return (action, time, took) => { 29 | const parts = ['action']; 30 | 31 | parts.push(`%c${String(action.type)}`); 32 | if (timestamp) parts.push(`%c@ ${time}`); 33 | if (duration) parts.push(`%c(in ${took.toFixed(2)} ms)`); 34 | 35 | return parts.join(' '); 36 | }; 37 | } 38 | 39 | function printBuffer(buffer, options) { 40 | const { 41 | logger, 42 | actionTransformer, 43 | titleFormatter = defaultTitleFormatter(options), 44 | collapsed, 45 | colors, 46 | level, 47 | diff, 48 | } = options; 49 | 50 | const isUsingDefaultFormatter = typeof options.titleFormatter === 'undefined'; 51 | 52 | buffer.forEach((logEntry, key) => { 53 | const { started, startedTime, action, prevState, error } = logEntry; 54 | let { took, nextState } = logEntry; 55 | const nextEntry = buffer[key + 1]; 56 | 57 | if (nextEntry) { 58 | nextState = nextEntry.prevState; 59 | took = nextEntry.started - started; 60 | } 61 | 62 | // Message 63 | const formattedAction = actionTransformer(action); 64 | const isCollapsed = typeof collapsed === 'function' 65 | ? collapsed(() => nextState, action, logEntry) 66 | : collapsed; 67 | 68 | const formattedTime = formatTime(startedTime); 69 | const titleCSS = colors.title ? `color: ${colors.title(formattedAction)};` : ''; 70 | const headerCSS = ['color: gray; font-weight: lighter;']; 71 | headerCSS.push(titleCSS); 72 | if (options.timestamp) headerCSS.push('color: gray; font-weight: lighter;'); 73 | if (options.duration) headerCSS.push('color: gray; font-weight: lighter;'); 74 | const title = titleFormatter(formattedAction, formattedTime, took); 75 | 76 | // Render 77 | try { 78 | if (isCollapsed) { 79 | if (colors.title && isUsingDefaultFormatter) { 80 | logger.groupCollapsed(`%c ${title}`, ...headerCSS); 81 | } else logger.groupCollapsed(title); 82 | } else if (colors.title && isUsingDefaultFormatter) { 83 | logger.group(`%c ${title}`, ...headerCSS); 84 | } else { 85 | logger.group(title); 86 | } 87 | } catch (e) { 88 | logger.log(title); 89 | } 90 | 91 | const prevStateLevel = getLogLevel(level, formattedAction, [prevState], 'prevState'); 92 | const actionLevel = getLogLevel(level, formattedAction, [formattedAction], 'action'); 93 | const errorLevel = getLogLevel(level, formattedAction, [error, prevState], 'error'); 94 | const nextStateLevel = getLogLevel(level, formattedAction, [nextState], 'nextState'); 95 | 96 | if (prevStateLevel) { 97 | if (colors.prevState) { 98 | const styles = `color: ${colors.prevState(prevState)}; font-weight: bold`; 99 | 100 | logger[prevStateLevel]('%c prev state', styles, prevState); 101 | } else logger[prevStateLevel]('prev state', prevState); 102 | } 103 | 104 | if (actionLevel) { 105 | if (colors.action) { 106 | const styles = `color: ${colors.action(formattedAction)}; font-weight: bold`; 107 | 108 | logger[actionLevel]('%c action ', styles, formattedAction); 109 | } else logger[actionLevel]('action ', formattedAction); 110 | } 111 | 112 | if (error && errorLevel) { 113 | if (colors.error) { 114 | const styles = `color: ${colors.error(error, prevState)}; font-weight: bold;`; 115 | 116 | logger[errorLevel]('%c error ', styles, error); 117 | } else logger[errorLevel]('error ', error); 118 | } 119 | 120 | if (nextStateLevel) { 121 | if (colors.nextState) { 122 | const styles = `color: ${colors.nextState(nextState)}; font-weight: bold`; 123 | 124 | logger[nextStateLevel]('%c next state', styles, nextState); 125 | } else logger[nextStateLevel]('next state', nextState); 126 | } 127 | 128 | if (logger.withTrace) { 129 | logger.groupCollapsed('TRACE'); 130 | logger.trace(); 131 | logger.groupEnd(); 132 | } 133 | 134 | if (diff) { 135 | diffLogger(prevState, nextState, logger, isCollapsed); 136 | } 137 | 138 | try { 139 | logger.groupEnd(); 140 | } catch (e) { 141 | logger.log('—— log end ——'); 142 | } 143 | }); 144 | } 145 | 146 | export default printBuffer; 147 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | level: 'log', 3 | logger: console, 4 | logErrors: true, 5 | collapsed: undefined, 6 | predicate: undefined, 7 | duration: false, 8 | timestamp: true, 9 | stateTransformer: state => state, 10 | actionTransformer: action => action, 11 | errorTransformer: error => error, 12 | colors: { 13 | title: () => 'inherit', 14 | prevState: () => '#9E9E9E', 15 | action: () => '#03A9F4', 16 | nextState: () => '#4CAF50', 17 | error: () => '#F20404', 18 | }, 19 | diff: false, 20 | diffPredicate: undefined, 21 | 22 | // Deprecated options 23 | transformer: undefined, 24 | }; 25 | -------------------------------------------------------------------------------- /src/diff.js: -------------------------------------------------------------------------------- 1 | import differ from 'deep-diff'; 2 | 3 | // https://github.com/flitbit/diff#differences 4 | const dictionary = { 5 | E: { 6 | color: '#2196F3', 7 | text: 'CHANGED:', 8 | }, 9 | N: { 10 | color: '#4CAF50', 11 | text: 'ADDED:', 12 | }, 13 | D: { 14 | color: '#F44336', 15 | text: 'DELETED:', 16 | }, 17 | A: { 18 | color: '#2196F3', 19 | text: 'ARRAY:', 20 | }, 21 | }; 22 | 23 | export function style(kind) { 24 | return `color: ${dictionary[kind].color}; font-weight: bold`; 25 | } 26 | 27 | export function render(diff) { 28 | const { kind, path, lhs, rhs, index, item } = diff; 29 | 30 | switch (kind) { 31 | case 'E': 32 | return [path.join('.'), lhs, '→', rhs]; 33 | case 'N': 34 | return [path.join('.'), rhs]; 35 | case 'D': 36 | return [path.join('.')]; 37 | case 'A': 38 | return [`${path.join('.')}[${index}]`, item]; 39 | default: 40 | return []; 41 | } 42 | } 43 | 44 | export default function diffLogger(prevState, newState, logger, isCollapsed) { 45 | const diff = differ(prevState, newState); 46 | 47 | try { 48 | if (isCollapsed) { 49 | logger.groupCollapsed('diff'); 50 | } else { 51 | logger.group('diff'); 52 | } 53 | } catch (e) { 54 | logger.log('diff'); 55 | } 56 | 57 | if (diff) { 58 | diff.forEach((elem) => { 59 | const { kind } = elem; 60 | const output = render(elem); 61 | 62 | logger.log(`%c ${dictionary[kind].text}`, style(kind), ...output); 63 | }); 64 | } else { 65 | logger.log('—— no diff ——'); 66 | } 67 | 68 | try { 69 | logger.groupEnd(); 70 | } catch (e) { 71 | logger.log('—— diff end —— '); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export const repeat = (str, times) => (new Array(times + 1)).join(str); 2 | 3 | export const pad = (num, maxLength) => repeat('0', maxLength - num.toString().length) + num; 4 | 5 | export const formatTime = time => `${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`; 6 | 7 | // Use performance API if it's available in order to get better precision 8 | export const timer = 9 | (typeof performance !== 'undefined' && performance !== null) && typeof performance.now === 'function' ? 10 | performance : 11 | Date; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import printBuffer from './core'; 2 | import { timer } from './helpers'; 3 | import defaults from './defaults'; 4 | /* eslint max-len: ["error", 110, { "ignoreComments": true }] */ 5 | /** 6 | * Creates logger with following options 7 | * 8 | * @namespace 9 | * @param {object} options - options for logger 10 | * @param {string | function | object} options.level - console[level] 11 | * @param {boolean} options.duration - print duration of each action? 12 | * @param {boolean} options.timestamp - print timestamp with each action? 13 | * @param {object} options.colors - custom colors 14 | * @param {object} options.logger - implementation of the `console` API 15 | * @param {boolean} options.logErrors - should errors in action execution be caught, logged, and re-thrown? 16 | * @param {boolean} options.collapsed - is group collapsed? 17 | * @param {boolean} options.predicate - condition which resolves logger behavior 18 | * @param {function} options.stateTransformer - transform state before print 19 | * @param {function} options.actionTransformer - transform action before print 20 | * @param {function} options.errorTransformer - transform error before print 21 | * 22 | * @returns {function} logger middleware 23 | */ 24 | function createLogger(options = {}) { 25 | const loggerOptions = Object.assign({}, defaults, options); 26 | 27 | const { 28 | logger, 29 | stateTransformer, 30 | errorTransformer, 31 | predicate, 32 | logErrors, 33 | diffPredicate, 34 | } = loggerOptions; 35 | 36 | // Return if 'console' object is not defined 37 | if (typeof logger === 'undefined') { 38 | return () => next => action => next(action); 39 | } 40 | 41 | // Detect if 'createLogger' was passed directly to 'applyMiddleware'. 42 | if (options.getState && options.dispatch) { 43 | // eslint-disable-next-line no-console 44 | console.error(`[redux-logger] redux-logger not installed. Make sure to pass logger instance as middleware: 45 | // Logger with default options 46 | import { logger } from 'redux-logger' 47 | const store = createStore( 48 | reducer, 49 | applyMiddleware(logger) 50 | ) 51 | // Or you can create your own logger with custom options http://bit.ly/redux-logger-options 52 | import { createLogger } from 'redux-logger' 53 | const logger = createLogger({ 54 | // ...options 55 | }); 56 | const store = createStore( 57 | reducer, 58 | applyMiddleware(logger) 59 | ) 60 | `); 61 | 62 | return () => next => action => next(action); 63 | } 64 | 65 | const logBuffer = []; 66 | 67 | return ({ getState }) => next => (action) => { 68 | // Exit early if predicate function returns 'false' 69 | if (typeof predicate === 'function' && !predicate(getState, action)) { 70 | return next(action); 71 | } 72 | 73 | const logEntry = {}; 74 | 75 | logBuffer.push(logEntry); 76 | 77 | logEntry.started = timer.now(); 78 | logEntry.startedTime = new Date(); 79 | logEntry.prevState = stateTransformer(getState()); 80 | logEntry.action = action; 81 | 82 | let returnedValue; 83 | if (logErrors) { 84 | try { 85 | returnedValue = next(action); 86 | } catch (e) { 87 | logEntry.error = errorTransformer(e); 88 | } 89 | } else { 90 | returnedValue = next(action); 91 | } 92 | 93 | logEntry.took = timer.now() - logEntry.started; 94 | logEntry.nextState = stateTransformer(getState()); 95 | 96 | const diff = loggerOptions.diff && typeof diffPredicate === 'function' 97 | ? diffPredicate(getState, action) 98 | : loggerOptions.diff; 99 | 100 | printBuffer(logBuffer, Object.assign({}, loggerOptions, { diff })); 101 | logBuffer.length = 0; 102 | 103 | if (logEntry.error) throw logEntry.error; 104 | return returnedValue; 105 | }; 106 | } 107 | 108 | // eslint-disable-next-line consistent-return 109 | const defaultLogger = ({ dispatch, getState } = {}) => { 110 | if (typeof dispatch === 'function' || typeof getState === 'function') { 111 | return createLogger()({ dispatch, getState }); 112 | } 113 | // eslint-disable-next-line no-console 114 | console.error(` 115 | [redux-logger v3] BREAKING CHANGE 116 | [redux-logger v3] Since 3.0.0 redux-logger exports by default logger with default settings. 117 | [redux-logger v3] Change 118 | [redux-logger v3] import createLogger from 'redux-logger' 119 | [redux-logger v3] to 120 | [redux-logger v3] import { createLogger } from 'redux-logger' 121 | `); 122 | }; 123 | 124 | export { defaults, createLogger, defaultLogger as logger }; 125 | 126 | export default defaultLogger; 127 | --------------------------------------------------------------------------------