Intro

Umami is a simple analytics tool. It does the basic view and event tracking without being too heavy or tracking too intrusive, while being fairly flexible for the frontend.

Umami stores the data (by default) in a Postgres database to display them in the dashboard. Getting access to the raw data is a bit more difficult, but we can reuse the dashboard endpoints.

Writing a helper class

The API is pretty straight forward, logging into the dashboard gives us a login token. So we're building a helper class that does the login and fetching of data for us.

Recent Umami updates changed how the token transmitted (used to be cookies), and how website IDs are generated (used to be a simple integer, is now a GUID
I'll keep this helper class updated when I notice some breaking changes

const fetch = require('node-fetch')  // commonjs
import fetch from 'node-fetch' // es6 modules

class Umami {
  constructor(config) {
    this.username = config.username
    this.password = config.password
    this.website = config.website
    this.url = config.url
    this.resolution = 'day'
    this.token = ''

    const today = new Date()
    this.endAt = today.getTime()
    this.startAt = today.setDate(today.getDate() - 30)
  }

  async auth() {
    const json = await fetch(`${this.url}/api/auth/login`, {
      'headers': {
        'accept': 'application/json',
        'content-type': 'application/json; charset=UTF-8',
        'cookie': '',
      },
      'body': JSON.stringify({
        username: this.username,
        password: this.password
      }),
      'method': 'POST',
    })
      .then(res => res.json());

    this.token = json.token

    return this
  }

  async request(url) {
    const divider = url.includes('?') ? '&' : '?'
    const stats = await fetch(`${url}${divider}start_at=${this.startAt}&end_at=${this.endAt}`, {
      'headers': {
        'accept': '*/*',
        'authorization': 'Bearer ' + this.token,
      },
      'method': 'GET',
    })
      .then(res => res.json())
      .catch(err => console.log(err))

    return stats
  }

  setTimerange(days) {
    const today = new Date()
    this.endAt = today.getTime()
    this.startAt = today.setDate(today.getDate() - days)

    return this
  }

  resolutionDay() { // umami is using this for the charts, groups the dataset
    this.resolution = 'day'

    return this
  }

  resolutionMonth() { // umami is using this for the charts, groups the dataset
    this.resolution = 'month'

    return this
  }

  last90Days() {
    this.setTimerange(90)

    return this
  }

  last30Days() {
    this.setTimerange(30)

    return this
  }

  last7Days() {
    this.setTimerange(7)

    return this
  }

  customRange(startAt, endAt) {
    this.startAt = startAt
    this.endAt = endAt

    return this
  }

  setWebsite(website) {
    this.website = website

    return this
  }

  async getStats() {
    return await this.request(`${this.url}/api/websites/${this.website}/stats`)
  }

  async getChartPageviews() {
    return await this.request(`${this.url}/api/websites/${this.website}/pageviews?unit=${this.resolution}&tz=Etc%2FUTC`)
  }

  async getChartEvents() {
    return await this.request(`${this.url}/api/websites/${this.website}/events?unit=${this.resolution}&tz=Etc%2FUTC`)
  }

  async getEvents() {
    return await this.request(`${this.url}/api/websites/${this.website}/metrics?type=event&tz=Etc%2FUTC`)
  }

  async getUrls() {
    return await this.request(`${this.url}/api/websites/${this.website}/metrics?type=url&tz=Etc%2FUTC`)
  }

  async getReferrers() {
    return await this.request(`${this.url}/api/websites/${this.website}/metrics?type=referrer&tz=Etc%2FUTC`)
  }									

  async getBrowsers() {
    return await this.request(`${this.url}/api/websites/${this.website}/metrics?type=browser&tz=Etc%2FUTC`)
  }

  async getOses() {
    return await this.request(`${this.url}/api/websites/${this.website}/metrics?type=os&tz=Etc%2FUTC`)
  }

  async getDevices() {
    return await this.request(`${this.url}/api/websites/${this.website}/metrics?type=device&tz=Etc%2FUTC`)
  }

  async getCountries() {
    return await this.request(`${this.url}/api/websites/${this.website}/metrics?type=country&tz=Etc%2FUTC`)
  }
}

async function umamiClient(config) {
  const instance = new Umami(config);
  await instance.auth();
  return instance;
}

module.exports = umamiClient // commonjs
export default umamiClient // es6 modules

With this helper, we can read all the data the dashboard is displaying, but one could extend the helper to create users or websites as well.
Just watch what the dashboard does when you create a user, and write some JS that does the same request.

Using the helper class

The helper class expects an object with the instance URL, the username and the password to login.

Umami gives every page an internal ID starting at 0, you can pass that ID into the constructor or overwrite it later on with setWebsite(ID) if you need to gather data from multiple sites.

const umami = require('./umami.js');

const api = await umami({
    url: 'https://app.umami.is',
    username: 'username',
    password: 'password',
    website: 'site-guid',
})

const stats = await api.getStats()
const views = await api.getUrls()
const chartData = await api.last90Days().resolutionMonth().getChartPageviews() // pageviews for the last 90 days grouped by month

Final thoughts

It goes without saying that you shouldn't use this in your frontend, collecting large amounts of data takes quite a time since Umami does not cache data.
And, well, you don't want to expose your login credentials in JavaScript.

But it's a great little helper to access Umamis data, I'm using it in my 11ty generation to fetch the number of views for each post to rank the most viewed posts.