334 lines
8.0 KiB
JavaScript
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()
|