Github Insights Dashboard
Tags: atlashackathon, nextjs
Published: 2022-01-08 08:13:32

### Overview of My Submission
https://github.com/SoftMaple/github-insights-view

### Submission Category: 

Action Star

### Link to Code

{% github SoftMaple/github-insights-view no-readme %}

### Context
Recently, I made a project to generate {% katex inline %}
 \LaTeX
{% endkatex %} sourcecode for better paper typesetting: 
{% github SoftMaple/Editor %}

The situation is that the github just saves the latest 14 days repo clones and views. So I want to make a dashboard to view all the data per day. For me, **CRON** job is better. 

I attempted github actions at the first time, {% github SoftMaple/insights-cronjob no-readme %} but its schedule is not precised, just as the below screenshots show:

![github-actions-scheduled-time](https://ik.imagekit.io/1winv85cn8g/SoftMaple/CRON/github-actions-scheduled_nDHRIOHieYs.png)

![scheduled time](https://ik.imagekit.io/1winv85cn8g/SoftMaple/CRON/scheduled-time_2q7ZeEJW1.png)

It delayed about 1 hour, so I think there is a better way to trigger the event **precisely**.

Thanks for MongoDB Atlas Hackathon on DEV, it give me a new approache to schedule CRON job, that's **Atlas Triggers**.

### Implementation
Firstly, to store the data, we need to call github API to retrieve `clones` and `views`. we will use an npm dependency called [`@octokit/core`](https://github.com/octokit/core.js#readme). Before using it, add it to dependencies:
![octokit](https://ik.imagekit.io/1winv85cn8g/SoftMaple/CRON/octokit_qCqhUzGo3.png)

```js
const { Octokit } = require("@octokit/core"); // do not forget this.
async function callOctokit(route, owner, repo) {
  const octokit = new Octokit({ auth: OCTOKIT_TOKEN });
  return await octokit.request(route, { owner, repo });
}

async function getClones(collection, owner, repo) {
  try {
    const { status, data } = await callOctokit(
      "GET /repos/{owner}/{repo}/traffic/clones", 
      owner, 
      repo
    );
    
    const { clones } = data;
    // console.log(clones);
  } catch(err) {
    throw new Error("Error fetching clones: ", err.message, owner, repo);
  }
}

async function getViews(collection, owner, repo) {
  try {
    const { status, data } = await callOctokit(
      "GET /repos/{owner}/{repo}/traffic/views", 
      owner, 
      repo
    );
    
    const { views } = data;
    // console.log(views);
  } catch(err) {
    throw new Error("Error fetching views: ", err.message, owner, repo);
  }
}
```

It works.
Next, we need to store the data, but which should we store?
I want to update the data in Front End per day, so we just trigger it and store the 'yesterday' clones and views, so we need to implement a util function to get yesterday's data.

There is an example at MongoDB Atlas: [scheduled-triggers](https://docs.mongodb.com/realm/triggers/scheduled-triggers/#example)

> Notice: [`dayjs`](https://github.com/iamkun/dayjs/) is also a way to determine whether a date is yesterday.

But we need to set the date hour from '7' to '0' for comparing with github response data timestamp, see more details here: https://docs.github.com/en/rest/reference/repository-metrics#traffic

```js
function getToday() {
  return setDaybreak(new Date());
}

function getYesterday() {
  const today = getToday();
  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  // console.log(yesterday);
  // we just need the timestamp as this format: YYYY-MM-DD
  return yesterday.toISOString().split('T')[0];
}

function setDaybreak(date) {
  date.setHours(0); // modified original '7'.
  date.setMinutes(0);
  date.setSeconds(0);
  date.setMilliseconds(0);
  return date;
}
```

and then modify `getClones` and `getViews` functions.

```js
async function getClones(collection, owner, repo) {
  try {
    const { status, data } = await callOctokit(
      "GET /repos/{owner}/{repo}/traffic/clones", 
      owner, 
      repo
    );
    
    const { clones } = data;
    // console.log(clones);
    const latestData = clones[clones.length - 1];
    const shouldUpdate = latestData.timestamp.split("T")[0] === getYesterday();
    if (shouldUpdate) {
      await collection.insertOne({ name: repo, ...latestData });
      console.log("getClones: ", repo);
    } else { // Notice: if clones count is 0, it will not be returned from the api.
      await collection.insertOne({ name: repo, timestamp: new Date().toISOString(), count: 0, uniques: 0 });
    }
  } catch(err) {
    throw new Error("Error fetching clones: ", err.message, owner, repo);
  }
}

async function getViews(collection, owner, repo) {
  try {
    const { status, data } = await callOctokit(
      "GET /repos/{owner}/{repo}/traffic/views", 
      owner, 
      repo
    );
    
    const { views } = data;
    // console.log(views);
    const latestData = views[views.length - 1];
    // console.log(latestData.timestamp.split("T")[0], getYesterday());
    const shouldUpdate = latestData.timestamp.split("T")[0] === getYesterday();
    if (shouldUpdate) {
      await collection.insertOne({ name: repo, ...latestData });
      console.log("getViews: ", repo);
    } else { // Notice: if views count is 0, it will not be returned from the api.
      await collection.insertOne({ name: repo, timestamp: new Date().toISOString(), count: 0, uniques: 0 });
    }
  } catch(err) {
    throw new Error("Error fetching views: ", err.message, owner, repo);
  }
}
```
Finally, call the two functions as below:

```js
exports = async function trigger() {
  /*
    A Scheduled Trigger will always call a function without arguments.
    Documentation on Triggers: https://docs.mongodb.com/realm/triggers/overview/

    Functions run by Triggers are run as System users and have full access to Services, Functions, and MongoDB Data.

    Access a mongodb service:
    const collection = context.services.get(<SERVICE_NAME>).db("db_name").collection("coll_name");
    const doc = collection.findOne({ name: "mongodb" });

    Note: In Atlas Triggers, the service name is defaulted to the cluster name.

    Call other named functions if they are defined in your application:
    const result = context.functions.execute("function_name", arg1, arg2);

    Access the default http client and execute a GET request:
    const response = context.http.get({ url: <URL> })

    Learn more about http client here: https://docs.mongodb.com/realm/functions/context/#context-http
  */
  const db = context.services.get("Insights").db("insights");
  const clones = db.collection("clones");
  const views = db.collection("views");
  
  let success = true,
      error = null;
      
  try {
    await getClones(clones, "SoftMaple", "Editor");
    await getViews(views,"SoftMaple", "Editor");
    
  } catch(err) {
    error = err.message;
    success = false;
  }
  
  return {
    success,
    error
  };
}
```
The last step: **do not forget to set CRON time**
![CRON](https://ik.imagekit.io/1winv85cn8g/SoftMaple/CRON/CRON_WFAnJHw3l.png)

### Final Result
{% youtube E4gKm7YmJD0 %}
{% youtube nm9avcg_1sA %}

Thanks MongoDB Atlas Hackathon on DEV again!