Announcing Serverless Analytics Framework

Statsbot Blog

Building a Serverless Stripe Analytics Dashboard

Serverless Analytics Dashboard with React.js

Author avatarArtyom Keydunov/Analytics/November 08, 2018
Building a Serverless Stripe Analytics Dashboard
Show Original

There are a bunch of solid services to help you get an understanding of your Stripe data. They could show out-of-the-box MRR, ARR, churn rate, and other vital SAAS metrics. The problem, though, with all of these services is customization. When it comes to the need to change how your MRR is calculated or simply redesign the dashboard layout, there is almost never a way to accomplish it within those services.

This tutorial walks through building a serverless Stripe dashboard from scratch with Cube.js. By serverless we mean that you don’t need to have a server to run your dashboard, all you need to do is host your static React app. In this tutorial, we’ll host it on AWS S3, but you can host it on any other static hosting, like Netlify.

The live demo of the final dashboard is available here.

And here you can check out the source code.

In first two sections we’ll set up a warehouse with Stripe data and define metrics. If you want, you can skip to the third section to start building a dashboard and use the demo Cube.js API token –

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpIjozOTkzNX0.KI-SgpN2kKRBquykM9bpXocTbwLCMkXM7IMY1b4vuMM

1. Setting up a Warehouse and Loading Data

You can use your own warehouse, such as Redshift or BigQuery, which are popular choices to run analytics queries. To keep things simple, we’ll use the free warehouse provided by Cube.js Cloud. It can also load data from different services, including Stripe, into your warehouse.

Alternatively, if you want to manage ETL on your own, we would recommend the open-source tool, Singer, or hosted Stitch Data service.

Cube.js Cloud is provided by Statsbot, so the first step is to create a free account here.

Connect your Stripe account.

Once the Stripe account is connected, the warehouse will be automatically generated. It could take up to 15 minutes for data to load into your warehouse, depending on the size of your Stripe account. While it’s loading we can define metrics we need.

2. Defining Metrics

Cube.js uses a concept of Data Schema to define metrics. It acts as an ORM for analytics, translating business definitions, such as MRR, into SQL queries. Among other things it empowers us to build reusable schemas, which can be packaged and distributed. We’re going to use the Stripe Schema package, the source code for which is available here.

The next step is to install the Stripe Schema package. We can do it via the web interface.

In the cloud IDE you can explore how each measure and dimension are defined. To learn more about the Cube.js Schema framework you can check out the docs.

We are almost ready to start working on our dashboard, but one last thing that is needed is to generate a Cube.js API token.

For the purpose of this tutorial, we need a simple global token. In the case that you need to limit access to certain metrics for different users, you can use Cube.js Secret to generate your own tokens. You can learn more about it here

3. Building a Dashboard with React

Now we’re ready to build our dashboard! By now you should have either your own Cube.js API token, or you can use a demo one –

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpIjozOTkzNX0.KI-SgpN2kKRBquykM9bpXocTbwLCMkXM7IMY1b4vuMM

Setting up a New React App

We’ll set everything up using Create React App, which is officially supported by the React team. It packages all the dependencies for React app and makes it easy to get started with a new project. Run the following commands in your terminal:

1
2
3
npx create-react-app stripe-dashboard-example
cd stripe-dashboard-example
yarn start

The last line starts a server on port 3000 and opens your web browser at http://localhost:3000.

We’ll build our UI with Reactstrap, which is a React wrapper for Bootstrap 4. Install Reactstrap and Bootstrap from NPM. Reactstrap does not include Bootstrap CSS, so this needs to be installed separately:

1
yarn add reactstrap bootstrap --save

Import Bootstrap CSS in the src/index.js file before importing './index.css':

1
import 'bootstrap/dist/css/bootstrap.min.css';

Now we are ready to use the Reactstrap components.

The next step is to select and install our visualization library. For this tutorial, we’re going to use Recharts. Cube.js is visualization agnostic, meaning you can use any library you want.

1
yarn add recharts --save

We’ll also use moment and numeral to nicely format dates and numbers.

1
yarn add moment numeral --save

Now that we have dependencies to display data and build a UI kit, we need to add the Cube.js API client to fetch the data from the server.

1
yarn add @cubejs-client/core @cubejs-client/react --save

Creating Your First Chart

Finally, we’re done with dependencies, so let’s go ahead and create our first chart. Replace the contents of src/App.js with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import React, { Component } from 'react';
import { Container, Row, Col } from 'reactstrap';
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import cubejs from '@cubejs-client/core';
import moment from 'moment';
import numeral from 'numeral';
import { QueryRenderer } from '@cubejs-client/react';

const cubejsApi = cubejs(process.env.REACT_APP_CUBEJS_TOKEN);
const currencyFormatter = (item) => numeral(item).format('$0,0')
const dateFormatter = (item) =>moment(item).format('MMM YY')

class App extends Component {
  render() {
    return (
      <Container fluid>
        <Row>
          <Col>
            <QueryRenderer
              query={{
                measures: ['StripeSaaSMetrics.mrr'],
                timeDimensions: [{
                  dimension: 'StripeSaaSMetrics.time',
                  dateRange: ['2016-01-01', '2017-12-31'],
                  granularity: 'month'
                }]
              }}
              cubejsApi={cubejsApi}
              render={({ resultSet }) => {
                if (!resultSet) {
                  return 'Loading...';
                }

                return (
                  <ResponsiveContainer width="100%" height={300}>
                    <AreaChart data={resultSet.chartPivot()}>
                      <XAxis dataKey="category" tickFormatter={dateFormatter} fontSize={12} />
                      <YAxis tickFormatter={currencyFormatter} fontSize={12}/>
                      <Tooltip formatter={currencyFormatter} labelFormatter={dateFormatter} />
                      <Area type="monotone" dataKey="StripeSaaSMetrics.mrr" name="MRR" stroke="rgb(106, 110, 229)" fill="rgba(106, 110, 229, .16)" />
                    </AreaChart>
                  </ResponsiveContainer>
                );
              }}
            />
          </Col>
        </Row>
      </Container>
    );
  }
}

export default App;

We are doing several things here:

  1. Importing all required components.
  2. Using Container, Row, and Column Reactstrap components to set up our layout.
  3. Using Cube.js QueryRenderer to fetch data and Recharts to display it.

Let’s look deeper at how we load data and draw the chart.

The first step is to initialize the Cube.js API client:

1
const cubejsApi = cubejs(process.env.REACT_APP_CUBEJS_TOKEN);

Here we are using the REACT_APP_CUBEJS_TOKEN environment variable. Create React App automatically loads your env variables from .env file if they start with REACT_APP_. Create a .env.development.local file with the following content:

1
REACT_APP_CUBEJS_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpIjozOTkzNX0.KI-SgpN2kKRBquykM9bpXocTbwLCMkXM7IMY1b4vuMM

Next, we are using the QueryRenderer Cube.js Component to load MRR data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<QueryRenderer
  query={{
    measures: ['StripeSaaSMetrics.mrr'],
    timeDimensions: [{
      dimension: 'StripeSaaSMetrics.time',
      dateRange: ['2016-01-01', '2017-12-31'],
      granularity: 'month'
    }]
  }}
  cubejsApi={cubejsApi}
  render={({ resultSet }) => {
    // ....
  }}
/>

The query itself is a plain JavaScript object. In our case, we are requesting one measure: SaaSMetrics.mrr, where SaaSMetrics is a cube name, and mrr is a measure name. To plot MRR over time, we need a time dimension to group by. The timeDimensions property is a shortcut for dimension with a filter. You can learn more here about Cube.js query format.

Lastly, we plot data with Recharts. The render parameter of QueryRenderer is a function of the type ({error, resultSet, isLoading}) => React.Node. The output of this function will be rendered by the QueryRenderer. A resultSet is an object containing data obtained from the query. If this object is not defined, it means that the data is still being fetched. resultSet provides multiple methods for data manipulation, but in our case, we need just the chartPivot method, which returns data in a format expected by Recharts.

We plot MRR data as an area chart inside a responsive container. We also do some tick formatting to format MRR as a currency and to nicely show dates. You can learn more about Rechart’s API on their website.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!resultSet) {
  return 'Loading...';
  }

return (
  <ResponsiveContainer width="100%" height={300}>
     <AreaChart data={resultSet.chartPivot()}>
     <XAxis dataKey="category" tickFormatter={dateFormatter} fontSize={12} />
     <YAxis tickFormatter={currencyFormatter} fontSize={12}/>
     <Tooltip formatter={currencyFormatter} labelFormatter={dateFormatter} />
     <Area type="monotone" dataKey="StripeSaaSMetrics.mrr" name="MRR" stroke="rgb(106, 110, 229)" fill="rgba(106, 110, 229, .16)" />
     </AreaChart>
   </ResponsiveContainer>
);

Restart the dev server and go to http://localhost:3000 and you should be able to see your first chart!

Refactoring and Styling Dashboard and Widgets

We want to add more charts to our dashboard, but before doing that it would make sense to factor out common parts of the chart to avoid code duplication. We’ll also start adding some styles to the dashboard and chart widgets to make them look pretty.

Create the src/Chart.js with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react';
import { QueryRenderer } from '@cubejs-client/react';
import './Chart.css'

const Chart = ({ cubejsApi, title, query, render }) => (
  <div className="Chart">
    <div className="ChartTitle">
      { title }
    </div>
    <div className="ChartBody">
      <QueryRenderer
        query={query}
        cubejsApi={cubejsApi}
        render={({ resultSet }) => {
          if (!resultSet) {
            return "Loading...";
          }

          return render(resultSet);
        }}
      />
    </div>
  </div>
);

export default Chart;

We’ve extracted some common charts’ logic into a component and also gave it some additional structure. We’ll keep component styles in the ‘src/Chart.css’, so create this file with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.Chart {
  background: #fff;
  border-radius: 4px;
  margin-bottom: 10px;
  box-shadow: 0 2px 4px rgba(141,149,166,0.1);
}

.ChartTitle {
  color: #50556C;
  font-size: 17px;
  font-weight: 400;
  padding: 7px 10px 4px;
  border-bottom: 1px solid #e7e7e7;
}

.ChartBody {
  overflow: hidden;
  padding: 5px 10px;
  position: relative;
  height: 300px;
  text-align: center;
}

.ChartBody h1 {
  margin-top: 100px;
}

Let’s also add some global styles to our dashboard. Replace the content of src/index.css with the following:

1
2
3
4
5
body {
  -webkit-font-smoothing: antialiased;
  padding-top: 30px;
  background: #f5f6f7;
}

Now, we need to update src/App.js to use one new Chart component. Replace App component in src/App.js with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class App extends Component {
  render() {
    return (
      <Container fluid>
        <Row>
          <Col>
            <Chart
              cubejsApi={cubejsApi}
              title="MRR Over Time"
              query={{
                measures: ['StripeSaaSMetrics.mrr'],
                timeDimensions: [{
                  dimension: 'StripeSaaSMetrics.time',
                  dateRange: ['2016-01-01', '2017-12-31'],
                  granularity: 'month'
                }]
              }}
              render={(resultSet) => (
                <ResponsiveContainer width="100%" height={300}>
                  <AreaChart data={resultSet.chartPivot()}>
                    <XAxis dataKey="category" tickFormatter={dateFormatter} fontSize={12} />
                    <YAxis tickFormatter={currencyFormatter} fontSize={12}/>
                    <Tooltip formatter={currencyFormatter} labelFormatter={dateFormatter} />
                    <Area type="monotone" dataKey="StripeSaaSMetrics.mrr" name="MRR" stroke="rgb(106, 110, 229)" fill="rgba(106, 110, 229, .16)" />
                  </AreaChart>
                </ResponsiveContainer>
              )}
            />
          </Col>
        </Row>
      </Container>
    );
  }
}

Now, we’re ready to add more widgets to our dashboard: Current MRR, MRR breakout by plans, and the active customers chart.

Adding More Charts

Let’s add the Current MRR widget. It going to be a simple single number, which is usually used to display important KPIs.

Let’s first set the width of the first Col component with the MRR chart to 8.

1
2
3
<Col md="8">
  // MRR over Time chart...
</Col>

And then add our next widget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Col md="4">;
  <Chart
    cubejsApi={cubejsApi}
    title="Current MRR"
    query={{
    measures: ['StripeSaaSMetrics.mrr'],
                timeDimensions: [{
                  dimension: 'StripeSaaSMetrics.time',
                  dateRange: ['today'],
                  granularity: null
     }]
   }}
    render={(resultSet) => (
       <h1 height={300}>
          { numeral(resultSet.chartPivot()[0]['StripeSaaSMetrics.mrr']).format('$0,0.00') }
       </h1>
     )}
   />
</Col>

Here we’re fetching the MRR measure, which is cumulative, and we’re using a rolling date range – today. It will always give us an actual MRR number. We don’t render anything with Recharts, instead we just simply display the single value we received.

Let’s go ahead and add two more charts on the next row – MRR by Plans Breakout and Active Customers – displayed as a pie chart and bar chart respectively. First, we need to import these chart components from Recharts. Replace an old Recharts import in src/App.js with the following:

1
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend, BarChart, Bar } from 'recharts';

Now, add the the next row with two columns in the src/App.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<Row>
  <Col md="6">
    <Chart
      cubejsApi={cubejsApi}
      title="MRR by Plans Breakout"
      query={{
        measures: ["StripeSaaSMetrics.mrr"],
        dimensions: ["StripeSaaSMetrics.plan"]
      }}
      render={resultSet => {
        const colors = ["#7DB3FF", "#49457B", "#FF7C78", "#FED3D0"];
        return (
          <ResponsiveContainer width="100%" height={300}>
            <PieChart>
              <Pie
                data={resultSet.chartPivot()}
                nameKey="category"
                dataKey="StripeSaaSMetrics.mrr"
              >
                {resultSet.chartPivot().map((entry, index) => (
                  <Cell fill={colors[index % colors.length]} />
                ))}
              </Pie>
              <Legend verticalAlign="middle" align="right" layout="vertical" />
              <Tooltip formatter={currencyFormatter} />
            </PieChart>
          </ResponsiveContainer>
        );
      }}
    />
  </Col>
  <Col md="6">
    <Chart
      cubejsApi={cubejsApi}
      title="Active Customers"
      query={{
        measures: ["StripeSaaSMetrics.activeCustomers"],
        timeDimensions: [
          {
            dimension: "StripeSaaSMetrics.time",
            dateRange: ["2016-01-01", "2017-12-30"],
            granularity: "month"
          }
        ]
      }}
      render={resultSet => (
        <ResponsiveContainer width="100%" height={300}>
          <BarChart data={resultSet.chartPivot()}>
            <XAxis
              dataKey="category"
              tickFormatter={dateFormatter}
              fontSize={12}
            />
            <YAxis fontSize={12} />
            <Tooltip labelFormatter={dateFormatter} />
            <Bar
              dataKey="StripeSaaSMetrics.activeCustomers"
              name="Active Customers"
              fill="rgb(106, 110, 229)"
            />
          </BarChart>
        </ResponsiveContainer>
      )}
    />
  </Col>
</Row>

In the first chart we fetch an MRR measure and a plan dimension. In response, we receive a breakdown of MRR by the plan dimension. We don’t specify any date filter, and the response will contain all the data we have in a warehouse. In the render props of this chart we define a colors array, that’s because Recharts doesn’t come with a predefined set of colors. You can customize these colors however you want, and add more, if you have more plans.

The Active Customers chart looks pretty familiar, because there is not much difference from our first MRR chart. We request a new measure here – active users, and plot it over time as a bar chart.

Now we’re ready to deploy our dashboard.

Deploying to S3

We’re going to deploy to AWS S3, so the first step is to create an account if you don’t have one already. Next, you need to create an S3 bucket and configure it to host a static website. There is a good official tutorial by AWS on how to do that – you can find it here.

Once you’ve create your bucket and configured it to host your static website, all we need is to build our project in production env and deploy to the bucket.

First, make sure you have .env.local.production in place with REACT_APP_CUBEJS_TOKEN. If you are deploying with the same Cube.js API key you used in production, you can just copy your .env.local.development

1
cp  .env.local.development .env.local.production

Create React App provides a convenient way to prepare an app for deployment. Simply run the following command in your working directory:

1
yarn run build

This packages all of our assets and places them in the build/ directory.

Now to deploy, simply run the following command; where YOUR_S3_DEPLOY_BUCKET_NAME is the name of the S3 Bucket you created:

1
aws s3 sync build/ s3://YOUR_S3_DEPLOY_BUCKET_NAME

And our dashboard should be live on S3! If you head over to the URL assigned to you, you should see it live.

You may also like: