Build your own web analytics dashboard with Node.js
Tags: node, react, javascript, webdev
Published: 2019-10-07 14:54:41

---
title: Build your own web analytics dashboard with Node.js
published: true
tags: node,react,javascript,webdev
canonical_url: https://blog.logrocket.com/web-analytics-with-node-js/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/eyb134i32txec9z9m35n.jpeg
---
**Written by [Jon Corbin](https://blog.logrocket.com/author/joncorbin/)**✏️

If you have ever used Google Analytics, you know it isn’t the prettiest interface to use. It gets the job done, sure, but I’m not a huge fan of how it looks, nor the color palette. I mean, look at this:

![Google Analytics Home Dashboard](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-analytics-home-dashboard.jpeg?resize=730%2C329&ssl=1)

It’s just so boring and bland — I need more color in my life than this. I also want some more customization from Google Analytics that it just doesn’t provide. Luckily, we’re software developers, so we can build our own version of Google Analytics to our standards!

## Google APIs

Lucky for us, Google provides a slew of different APIs for us to use in our projects. We just need to set this up in our [Google Developer account](https://console.developers.google.com/apis/).

### Create a new project

First we’ll need to create a new project by clicking on the projects selection in the top left:

![Google APIs My Project Dropdown](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-apis-my-project.jpeg?resize=307%2C58&ssl=1)

Then create a new project and name it whatever you’d like.

![Google APIs New Project Page](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-apis-new-project.jpeg?resize=730%2C194&ssl=1)

![Google APIs New Project Name](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-apis-project-name.jpeg?resize=536%2C235&ssl=1)

### Add Google Analytics API

Once we create our project, we need to add some services so that we can use the Google Analytics API. To do this, we’ll click the **Enable APIs and Services** at the top of the page.

![Enable APIs And Services Option](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/enable-apis-and-services.jpeg?resize=410%2C56&ssl=1)

Once at the **APIs & Services** page, we’re going to search for “google analytics api” to add that to our project. _Do not add the Google Analytics Reporting API_. This is not the API we want.

![Google Analytics API in Search Results](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-analytics-api.jpeg?resize=730%2C269&ssl=1)

![Enabling The Google Analytics API](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/enable-google-analytics-api.jpeg?resize=687%2C220&ssl=1)

### Create a service account

After we add the Analytics API, we need to create a service account so that our app can access the API. To do this, let’s head over to the credentials section from the console homescreen.

![Google APIs Credentials Section](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-apis-credentials.jpeg?resize=200%2C382&ssl=1)

Once there, click on the **Create Credentials**  dropdown and select **Service Account Key**.

![Google APIs Create Credentials Dropdown](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/create-credentials-service-account-key.jpeg?resize=589%2C535&ssl=1)

Now set the options you see to the following (apart from **Service account name** — you can name that whatever you’d like).

![Google APIs Service Account Info](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/service-account-options.jpeg?resize=509%2C440&ssl=1)

Once you click **Create** , a JSON file will be generated. Save this in a known location, as we’ll need part of the contents.

![Generated JSON Key](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/json-file.jpeg?resize=544%2C634&ssl=1)

In that JSON file, find the client email and copy it. Then head over to Google Analytics and add a new user to your view. Do this by first clicking on the gear in the lower left-hand corner, then go to **User Management** in the view section.

![Google Analytics User Management](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-analytics-user-management.jpeg?resize=730%2C457&ssl=1)

Here, add a new user by clicking the big blue plus in the upper right-hand corner and selecting **Add users**.

![Google Analytics Add Users Option](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-analytics-add-users.jpeg?resize=234%2C233&ssl=1)

Paste in the client email from your JSON file, and make sure **Read & Analyze** is checked off in permissions. These are the only permissions we want to give this account.

![Google Analytics User Permissions Options](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-analytics-user-permissions.jpeg?resize=730%2C351&ssl=1)

Finally, we want to get the view ID for later. From your admin settings, go to view settings and copy the **View ID** for later (better yet, just keep this in a separate open tab).

![Google Analytics View Settings](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/google-analytics-view-id.jpeg?resize=730%2C212&ssl=1)

Your Google APIs should be ready to go now!

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Back end

For our back end, we will be using Node.js. Let’s get started by setting up our project! For this I will be using `yarn` as my package manager, but `npm` should work fine as well.

### Setup

First, let’s run `yarn init` to get our structure started. Enter the name, description, and such that you like. Yarn will set our entry point as `server.js` rather than `index.js`, so this is what that will refer to from here on. Now let’s add our dependencies:

```jsx
$ yarn add cors dotenv express googleapis
```

We will also want to add `concurrently` and `jest` to our dev dependencies since we will be using this in our scripts.

```jsx
$ yarn add -D concurrently
```

Speaking of which, let’s set those up now. In our `package.json`, we’ll want to set our scripts to be:

```jsx
"scripts": {
    "test_server": "jest ./ --passWithNoTests",
    "test_client": "cd client && yarn test",
    "test": "concurrently \"yarn test_server\" \"yarn test_client\"",
    "start": "concurrently \"npm run server\" \"npm run client\"",
    "server": "node server.js",
    "client": "cd client && npm start",
    "build": "cd client && yarn build"
  },
```

Finally, we will want to create a `.env` file to store our secrets and some configuration. Here’s what we’ll want to add to it:

```jsx
CLIENT_EMAIL="This is the email in your json file from google"
PRIVATE_KEY="This is also in the json file"
VIEW_ID="The view id from google analytics you copied down earlier"
SERVER_PORT=3001 // or whatever port you'd like
NODE_ENV="dev"
```

Great — now we’re basically ready to start developing our server. If you want, you can add `eslint` to your dependencies now before getting started (which I would recommend).

### Server

Let’s get started on this server file now, shall we? First, let’s create it with `touch server.js`. Now open that up in your favorite editor. At the top of this, we’ll want to define some things:

```jsx
require('dotenv').config();

// Server
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
const server = require('http').createServer(app);

// Config
const port = process.env.SERVER_PORT;
if (process.env.NODE_ENV === 'production') {
  app.use(express.static('client/build'));
}
```

Here we’re going to load in our `.env` by using `require('dotenv').config()`, which handles the hard work for us. This loads all our variables into `process.env` for later use.

Next, we define our server, for which we use `express`. We add `cors` to our Express app so we can access it from our front end later. Then, we wrap our app in `require('http').createServer` so that we can add some fun stuff with Socket.IO later on.

Finally, we do some configuration by setting a global constant `port` to shorthand this later and change our `static` path based on our `NODE_ENV` variable.

Now let’s make our server listen to our port by adding this to the bottom of our `server.js` file:

```jsx
server.listen(port, () => {
  console.log(`Server running at localhost:${port}`);
});
```

Awesome! That’s all we can really do for our server until we develop our Google APIs library.

### Analytics library

Back at our terminal, let’s create a new directory called `libraries/` using `mkdir libraries` and create our analytics handler. I will call this `gAnalytics.js`, which we can create using `touch libraries/gAnalytics.js` and then switching back to the editor.

In `gAnalytics.js`, let’s define some configuration:

```jsx
// Config
const clientEmail = process.env.CLIENT_EMAIL;
const privateKey = process.env.PRIVATE_KEY.replace(new RegExp('\\\\n'), '\n');
const scopes = ['https://www.googleapis.com/auth/analytics.readonly'];
```

We need to pull in our client email and private key (which were pulled from the JSON credential file provided by Google API Console) from the `process.env`, and we need to replace any `\\n`s in our private key (which is how `dotenv` will read it in) and replace them with `\n`. Finally, we define some scopes for Google APIs. There are quite a few different options here, such as:

```jsx
https://www.googleapis.com/auth/analytics to view and manage the data
https://www.googleapis.com/auth/analytics.edit to edit the management entities
https://www.googleapis.com/auth/analytics.manage.users to manage the account users and permissions
```

And quite a few more, but we only want read-only so that we don’t expose too much with our application.

Now let’s set up Google Analytics by using those variables:

```jsx
// API's
const { google } = require('googleapis');
const analytics = google.analytics('v3');
const viewId = process.env.VIEW_ID;
const jwt = new google.auth.JWT({
  email: clientEmail,
  key: privateKey,
  scopes,
});
```

Here we just require `google` to create `analytics` and `jwt`. We also pull out the `viewId` from `process.env`. We created a [JWT](https://blog.logrocket.com/how-to-secure-a-rest-api-using-jwt-7efd83e71432/) here to authorize ourselves later on when we need some data. Now we need to create some functions to actually retrieve the data. First we’ll create the fetching function:

```jsx
async function getMetric(metric, startDate, endDate) {
  await setTimeout[Object.getOwnPropertySymbols(setTimeout)[0]](
    Math.trunc(1000 * Math.random()),
  ); // 3 sec
  const result = await analytics.data.ga.get({
    auth: jwt,
    ids: `ga:${viewId}`,
    'start-date': startDate,
    'end-date': endDate,
    metrics: metric,
  });
  const res = {};
  res[metric] = {
    value: parseInt(result.data.totalsForAllResults[metric], 10),
    start: startDate,
    end: endDate,
  };
  return res;
}
```

There’s a bit to this one, so let’s break it down. First, we make this async so that we can fetch many metrics at once. There’s a quote imposed by Google, however, so we need to add a random wait to it using

```jsx
await setTimeout[Object.getOwnPropertySymbols(setTimeout)[0]](
    Math.trunc(1000 * Math.random()),
  );
```

This would very likely introduce scalability issues if you have many users trying to load data, but I’m just one person, so it works for my needs.

Next, we fetch the data using `analytics.data.ga.get`, which will return a rather large object with a ton of data. We don’t need all of it, so we just take out the important bit: `result.data.totalsForAlResults[metric]`. This is a string, so we convert it to an int and return it in an object with our start and end dates.

Next, let’s add a way of batch-getting metrics:

```jsx
function parseMetric(metric) {
  let cleanMetric = metric;
  if (!cleanMetric.startsWith('ga:')) {
    cleanMetric = `ga:${cleanMetric}`;
  }
  return cleanMetric;
}
function getData(metrics = ['ga:users'], startDate = '30daysAgo', endDate = 'today') {
  // ensure all metrics have ga:
  const results = [];
  for (let i = 0; i < metrics.length; i += 1) {
    const metric = parseMetric(metrics[i]);
    results.push(getMetric(metric, startDate, endDate));
  }
  return results;
}
```

This will make it easy for us to request a bunch of metrics all at once. This just returns a list of `getMetric` promises. We also add in a way to clean up the metric names passed to the function using `parseMetric`, which just adds `ga:` to the front of the metric if it isn’t there already.

Finally, export `getData` at the bottom and our library is good to go.

```jsx
module.exports = { getData };
```

### Tying it all in

Now let’s combine our library and server by adding some routes. In `server.js`, we’ll add the following path:

```jsx
app.get('/api', (req, res) => {
  const { metrics, startDate, endDate } = req.query;
  console.log(`Requested metrics: ${metrics}`);
  console.log(`Requested start-date: ${startDate}`);
  console.log(`Requested end-date: ${endDate}`);
  Promise.all(getData(metrics ? metrics.split(',') : metrics, startDate, endDate))
    .then((data) => {
      // flatten list of objects into one object
      const body = {};
      Object.values(data).forEach((value) => {
        Object.keys(value).forEach((key) => {
          body[key] = value[key];
        });
      });
      res.send({ data: body });
      console.log('Done');
    })
    .catch((err) => {
      console.log('Error:');
      console.log(err);
      res.send({ status: 'Error getting a metric', message: `${err}` });
      console.log('Done');
    });
});
```

This path allows our client to request a list of metrics (or just one metric) and then return all the data once it’s retrieved, as we can see by `Promise.all`. This will wait until all promises in the given list are completed or until one fails.

We can then add a `.then` that takes a `data` param. This `data` param is a list of data objects that we created in `gAnalytics.getData`, so we iterate through all the objects and combine them into a body object. This object is what will be sent back to our client in the form `res.send({data: body});`.

We’ll also add a `.catch` to our `Promise.all`, which will send back an error message and log the error.

Now let’s add the `api/graph/` path, which will be used for… well, graphing. This will be very similar to our `/api` path but with it’s own nuances.

```jsx
app.get('/api/graph', (req, res) => {
  const { metric } = req.query;
  console.log(`Requested graph of metric: ${metric}`);
  // 1 week time frame
  let promises = [];
  for (let i = 7; i >= 0; i -= 1) {
    promises.push(getData([metric], `${i}daysAgo`, `${i}daysAgo`));
  }
  promises = [].concat(...promises);
  Promise.all(promises)
    .then((data) => {
      // flatten list of objects into one object
      const body = {};
      body[metric] = [];
      Object.values(data).forEach((value) => {
        body[metric].push(value[metric.startsWith('ga:') ? metric : `ga:${metric}`]);
      });
      console.log(body);
      res.send({ data: body });
      console.log('Done');
    })
    .catch((err) => {
      console.log('Error:');
      console.log(err);
      res.send({ status: 'Error', message: `${err}` });
      console.log('Done');
    });
});
```

As you can see, we still rely on `gAnalytics.getData` and `Promise.all`, but instead, we get the data for the last seven days and smash that all into one list to send back in the body.

That’s it for our server now. Easy peasy, wouldn’t you say? Now for the real beast, the front end.

## Front end

Front ends are a ton of fun but can be quite a challenge to develop and design. Let’s give it a shot, though! For our front end, we will be using the [React](https://reactjs.org/) framework in all its glory. I recommend getting up, going for a walk, maybe getting a glass of water before we get started.

You didn’t do any of those things, did you? Alright, fine, let’s get started.

### Setup and structure

First, we need to create our boilerplate. We’re going to use the [create-react-app](https://github.com/facebook/create-react-app) boilerplate as it’s always a great starting point. So, run `create-react-app client` and let it do it’s thing. Once finished, we’ll install some dependencies that we’ll need. Make sure you `cd` into the `client/` folder and then run `$ yarn add @material-ui/core prop-types recharts`.

Again, set up eslint here if you’d like it. Next we’ll clean up `src/App.js` before moving on to the structure. Open up `src/App.js` and remove everything so that the only thing left is:

```jsx
import React from 'react';
import './App.css';
function App() {
  return (
    <div className="App">
    </div>
  );
}
export default App;
```

We also want to delete `serviceWorker.js` and remove it from `src/index.js`.

For structure, we’re just going to set up everything right away and develop afterwards. Here’s how our `src` folder is going to look (which will make sense later):

```jsx
├── App.css
├── App.js
├── App.test.js
├── components
│   ├── Dashboard
│   │   ├── DashboardItem
│   │   │   ├── DashboardItem.js
│   │   │   └── DataItems
│   │   │       ├── index.js
│   │   │       ├── ChartItem
│   │   │       │   └── ChartItem.js
│   │   │       └── TextItem
│   │   │           └── TextItem.js
│   │   └── Dashboard.js
│   └── Header
│       └── Header.js
├── index.css
├── index.js
├── theme
│   ├── index.js
│   └── palette.js
└── utils.js
```

Create all of those files and folders, as we will be editing them to build our app. From here, every file reference is relative to the `src/` folder.

## Components

### `App` and `theme`

Let’s start back at `App`. We need to edit this to look like the below:

```jsx
import React from 'react';
import './App.css';
import Dashboard from './components/Dashboard/Dashboard';
import { ThemeProvider } from '@material-ui/styles';
import theme from './theme';
import Header from './components/Header/Header';
function App() {
  return (
    <ThemeProvider theme={theme}>
      <div className="App">
        <Header text={"Analytics Dashboard"}/>
        <br/>
        <Dashboard />
      </div>
    </ThemeProvider>
  );
}
export default App;
```

This will pull in the necessary components and create our theme provider. Next, let’s edit that theme. Open up `theme/index.js` and add the following:

```jsx
import { createMuiTheme } from '@material-ui/core';
import palette from './palette';
const theme = createMuiTheme({
  palette,
});
export default theme;
```

Next open up `theme/palette.js` and add the following:

```jsx
import { colors } from '@material-ui/core';
const white = '#FFFFFF';
const black = '#000000';
export default {
  black,
  white,
  primary: {
    contrastText: white,
    dark: colors.indigo[900],
    main: colors.indigo[500],
    light: colors.indigo[100]
  },
  secondary: {
    contrastText: white,
    dark: colors.blue[900],
    main: colors.blue['A400'],
    light: colors.blue['A400']
  },
  text: {
    primary: colors.blueGrey[900],
    secondary: colors.blueGrey[600],
    link: colors.blue[600]
  },
  background: {
    primary: '#f2e1b7',
    secondary: '#ffb3b1',
    tertiary: '#9ac48d',
    quaternary: '#fdae03',
    quinary: '#e7140d',
  },
};
```

The above will all let us use theme within our components for different styling options. We also define our theme colors, which you can change to your heart’s content. I liked the pastel-like feel of these.

### `Header`

Next, let’s create our header. Open up `components/Header/header.js` and add in this:

```jsx
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import AppBar from '@material-ui/core/AppBar';
const styles = (theme) => ({
  header: {
    padding: theme.spacing(3),
    textAlign: 'center',
    color: theme.palette.text.primary,
    background: theme.palette.background.primary,
  },
});
export const Header = (props) => {
  const { classes, text } = props;
  return (
    <AppBar position="static">
      <Paper className={classes.header}>{text}</Paper>
    </AppBar>
  );
};
Header.propTypes = {
  classes: PropTypes.object.isRequired,
  text: PropTypes.string.isRequired,
};
export default withStyles(styles)(Header);
```

This will create a horizontal bar at the top of our page, with the text being whatever we set the prop to. It also pulls in our styling and uses that to make it look oh so good.

### `Dashboard`

Moving on, let’s now work on `components/Dashboard/Dashboard.js`. This is a much simpler component and looks like this:

```jsx
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import DashboardItem from './DashboardItem/DashboardItem';
import { isMobile } from '../../utils';
const styles = () => ({
  root: {
    flexGrow: 1,
    overflow: 'hidden',
  },
});
const Dashboard = (props) => {
  const { classes } = props;
  return (
    <div className={classes.root}>
      <Grid container direction={isMobile ? 'column' : 'row'} spacing={3} justify="center" alignItems="center">
        <DashboardItem size={9} priority="primary" metric="Users" visual="chart" type="line" />
        <DashboardItem size={3} priority="secondary" metric="Sessions"/>
        <DashboardItem size={3} priority="primary" metric="Page Views"/>
        <DashboardItem size={9} metric="Total Events" visual="chart" type="line"/>
      </Grid>
    </div>
  );
};
Dashboard.propTypes = {
  classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Dashboard);
```

Here we add a few `Dashboard Item`s as examples with different metrics. These metrics are from the [Google API’s Metrics & Dimensions Explore](https://developers.google.com/analytics/devguides/reporting/core/v4/). We also need to create a `utils.js` file containing this:

```jsx
export function numberWithCommas(x) {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
export const isMobile = window.innerWidth <= 500;
```

This will tell us if the user is on mobile or not. We want a responsive app, so we need to know whether the user is on mobile. Alright, let’s move on.

### `DashboardItem`

Next up, we have the `DashboardItem`, which we will edit `Dashboard/DashboardItem/DashboardItem.js` to create. Add this to that file:

```jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import { TextItem, ChartItem, RealTimeItem } from './DataItems';
import { numberWithCommas, isMobile } from '../../../utils';
const styles = (theme) => ({
  paper: {
    marginLeft: theme.spacing(1),
    marginRight: theme.spacing(1),
    paddingTop: theme.spacing(10),
    textAlign: 'center',
    color: theme.palette.text.primary,
    height: 200,
    minWidth: 300,
  },
  chartItem: {
    paddingTop: theme.spacing(1),
    height: 272,
  },
  mainMetric: {
    background: theme.palette.background.quaternary,
  },
  secondaryMetric: {
    background: theme.palette.background.secondary,
  },
  defaultMetric: {
    background: theme.palette.background.tertiary,
  },
});
class DashboardItem extends Component {
  constructor(props) {
    super(props);
    const {
      classes,
      size,
      metric,
      priority,
      visual,
      type,
    } = this.props;
    this.state = {
      classNames: classes,
      size,
      metric,
      priority,
      visual,
      type,
      data: 'No data',
    };
  }
  componentDidMount() {
    this.getMetricData();
    this.getClassNames();
  }
  getMetricData() {
    const { visual, metric } = this.state;
    const strippedMetric = metric.replace(' ', '');

    let url;
    if (visual === 'chart') {
      url = `http://localhost:3001/api/graph?metric=${strippedMetric}`;
    } else {
      url = `http://localhost:3001/api?metrics=${strippedMetric}`;
    }
    fetch(url, {
      method: 'GET',
      mode: 'cors',
    })
      .then((res) => (res.json()))
      .then((data) => {
        let value;
        let formattedValue;
        if (visual === 'chart') {
          value = data.data[strippedMetric];
          formattedValue = value;
        } else {
          try {
            value = strippedMetric.startsWith('ga:') ? data.data[strippedMetric] : data.data[`ga:${strippedMetric}`];
            formattedValue = numberWithCommas(parseInt(value.value, 10));
          } catch (exp) {
            console.log(exp);
            formattedValue = "Error Retrieving Value"
          }
        }
        this.setState({ data: formattedValue });
      });
  }
  getClassNames() {
    const { priority, visual } = this.state;
    const { classes } = this.props;
    let classNames = classes.paper;
    switch (priority) {
      case 'primary':
        classNames = `${classNames} ${classes.mainMetric}`;
        break;
      case 'secondary':
        classNames = `${classNames} ${classes.secondaryMetric}`;
        break;
      default:
        classNames = `${classNames} ${classes.defaultMetric}`;
        break;
    }
    if (visual === 'chart') {
      classNames = `${classNames} ${classes.chartItem}`;
    }
    this.setState({ classNames });
  }
  getVisualComponent() {
    const { data, visual, type } = this.state;
    let component;
    if (data === 'No data') {
      component = <TextItem data={data} />;
    } else {
      switch (visual) {
        case 'chart':
          component = <ChartItem data={data} xKey='start' valKey='value' type={type} />;
          break;
        default:
          component = <TextItem data={data} />;
          break;
      }
    }
    return component;
  }
  render() {
    const {
      classNames,
      metric,
      size,
    } = this.state;
    const visualComponent = this.getVisualComponent();
    return (
      <Grid item xs={(isMobile || !size) ? 'auto' : size} zeroMinWidth>
        <Paper className={`${classNames}`}>
          <h2>{ metric }</h2>
          {visualComponent}
        </Paper>
      </Grid>
    );
  }
}
DashboardItem.propTypes = {
  size: PropTypes.number,
  priority: PropTypes.string,
  visual: PropTypes.string,
  type: PropTypes.string,
  classes: PropTypes.object.isRequired,
  metric: PropTypes.string.isRequired,
};
DashboardItem.defaultProps = {
  size: null,
  priority: null,
  visual: 'text',
  type: null,
};
export default withStyles(styles)(DashboardItem);
```

This component is pretty massive, but it’s the bread and butter of our application. To sum it up in a few sentences, this component is how we can have a highly customizable interface. With this component, depending on the props passed, we can change the size, color, and type of visual. The `DashboardItem` component also fetches the data for itself and then passes it to its visual component.

We do have to create those visual components, though, so let’s do that.

### Visual components (`DataItems`)

We need to create both the `ChartItem` and `TextItem` for our `DashboardItem` to render properly. Open up `components/Dashboard/DashboardItem/DataItems/TextItem/TextItem.js` and add the following to it:

```jsx
import React from 'react';
import PropTypes from 'prop-types';

export const TextItem = (props) => {
  const { data } = props;
  let view;
  if (data === 'No data') {
    view = data;
  } else {
    view = `${data} over the past 30 days`
  }
  return (
    <p>
      {view}
    </p>
  );
};
TextItem.propTypes = {
  data: PropTypes.string.isRequired,
};
export default TextItem;
```

This one is super simple — it basically displays the text passed to it as the `data` prop. Now let’s do the `ChartItem` by opening up `components/Dashboard/DashboardItem/DataItems/ChartItem/ChartItem.js` and adding this into it:

```jsx
import React from 'react';
import PropTypes from 'prop-types';
import {
  ResponsiveContainer, LineChart, XAxis, YAxis, CartesianGrid, Line, Tooltip,
} from 'recharts';
export const ChartItem = (props) => {
  const { data, xKey, valKey } = props;
  return (
    <ResponsiveContainer height="75%" width="90%">
      <LineChart data={data}>
        <XAxis dataKey={xKey} />
        <YAxis type="number" domain={[0, 'dataMax + 100']} />
        <Tooltip />
        <CartesianGrid stroke="#eee" strokeDasharray="5 5" />
        <Line type="monotone" dataKey={valKey} stroke="#8884d8" />
      </LineChart>
    </ResponsiveContainer>
  );
};
ChartItem.propTypes = {
  data: PropTypes.array.isRequired,
  xKey: PropTypes.string,
  valKey: PropTypes.string,
};
ChartItem.defaultProps = {
  xKey: 'end',
  valKey: 'value',
};
export default ChartItem;
```

This will do exactly what it sounds like it does: render a chart. This uses that `api/graph/` route we added to our server.

### Finished!

At this point, you should be good to go with what we have! All you need to do is run `yarn start` from the topmost directory, and everything should boot up just fine.

## Real time

One of the best parts of Google Analytics is the ability to see who is using your site in real time. We can do that, too! Sadly, Google APIs has the Realtime API as a closed beta, but again, we’re software developers! Let’s make our own.

### Back end

#### Adding Socket.IO

We’re going to use Socket.IO for this since it allows for real-time communications between machines. First, add Socket.IO to your dependencies with `yarn add socket.io`. Now, open up your `server.js` file and add the following to the top of it:

```jsx
const io = require('socket.io').listen(server);
```

You can add this just below the `server` definition. And at the bottom, but above the `server.listen`, add the following:

```jsx
io.sockets.on('connection', (socket) => {
  socket.on('message', (message) => {
    console.log('Received message:');
    console.log(message);
    console.log(Object.keys(io.sockets.connected).length);
    io.sockets.emit('pageview', { connections: Object.keys(io.sockets.connected).length - 1 });
  });
});
```

This will allow our server to listen for sockets connecting to it and sending it a message. When it receives a message, it will then emit a `'pageview'` event to all the sockets (this probably isn’t the safest thing to do, but we’re only sending out the number of connections, so it’s nothing important).

#### Create public script

To have our clients send our server a message, they need a script! Let’s create a script in `client/public` called `realTimeScripts.js`, which will contain:

```jsx
const socket = io.connect();
socket.on('connect', function() {
  socket.send(window.location);
});
```

Now we just need to reference these two scripts in any of our webpages, and the connection will be tracked.

```jsx
<script src="/socket.io/socket.io.js"></script>
<script src="realTimeScripts.js"></script>
```

The `/socket.io/socket.io.js` is handled by the installation of `socket.io`, so there is no need to create this.

### Front end

#### Create a new component

To view these connections, we need a new component. Let’s first edit `DashboardItem.js` by adding the following to `getMetricData`:

```jsx
    //...
    const strippedMetric = metric.replace(' ', '');
    // Do not need to retrieve metric data if metric is real time, handled in component
    if (metric.toUpperCase() === "REAL TIME") {
      this.setState({ data: "Real Time" })
      return;
    }
    //...
```

This will set our state and return us out of the `getMetricData` function since we don’t need to fetch anything. Next, let’s add the following to `getVisualComponent`:

```jsx
    //...
      component = <TextItem data={data} />;
    } else if (data === 'Real Time') {
      component = <RealTimeItem />
    } else {
      switch (visual) {
    //...
```

Now our visual component will be set to our `RealTimeItem` when the `metric` prop is `"Real Time"`.

Now we need to create the `RealTimeItem` component. Create the following path and file: `Dashboard/DashboardItem/DataItems/RealTimeItem/RealTimeItem.js`. Now add the following to it:

```jsx
import React, { useState } from 'react';
import openSocket from 'socket.io-client';
const socket = openSocket('http://localhost:3001');
const getConnections = (cb) => {
  socket.on('pageview', (connections) => cb(connections.connections))
}
export const RealTimeItem = () => {
  const [connections, setConnections] = useState(0);
  getConnections((conns) => {
    console.log(conns);
    setConnections(conns);
  });
  return (
    <p>
      {connections}
    </p>
  );
};

export default RealTimeItem;
```

This will add a real-time card to our dashboard.

## And we’re finished!

You should now have a fully functional dashboard that looks like this:

![Completed Custom Analytics Dashboard](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/finished-custom-analytics-dashboard.jpeg?resize=730%2C310&ssl=1)

This is meant to be a highly extendable dashboard where you can add new data items in a similar way to how we added the real-time item. I will continue to develop this out further, as I have thought of several other things I want to do with this, including an **Add Card** button, changing sizes, different chart types, adding dimensions, and more! If you would like me to continue writing about this dashboard, let me know! Finally, if you would like to see the source code, you can find the [repo here](https://github.com/Jrc356/analytics-dashboard).
* * * 
**Editor's note:** Seeing something wrong with this post? You can find the correct version [here](https://blog.logrocket.com/web-analytics-with-node-js/).

## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend 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 the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *
The post [Build your own web analytics dashboard with Node.js](https://blog.logrocket.com/web-analytics-with-node-js/) appeared first on [LogRocket Blog](https://blog.logrocket.com).