Skip to content

Commit

Permalink
Issue #1 MVP send umami report
Browse files Browse the repository at this point in the history
  • Loading branch information
boly38 committed Jul 2, 2022
1 parent e79f5a7 commit cec7f8c
Show file tree
Hide file tree
Showing 924 changed files with 158,162 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/umamiReport.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: umami-report

on:
schedule:
- cron: '0 10 * * *'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

jobs:
umamiReport:
name: umami report example
runs-on: ubuntu-latest

steps:
- name: Create Umami report
uses: boly38/action-umami-report@v1
with:
umami-server: ${{secrets.UMAMI_SERVER}}
umami-user: ${{secrets.UMAMI_USERNAME}}
umami-password: ${{secrets.UMAMI_PASSWORD}}
umami-site-domain: ${{secrets.UMAMI_SIDE_DOMAIN}}

- name: Send Umami report to discord channel
uses: sinshutu/upload-to-discord@master
env:
DISCORD_WEBHOOK: ${{ secrets.UMAMI_TO_DISCORD_WEBHOOK }}
with:
args: umamiReport.md
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#Intellij
.idea/
*.iml

# github actions require node_modules too
# https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action

# dont push
*.dontpush.*
umamiReport.md
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
# action-umami-report

This action prints daily umami report into a given file

## Inputs

## `umami-server`

**Required** The umami server instance. Example `"https://umami.mysite.com"`.

## `umami-user`

**Required** The umami server instance user. Default `"admin"`.

## `umami-password`

**Required** The umami server instance password.

## `umami-site-domain`

**Required** The umami site domain name. Example `"www.mysite.com"`.Default selecting the first site domain name.

## Outputs

## `umamiReport.md`

The umami report for last 24h.

## Example usage

```yaml
- name: Create Umami report
uses: boly38/action-umami-report@v1
with:
umami-server: ${{secrets.UMAMI_SERVER}}
umami-user: ${{secrets.UMAMI_USERNAME}}
umami-password: ${{secrets.UMAMI_PASSWORD}}
umami-site-domain: ${{secrets.UMAMI_SIDE_DOMAIN}}
```
Full working sample: cf. [umamiReport.yml](.github/workflows/umamiReport.yml)
# See also
## Umami
- Umami [API](https://umami.is/docs/api) - [Source](https://github.com/umami-software/umami)
## possible next step
- send the report [by email](https://github.com/dawidd6/action-send-mail), on [discord](https://github.com/marketplace/actions/upload-to-discord), etc..
19 changes: 19 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Create Umami report
description: Produce umamiReport.md from umami site last 24h stats
author: boly38
inputs:
umami-server:
description: The umami server instance.
required: true
umami-user:
description: The umami server instance user
required: true
umami-password:
description: The umami server instance password
required: true
umami-site-domain:
description: The umami site domain name
required: false
runs:
using: node16
main: main.js
12 changes: 12 additions & 0 deletions env/initenv.template.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash
export UMAMI_SERVER="https://umami.replace-me.exemple.com"
export UMAMI_USER="admin"
export UMAMI_PASSWORD="12333321"
# export UMAMI_SITE_DOMAIN="replace-me.exemple.com"
export UMAMI_SITE_DOMAIN="*first*"

### Dev debug API
# export UMAMI_DEBUG_RESPONSE=true
# export UMAMI_DEBUG_REQUEST=true
### Dev debug Action
# export UMAMI_DEBUG_ACTION=true
36 changes: 36 additions & 0 deletions lib/action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import fs from 'fs';
import dayjs from 'dayjs';

import umamiApi from './umami/api.js'

const DEBUG_ACTION = process.env.UMAMI_DEBUG_ACTION === 'true';
const rethrow = (err) => {throw err;}

class Action {

static async produceReport(umamiSite, umamiSiteStat, targetFile) {
const reportDateTime = dayjs().format('DD/MM/YYYY HH:mm');
var umamiReport = `# ${reportDateTime} - Umami report\n`
umamiReport += `for ${umamiSite.domain} [last 24 hours] :\n\n`;
umamiReport += ` - ${umamiSiteStat.pageviews.value} (change:${umamiSiteStat.pageviews.change}) page views\n`;
umamiReport += ` - ${umamiSiteStat.uniques.value} (change:${umamiSiteStat.uniques.change}) uniques\n`;
umamiReport += ` - ${umamiSiteStat.bounces.value} (change:${umamiSiteStat.bounces.change}) bounces\n`;
umamiReport += ` - ${umamiSiteStat.totaltime.value} (change:${umamiSiteStat.totaltime.change}) totaltime\n`;
umamiReport += '\n';
fs.writeFileSync(targetFile, umamiReport);
}

static async umamiDailyReportV0(server, user, password, domain = '*first*', targetFile = 'umamiReport.md') {
const authData = await umamiApi.login(server, user, password).catch(rethrow);
const sites = await umamiApi.getSites(server, authData).catch(rethrow);
const site = umamiApi.selectSiteByDomain(sites, domain);
const siteStats = await umamiApi.getStats(server, authData, site).catch(rethrow);
DEBUG_ACTION && console.log(site);
DEBUG_ACTION && console.log(siteStats);
Action.produceReport(site, siteStats, targetFile);
return { site, siteStats, targetFile }
}

}

export default Action;
85 changes: 85 additions & 0 deletions lib/umami/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fetch from 'node-fetch';
import querystring from 'node:querystring';

// DEV debug
const DEBUG_RESPONSE = process.env.UMAMI_DEBUG_RESPONSE === 'true';
const DEBUG_REQUEST = process.env.UMAMI_DEBUG_REQUEST === 'true';

//~ Utils
const arrayIncludesAllOf = (arr, target) => target.every(v => arr.includes(v));
const isSet = (value) => value !== null && value !== undefined;
const isNotEmptyArray = (value) => isSet(value) && Array.isArray(value) && value.length > 0;
const isUmamiSiteData = (data) => isSet(data) && arrayIncludesAllOf(Object.keys(data), ['website_id', 'website_uuid', 'name', 'domain', 'created_at']);
const givenAuthData = (authData) => {
if (!isSet(authData) || !isSet(authData.token)) {
throw "expect valid auth data to query api";
}
};
const rethrow = (err) => {throw err;}

// Umami API : https://umami.is/docs/api
// TODO : move this util class into a dedicated node-umami-api repository

class UmamiApi {

static async login(serverUrl, username, password) {
const authResponse = await fetch(serverUrl+ "/api/auth/login",
{method: 'POST', body: JSON.stringify({username, password}),
headers: {'Content-Type': 'application/json'}
}).catch(rethrow);
if (authResponse.status != 200) {
throw "Invalid login"
}
const authData = await authResponse.json();
DEBUG_RESPONSE && console.log(authData);
return authData;
}

static async getSites(serverUrl, authData) {
givenAuthData(authData);
const getSitesResponse = await fetch(serverUrl+ "/api/websites",
{ headers: { "Authorization": `Bearer ${authData.token}`} }).catch(rethrow);
if (getSitesResponse.status != 200) {
throw "Unable to get sites - " + await getSitesResponse.text();
}
const sitesData = await getSitesResponse.json();
DEBUG_RESPONSE && console.log(sitesData);
return sitesData;
}

static selectSiteByDomain(sitesData, siteDomain = '*first*') {
if (!isNotEmptyArray(sitesData)) {
throw "No sites data provided";
}
if (!isUmamiSiteData(sitesData[0])) {
throw "Unexpected sites data provided";
}
if (siteDomain === '*first*') {
return sitesData[0];
}
return sitesData.find( d => d.domain === siteDomain );
}

static async getStats(serverUrl, authData, siteData) {
givenAuthData(authData);
if (!isUmamiSiteData(siteData)) {
throw "Unexpected site data provided";
}
const start_at = Date.now() - (60000 * 60 * 24);
const end_at = Date.now();
// const statsUrl = serverUrl+ `/api/website/${siteData.website_id}/stats?start_at=${start_at}&end_at=${end_at}`;
const statsUrl = serverUrl+ `/api/website/${siteData.website_id}/stats?` + querystring.stringify({ start_at, end_at });
DEBUG_REQUEST && console.log(statsUrl);
const getStatsResponse = await fetch(statsUrl,
{ headers: { "Authorization": `Bearer ${authData.token}`} }).catch(rethrow);
if (getStatsResponse.status != 200) {
throw `Unable to get stats - ${getStatsResponse.status} - ` + await getStatsResponse.text();
}
const sitesStat = await getStatsResponse.json();
DEBUG_RESPONSE && console.log(sitesStat);
return sitesStat;
}

}

export default UmamiApi;
28 changes: 28 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import action from './lib/action.js'
import core from '@actions/core';
import github from '@actions/github';

const umamiServer = core.getInput('umami-server', { required: true });
var umamiUser = core.getInput('umami-user', { required: true });
const umamiPassword = core.getInput('umami-password', { required: true });
const umamiSiteDomain = core.getInput('umami-site-domain');// *first*
const rethrow = (err) => {throw err;};
const printContext = () => {
// Get the JSON webhook payload for the event that triggered the workflow
const payload = JSON.stringify(github.context.payload, undefined, 2)
console.log(`The event payload: ${payload}`);
};
try {
if (umamiServer === null || umamiServer === undefined) {
throw "please setup your environment"
}
if (umamiUser === null || umamiUser === undefined) {
umamiUser = 'admin';
}
// printContext();
const reportResult = await action.umamiDailyReportV0(umamiServer, umamiUser, umamiPassword, umamiSiteDomain).catch(rethrow);
core.info(`Generated : ${reportResult.targetFile}`);
} catch (error) {
console.info(`ERROR: ${error}`)
core.setFailed(error);
}
20 changes: 20 additions & 0 deletions manual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import action from './lib/action.js'
import core from '@actions/core';
import github from '@actions/github';

const UMAMI_SERVER = process.env.UMAMI_SERVER || null; // "https://umami.exemple.com";
const UMAMI_USER = process.env.UMAMI_USER || "admin";
const UMAMI_PASSWORD = process.env.UMAMI_PASSWORD || null;
const UMAMI_SITE_DOMAIN = process.env.UMAMI_SITE_DOMAIN || "*first*";
const rethrow = (err) => {throw err;}

try {
if (UMAMI_SERVER === null) {
throw "please setup your environment UMAMI_SERVER, UMAMI_USER, UMAMI_PASSWORD, UMAMI_SITE_DOMAIN"
}
const reportResult = await action.umamiDailyReportV0(UMAMI_SERVER, UMAMI_USER, UMAMI_PASSWORD, UMAMI_SITE_DOMAIN)
.catch(rethrow);
console.info(`Generated : ${reportResult.targetFile}`);
} catch (error) {
console.info(`ERROR: ${error}`)
}
Loading

0 comments on commit cec7f8c

Please sign in to comment.