DEV Community

Cover image for Build your own web analytics dashboard with Node.js
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Build your own web analytics dashboard with Node.js

Written by Jon Corbin✏️

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

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.

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

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

Google APIs New Project Page

Google APIs New Project Name

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

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

Enabling The Google Analytics API

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

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

Google APIs Create Credentials Dropdown

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

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

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

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

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

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

Your Google APIs should be ready to go now!

LogRocket Free Trial Banner

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:

$ yarn add cors dotenv express googleapis
Enter fullscreen mode Exit fullscreen mode

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

$ yarn add -D concurrently
Enter fullscreen mode Exit fullscreen mode

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

"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"
  },
Enter fullscreen mode Exit fullscreen mode

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:

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"
Enter fullscreen mode Exit fullscreen mode

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:

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'));
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

// 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'];
Enter fullscreen mode Exit fullscreen mode

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 \\ns 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:

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
Enter fullscreen mode Exit fullscreen mode

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:

// 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,
});
Enter fullscreen mode Exit fullscreen mode

Here we just require google to create analytics and jwt. We also pull out the viewId from process.env. We created a JWT 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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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

await setTimeout[Object.getOwnPropertySymbols(setTimeout)[0]](
    Math.trunc(1000 * Math.random()),
  );
Enter fullscreen mode Exit fullscreen mode

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

module.exports = { getData };
Enter fullscreen mode Exit fullscreen mode

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:

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');
    });
});
Enter fullscreen mode Exit fullscreen mode

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.

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');
    });
});
Enter fullscreen mode Exit fullscreen mode

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 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 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:

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

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

├── 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
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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:

import { createMuiTheme } from '@material-ui/core';
import palette from './palette';
const theme = createMuiTheme({
  palette,
});
export default theme;
Enter fullscreen mode Exit fullscreen mode

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

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',
  },
};
Enter fullscreen mode Exit fullscreen mode

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:

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);
Enter fullscreen mode Exit fullscreen mode

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:

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);
Enter fullscreen mode Exit fullscreen mode

Here we add a few Dashboard Items as examples with different metrics. These metrics are from the Google API’s Metrics & Dimensions Explore. We also need to create a utils.js file containing this:

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

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:

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);
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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:

const io = require('socket.io').listen(server);
Enter fullscreen mode Exit fullscreen mode

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

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 });
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

const socket = io.connect();
socket.on('connect', function() {
  socket.send(window.location);
});
Enter fullscreen mode Exit fullscreen mode

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

<script src="/socket.io/socket.io.js"></script>
<script src="realTimeScripts.js"></script>
Enter fullscreen mode Exit fullscreen mode

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:

    //...
    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;
    }
    //...
Enter fullscreen mode Exit fullscreen mode

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:

    //...
      component = <TextItem data={data} />;
    } else if (data === 'Real Time') {
      component = <RealTimeItem />
    } else {
      switch (visual) {
    //...
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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

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.


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket 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.


The post Build your own web analytics dashboard with Node.js appeared first on LogRocket Blog.

Top comments (1)

Collapse
 
fsjsd profile image
Chris Webb

Beast of an article! Some great stuff in here.

With this bit at the end -

export const RealTimeItem = () => {
  const [connections, setConnections] = useState(0);
  getConnections((conns) => {
    console.log(conns);
    setConnections(conns);
  });
  return (
    <p>
      {connections}
    </p>
  );
};

If I've read it correctly, I'd suggest wrapping that getConnections call in a useEffect hook (with empty dependencies array) otherwise it will re run every time React goes through a render cycle and resubscribe to the socket event each time.

so -

export const RealTimeItem = () => {
  const [connections, setConnections] = useState(0);

  useEffect(() => {
    getConnections((conns) => {
      console.log(conns);
      setConnections(conns);
    });

    return () => { 
      // unsubscribe from socket here
    };
  }, []);

  return (
    <p>
      {connections}
    </p>
  );
};