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 stores a cookie. So we're building a helper class that does the login and fetching of data for us.

The login is storing a cookie in the browser, that's the only difficult part. All other endpoints are URLs that expect a cookie and return data.

umami.js
const fetch = require('node-fetch')

function parseCookies(response) {
const raw = response.headers.raw()['set-cookie'];
return raw.map((entry) => {
const parts = entry.split(';');
const cookiePart = parts[0];
return cookiePart;
}).join(';');
}

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

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

async auth() {
const req = 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',
});

this.cookies = parseCookies(req)

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': '*/*',
'cookie': this.cookies,
},
'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/website/${this.website}/stats`)
}

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

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

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

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

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

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

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

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

async getCountries() {
return await this.request(`${this.url}/api/website/${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

With this helper you can read all the data the dashboard is displaying, but one could extend the helper to create users or websites as well. Watch what the dashboard does when you create a user and imitating it with the Javascript code.

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: 1, // internal website-id
})

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.