DEV Community

Joseph Sutton
Joseph Sutton

Posted on • Edited on

Better CircleCI Insights Visualization with Recharts

CircleCI introduced insights some time ago, but I wanted a timeline-like graph of all successful deployments and builds. Since the only way to graph builds is to view each branch, we'll need to do some querying and organizing. I've never used Recharts before, but it was relatively simple to use.

TLDR: Here's the GitHub repository.

Frontend

Since we're just wanting to get up and running, we can create a quick React project with npx create-react-app. After that, we'll have a bare React application and we can start looking into how we can gather metrics on our CircleCI builds and deployments.

Getting the Data

First, we need a script to grab both our deployment and build data. Luckily, CircleCI's API endpoints make that easy enough for us. I'll be using an open source project I'm contributing to currently.

Deployment Data

Let's create our first script for grabbing deployment data. We'll call this script get-deployment-data.js and slap it in our root directory.

const axios = require('axios');
const fs = require('fs').promises;

require('dotenv').config();

(async () => {
  const apiKey = process.argv[2] || process.env.CIRCLE_TOKEN;
  const vcsSlug = process.argv[3] || process.env.VCS_SLUG;
  const workflowName = process.argv[4] || process.env.DEPLOYMENT_WORKFLOW_NAME;

  const { data } = await axios.get(`https://circleci.com/api/v2/insights/${vcsSlug}/workflows/${workflowName}`, {
    headers: { 
      'Circle-Token': `${apiKey}`,
      'Access-Control-Allow-Origin': '*',
    },
  });

  data.items = data.items.map(item => {
    const dtCreated = new Date(item.created_at);

    return { 
      ...item,
      name: `${dtCreated.getMonth() + 1}/${dtCreated.getDate()}`,
    }
  });

  data.items.sort((a,b) => {
    const aDate = new Date(a.created_at);
    const bDate = new Date(b.created_at);

    return aDate > bDate || 0;
  })

  const jsonString = JSON.stringify(data.items);

  await fs.writeFile('./src/data/deployment-data.json', Buffer.from(jsonString, 'utf-8'));
})();
Enter fullscreen mode Exit fullscreen mode

After using this script, it'll create a JSON file of the deployment data in the src/data directory:

node ./get-deployment-data.js abc123-def321-abcdef-123321 gh/sutt0n/test-repo deploy
Enter fullscreen mode Exit fullscreen mode

Build Data

This one is a little tricky, because we can't simply query the workflow name, build for some reason. We have to obtain a select history of builds and grab the branch names returned from that. After we have the branch names, we can query the build workflow for a specific branch. Oh, and CircleCI's API has a rate limiter - so that's fun. We can add axios-retry and that should do the trick:

/**
* get-build-data.js
* Usage: node ./get-build-data.js <circle ci token> <vcs slug> <workflow name>
* Example: node ./get-build-data.js abc123-def321-abc-1234 gh/flexion/ef-cms hourly
* Returns: Creates ./data/build-data.json
**/

const axios = require('axios');
const axiosRetry = require('axios-retry');
const fs = require('fs').promises;

axiosRetry(axios, { retryDelay: axiosRetry.exponentialDelay, retries: 10 });

require('dotenv').config();

(async () => {
  const apiKey = process.argv[2] || process.env.CIRCLE_TOKEN;
  const vcsSlug = process.argv[3] || process.env.VCS_SLUG;

  const results = [];

  let offset = 0;

  for(let i = 0; i <= 3; i++) {
    offset += 100 * i;
    let { data } = await axios.get(`https://circleci.com/api/v1.1/project/${vcsSlug}?shallow=true&limit=100&offset=${offset}`, {
      headers: { 
        'Circle-Token': `${apiKey}`,
        'Access-Control-Allow-Origin': '*',
      },
    });

    results.push(...data);
  }

  const branches = results.map(result => {
    if(result.workflows.workflow_name !== 'hourly' && result.lifecycle === 'finished') {
      return result.branch;
    }

    return null;
  }).filter((x,idx) => x && x.indexOf(x) === idx);

  const allResults = [];

  for(const branch of branches) {
    const {data} = await axios.get(`https://circleci.com/api/v2/insights/${vcsSlug}/workflows/build-and-deploy?branch=${branch}`, {
      headers: { 
        'Circle-Token': `${apiKey}`,
        'Access-Control-Allow-Origin': '*',
      },
    });
    allResults.push(...data.items);
  }

  const jsonString = JSON.stringify(allResults);

  await fs.writeFile('./src/data/build-data.json', Buffer.from(jsonString, 'utf-8'));
})();
Enter fullscreen mode Exit fullscreen mode

Charts

I decided the best approach was to graph out our credits and duration (in seconds) using the SimpleLineCart. Here's what our App.jsx looks like:

import './App.css';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import deploymentStats from './data/deployment-data.json'
import buildStats from './data/build-data.json'

function App() {
  return (
    <div className="App">
      <header>CircleCI Insights</header>
      <div className="insights">
        <h2 className="padding-top-0">Deployment Stats</h2>
        <LineChart
          width={1500}
          height={500}
          data={deploymentStats}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis reversed dataKey="name" dy={5} minTickGap={75}/>
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="credits_used" stroke="#8884d8" dy={30} />
          <Line type="monotone" dataKey="duration" stroke="#82ca9d" dy={30} activeDot={{ r: 3 }} />
        </LineChart>
      </div>
      <div className="insights">
        <h2 className="padding-top-0">Build Stats</h2>
        <LineChart
          width={1500}
          height={500}
          data={buildStats}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis reversed dataKey="name" dy={5} minTickGap={75}/>
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="credits_used" stroke="#8884d8" dy={30} />
          <Line type="monotone" dataKey="duration" stroke="#82ca9d" dy={30} activeDot={{ r: 3 }} />
        </LineChart>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now let's run npm run start and we should see our deployment chart at the top:

alt text

Looks like we had a deployment that hanged (referring to the green spike). Thankfully it wasn't entirely synergistic with the credit usage.

Now, for our build chart. By filtering out the failed builds (including cancelled ones), we can get a better visualization on the relationship for our branch builds:

alt text

With the JSON returned, we can definitely get information back to that specific build to see what happened, and Recharts has an API event that allows us to click to get the data returned for a specific data point.

Thanks

I hope this helps you in better visualizing your credit usage with CircleCI! Thank you for reading, and please don't hesitate to ask questions or provide feedback in the comment section.

Top comments (0)