mon-entreprise/source/scripts/fetch-stats.js

334 lines
8.0 KiB
JavaScript

// This script uses the Matomo API which requires an access token
// Once you have your access token you can put it in a `.env` file at the root
// of the project to enable it during development. For instance:
//
// MATOMO_TOKEN=f4336c82cb1e494752d06e610614eab12b65f1d1
//
// Matomo API documentation:
// https://developer.matomo.org/api-reference/reporting-api
require('dotenv').config()
require('isomorphic-fetch')
const querystring = require('querystring')
const { createDataDir, writeInDataDir } = require('./utils.js')
const R = require('ramda')
const apiURL = params => {
const query = querystring.stringify({
period: 'month',
date: 'last1',
method: 'API.get',
format: 'JSON',
module: 'API',
idSite: 39,
language: 'fr',
apiAction: 'get',
token_auth: process.env.MATOMO_TOKEN,
...params
})
return `https://stats.data.gouv.fr/index.php?${query}`
}
async function main() {
createDataDir()
const stats = {
simulators: await fetchSimulatorsMonth(),
monthlyVisits: await fetchMonthlyVisits(),
dailyVisits: await fetchDailyVisits(),
statusChosen: await fetchStatusChosen(),
feedback: await fetchFeedback(),
channelType: await fetchChannelType()
}
writeInDataDir('stats.json', stats)
}
function xMonthAgo(x = 0) {
const date = new Date()
if (date.getMonth() - x > 0) {
date.setMonth(date.getMonth() - x)
} else {
date.setMonth(12 + date.getMonth() - x)
date.setFullYear(date.getFullYear() - 1)
}
return date.toISOString().substring(0, 7)
}
async function fetchSimulatorsMonth() {
const getDataFromXMonthAgo = async x => {
const date = xMonthAgo(x)
return { date, visites: await fetchSimulators(`${date}-01`) }
}
return {
currentMonth: await getDataFromXMonthAgo(0),
oneMonthAgo: await getDataFromXMonthAgo(1),
twoMonthAgo: await getDataFromXMonthAgo(2)
}
}
async function fetchSimulators(dt) {
try {
const response = await fetch(
apiURL({
period: 'month',
date: `${dt}`,
method: 'Actions.getPageUrls',
filter_limits: -1
})
)
const firstLevelData = await response.json()
const coronavirusPage = firstLevelData.find(
page => page.label === '/coronavirus'
)
// Visits on simulators pages
const idSubTableSimulateurs = firstLevelData.find(
page => page.label === 'simulateurs'
).idsubdatatable
const responseSimulateurs = await fetch(
apiURL({
date: `${dt}`,
method: 'Actions.getPageUrls',
search_recursive: 1,
filter_limits: -1,
idSubtable: idSubTableSimulateurs
})
)
const dataSimulateurs = await responseSimulateurs.json()
const resultSimulateurs = dataSimulateurs
.filter(({ label }) =>
[
'/salarié',
'/auto-entrepreneur',
'/artiste-auteur',
'/indépendant',
'/comparaison-régimes-sociaux',
'/assimilé-salarié'
].includes(label)
)
/// Two '/salarié' pages are reported on Matomo, one of which has very few
/// visitors. We delete it manually.
.filter(
x =>
x.label != '/salarié' ||
x.nb_visits !=
dataSimulateurs
.filter(x => x.label == '/salarié')
.reduce((a, b) => Math.min(a, b.nb_visits), 1000)
)
// Add iframes
const idTableIframes = firstLevelData.find(page => page.label == 'iframes')
.idsubdatatable
const responseIframes = await fetch(
apiURL({
date: `${dt}`,
method: 'Actions.getPageUrls',
search_recursive: 1,
filter_limits: -1,
idSubtable: idTableIframes
})
)
const dataIframes = await responseIframes.json()
const resultIframes = dataIframes.filter(x =>
[
'/simulateur-embauche?couleur=',
'/simulateur-autoentrepreneur?couleur='
].includes(x.label)
)
const groupSimulateursIframesVisits = ({ label }) =>
label.startsWith('/simulateur-embauche')
? '/salarié'
: label.startsWith('/simulateur-autoentrepreneur')
? '/auto-entrepreneur'
: label
const sumVisits = (acc, { nb_visits }) => acc + nb_visits
const results = R.reduceBy(
sumVisits,
0,
groupSimulateursIframesVisits,
[...resultSimulateurs, ...resultIframes, coronavirusPage].filter(
x => x !== undefined
)
)
return Object.entries(results)
.map(([label, nb_visits]) => ({ label, nb_visits }))
.sort((a, b) => b.nb_visits - a.nb_visits)
} catch (e) {
console.log('fail to fetch Simulators Visits')
return null
}
}
// We had a tracking bug in 2019, in which every click on Safari+iframe counted
// as a visit, so the numbers are manually corrected.
const visitsIn2019 = {
'2019-01': 119541,
'2019-02': 99065,
'2019-03': 122931,
'2019-04': 113454,
'2019-05': 118637,
'2019-06': 152981,
'2019-07': 141079,
'2019-08': 127326,
'2019-09': 178474,
'2019-10': 198260,
'2019-11': 174515,
'2019-12': 116305
}
async function fetchMonthlyVisits() {
try {
const response = await fetch(
apiURL({
period: 'month',
date: 'previous12',
method: 'VisitsSummary.getUniqueVisitors'
})
)
const data = await response.json()
const result = Object.entries({ ...data, ...visitsIn2019 })
.sort(([t1], [t2]) => (t1 > t2 ? 1 : -1))
.map(([date, visiteurs]) => ({ date, visiteurs }))
return result
} catch (e) {
console.log('fail to fetch Monthly Visits')
return null
}
}
async function fetchDailyVisits() {
try {
const response = await fetch(
apiURL({
period: 'day',
date: 'previous30',
method: 'VisitsSummary.getUniqueVisitors'
})
)
const data = await response.json()
return Object.entries(data).map(([date, visiteurs]) => ({
date,
visiteurs
}))
} catch (e) {
console.log('fail to fetch Daily Visits')
return null
}
}
async function fetchStatusChosen() {
try {
const response = await fetch(
apiURL({
method: 'Events.getAction',
label: 'status chosen',
date: 'previous1'
})
)
const data = await response.json()
const response2 = await fetch(
apiURL({
method: 'Events.getNameFromActionId',
idSubtable: Object.values(data)[0][0].idsubdatatable,
date: 'previous1'
})
)
const data2 = await response2.json()
const result = Object.values(data2)[0].map(({ label, nb_visits }) => ({
label,
nb_visits
}))
return result
} catch (e) {
console.log('fail to fetch Status Chosen')
return null
}
}
async function fetchFeedback() {
try {
const APIcontent = await fetch(
apiURL({
method: 'Events.getCategory',
label: 'Feedback > @rate%20page%20usefulness',
date: 'previous5'
})
)
const APIsimulator = await fetch(
apiURL({
method: 'Events.getCategory',
label: 'Feedback > @rate%20simulator',
date: 'previous5'
})
)
const feedbackcontent = await APIcontent.json()
const feedbacksimulator = await APIsimulator.json()
let content = 0
let simulator = 0
let j = 0
// The weights are defined by taking the coefficients of an exponential
// smoothing with alpha=0.8 and normalizing them. The current month is not
// considered.
const weights = [0.0015, 0.0076, 0.0381, 0.1905, 0.7623]
for (const i in feedbackcontent) {
content += feedbackcontent[i][0].avg_event_value * weights[j]
simulator += feedbacksimulator[i][0].avg_event_value * weights[j]
j += 1
}
return {
content: Math.round(content * 10),
simulator: Math.round(simulator * 10)
}
} catch (e) {
console.log('fail to fetch feedbacks')
return null
}
}
async function fetchChannelType() {
try {
const response = await fetch(
apiURL({
period: 'month',
date: 'last3',
method: 'Referrers.getReferrerType'
})
)
const data = await response.json()
const result = R.map(
date =>
date
.filter(x =>
['Sites web', 'Moteurs de recherche', 'Entrées directes'].includes(
x.label
)
)
.map(({ label, nb_visits }) => ({
label,
nb_visits
})),
data
)
const dates = Object.keys(result).sort((t1, t2) => t1 - t2)
return {
currentMonth: { date: dates[0], visites: result[dates[0]] },
oneMonthAgo: { date: dates[1], visites: result[dates[1]] },
twoMonthAgo: { date: dates[2], visites: result[dates[2]] }
}
} catch (e) {
console.log('fail to fetch channel type')
return null
}
}
main()