A while back, when still at Condè Nast International, I prepared a talk titled "Tales after midnight", aka a an anthology of modern horror cautionary stories.

The aim of the talk was looking at some real life tech incidents I was either part of or exposed to. Not to scare the attendees, but to focus on the potential learning opportunity and long term outcome: ultimately something might go wrong, but how fast you can recover and the takeaways will make up for the problem.

CNI had quite a good culture in terms of incident management, starting from a shared no blame culture, necessary to foster trust and openness, down to the high standards to write production readiness checklists and run books.

As part of this, the Post Mortem – or Incident Retro – meeting played a key role in both defining the tone of the conversation but also creating a true goldmine of shared knowledge. One of the most important aspects of that meeting, if not they key one is the root cause analysis.

In CNI this was done with a practice called 5-whys.

The 5-whys is a technique developed within Toyota:

Having no problems is the biggest problem of all.
Taiichi Ohno saw a problem not as a negative, but, in fact, as ‘a kaizen (continuous improvement) opportunity in disguise.’ Whenever one cropped up, he encouraged his staff to explore problems first-hand until the root causes were found. ‘Observe the production floor without preconceptions,’ he would advise. ‘Ask why five times about every matter.

That's the practice in short: when you encounter a problem, ask why at least 5 times until you understand the root cause.

The problem I had with it, was that often the process was not linear: an anseer could lead to more statements, and hence branch the discourse to multiple threads, as it happens sometimes (often?) the root causes of an incident are a mix of issues.

So I built something.

I found a nice visual representation of the 5-whys on an article

from: https://kanbanize.com/lean-management/improvement/5-whys-analysis-tool

So I took my React/Redux/Styled Components toolbelt and started putting things together.

Just an example of a fairly old incident I saw happening

In itself the app is nothing too complex.

Let's dive in.

The UI

The arrows are taking advantage of a technique presented in a very well written article about css shapes on css-tricks: each item has a before and after pseudo-element with the triangle shape applied.

I tried to make good use of reusable classes and keep the form items as accessible as HTML offers out of the box: clickable things are either buttons or labels, based on context.

And that was it really.

The interesting bit was determining the shape of the data: I didn't want to work with nested items, so I went for a flat array, keeping the threading in check with a parent id property.

Each item in the array would look like this:

{
  content: 'human mistake',
  id: 'kh54a75n' // each id is generated with (+new Date()).toString(36)
  parent: 'kh54abz7'
}

Except for the first item, in which the parent would be null.

This allowed me to deal with the threading in a rather trivial fashion, and applying the correct indentation: I have a Content component connected with the redux store mounted recursively, filtering the array of item, based on the id property received. It might sound complicated, but if you are familiar with Redux connected components, it shouldn't sound particularly complex:

// Content/index.js
import { connect } from 'react-redux';
import Content from './Content';

const mapStateToProps = (state, props) => ({
  // the state content array gets filtered using the props 
  // passed to the connected component
  content: state.content.filter(({ parent }) => parent == props.parent)
});

export default connect(mapStateToProps)(Content);
// Content/Content.js
import SingleItem from './SingleItem';

export default () => {
  // [...]

  // the Content component applies a margin left to every block 
  // and loops over the items, loading each "SingleItem"
  return <Indent>
  {
    content.map((item) => (
      <SingleItem key={item.id} item={item} />
    )
  }
  </Indent>
}
// Content/SingleItem.js
import Content from 'components/Content';

export default () => {
  // the SingleItem renders the item and then the Content component,
  // recursively, passing the current item id as filter
  return <>
    <Body {...props} />
    <Content parent={props.item.id} />
  </>
}

Of course in the final version there's more to it, including redux actions, some extra markup etc, but that's what it boils down to.

The Redux Store & the reducers

I use an helper to work with reducers, because I find the practice of using switch statements less than readable. By using this:

export const createReducers = (reducers = {}) => 
	(state, { type, payload } = {}) => 
	reducers[type] ? reducers[type](state, payload) : state;

I can create a mapped object instead, but let's break it down:

createReducers is a function that takes an argument, defaulted to an empty object (reducers = {}). This function returns another function, with the usual interface of any redux reducer: (state, action).

Once a reducer is invoked, what happens is that my helper checks against the original object if there's a property matching the type of the action, if there's one, it invokes that function, otherwise it returns the state itself, unmodified.

This allows me to write my reducers in this fashion:

import constants from 'store/constants';

const generateID = () => (+new Date()).toString(36).slice(-8);

const reducer = createReducers({
  [constants.ADD]: (state, { item }) => {
    const content = [...state.content];
    content.push({ 
      id: generateID(), 
      content: item, 
      parent: state.active || null 
    });

    return { ...state, content };
  },
  [constants.EDIT]: (state, { id }) => {
  	// save the edited item in a draft property, which will
    // continue being edited until saving
    const draft = state.content.find((item) => item.id === id);
    return { ...state, draft };
  },
  [constants.SAVE]: (state, draft) => {
    // replace the item in the content array, with the edited version 
    // in the draft property, then empty the latter to restore the UI
    const { content } = state;
    const draftIdx = content.findIndex(({ id }) => id === draft.id);
    const newContent = [
      ...content.slice(0, draftIdx),
      draft,
      ...content.slice(draftIdx + 1),
    ];
    return { ...state, content: newContent, draft: {} };
  },
});
This reducer offers the basic functionalities of the 5 whys webapp

Saving the state

Using services such as Netlify, implies being wary of introducing a database, to store user data. There are a lot of good reasons to do it, but ultimately also a ton of good reasons to shy away from introducing such complexity, especially when we are talking about pet projects.

My initial approach was similar to what diagrams.net does – letting you download an XML file, which can be added to your repo or attached to a confluence page, for example – and I added a download/restore set of buttons.

The first one is incredibly straightforward:

export default ({ content }) => {
  const href = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(content))}`;
  return <a download="5whys.json" href={href}>Download</a>
}

The restore a bit more complicated, but ultimately not terribly so

export default ({ restoreData }) => {
  const onChange = useCallback((e) => {
    const file = e.target.files[0];
    const reader = new FileReader();

    reader.onload = (event) => {
      const data = JSON.parse(event.target.result);
      
      // restoreData is an action of my store that overrides the current
      // content with the one in the json. Adding this method to my reducer
      // [constants.RESTORE_DATA]: (state, data) => ({ ...state, content: data }),
      // The button is only available when the page has no new content
      restoreData(data);
    };

    reader.readAsText(file);
  }, [restoreData]);

  return (
    <Fragment>
      <Label htmlFor="file">Upload</Label>
      <input onChange={onChange} type="file" id="file" style={{ display: 'none' }} />
    </Fragment>
  )
};

Everything was nice and sound, it was possible to download the analysis and store in a repo, for example, but no mean to share a link, let alone edit collaboratively.

Introducing Firebase

I thought it would've been more difficult to add a cloud sync to the project.

I love Firebase, so that's immediately they way I looked at, when I thought about live collaboration. I wrote a very dumb adapter

// first check if I landed on a page with already an id
let id = document.location.pathname.substring(1);
if (!uuid.validate(id)) {
  // if not, or not valid, let's set up one
  id = uuid.v4();
}

export const fromDb = (dispatch) => {
  // this is the connector to update the store based on the changes in firebase
  // the reducer associated is slighthly more complex than the one restoring
  // the local JSON, just because it has to avoid leaving the other browser
  // with a null parent id on the form, but other than that is pretty similar
  db.ref(`/${env}/${id}`).on('value', (data) => {
    if (data.val()) {
      dispatch({
        type: constants.SYNC,
        payload: data.val(),
      });
    }
  });
};

export const toDb = (state) => {
  // this is the connector from redux to the DB
  // I only activate url and sync if the user wants, to avoid pointless
  // data consumption (for the user and on my firebase account)
  if (state.data.cloud) {
    window.history.replaceState(null, document.title, `/${id}`);

    const { content } = state.data;
    db.ref(`/${env}/${id}`).set({ content });
  }
};

What's left is only a React hook and a bit of UI tweaks:

import useCopy from '@react-hook/copy';

// the onCloudEnabled prop is the action creator which turns the cloud property
// on the store data to true, activating effectively the sync
export default ({ cloud, onCloudEnabled}) => {
  const { copied, copy, reset } = useCopy(document.location.href);
  
  // yes, this double ternary is disgusting
  const text = !cloud ? 'Enable cloud sync' : (!copied ? 'Cloud sync enabled, click to copy link' : 'Link copied');
  
  return <EnableCloud 
    onMouseOut={reset}
    onClick={!cloud ? onCloudEnabled : copy} 
  >{text}</EnableCloud>
}

The Code

Colours: https://github.com/cedmax/5-whys