diff --git a/.env b/.env index 0572ee9920c0cd5211cc66e35baac5be4f82fd2a..303eb65a9d41b81e6fe169a011c8e823a9db57c7 100755 --- a/.env +++ b/.env @@ -70,3 +70,9 @@ SENTRY_DSN= # Php path should be automatically detected, but if you encounter some trouble you can use following variable : # PHP_PATH=/usr/bin/php + +###> Matomo integration - see docs/matomo.md ### +MATOMO_URL= +MATOMO_SITE_ID= +MATOMO_USER_TOKEN= +###< Matomo ### diff --git a/assets/admin.pack.js b/assets/admin.pack.js index b3547a08951b2f2c4f4eff4cd7e7c59854e51131..a198fab3045e53d1903d133a6083d9bca8623600 100755 --- a/assets/admin.pack.js +++ b/assets/admin.pack.js @@ -7,5 +7,8 @@ import './js/admin/element-import/element-import' import './js/admin/osm-tags' import './js/admin/element-edit' import './js/admin/source-priority' +import './js/admin/charts' + + import './js/i18n' diff --git a/assets/js/admin/MatomoVisits.vue b/assets/js/admin/MatomoVisits.vue new file mode 100644 index 0000000000000000000000000000000000000000..0983e93c1944746bed78f774b7f78e2a04bc2aeb --- /dev/null +++ b/assets/js/admin/MatomoVisits.vue @@ -0,0 +1,111 @@ +<template> + <div class="matomo-visits"> + <h4>{{ t('js.charts.title_visitors') }}</h4> + <div class="form-inline text-center"> + <div class="form-group"> + <label>{{ t('js.charts.display') }}</label> + <select class="form-control" v-model="field"> + <option v-for="type in ['nb_visits', 'avg_time_on_page', ]" :key="type" :value="type"> + {{ t(`js.charts.field.${type}`) }} + </option> + </select> + </div> + <div class="form-group"> + <label>{{ t('js.charts.last') }}</label> + <input class="input-last form-control" type="number" v-model="lastCount"/> + <select class="form-control" v-model="period"> + <option v-for="type in ['day', 'week', 'month', 'year']" :key="type" :value="type"> + {{ t(`js.charts.period.${type}`) }} + </option> + </select> + </div> + <button type="button" class="btn btn-default" @click="loadData">{{ t('js.charts.reload') }}</button> + </div> + <div class="chart-container" ref="chartContainer"> + <div class="loader"><i class="fa fa-spienner fa-spin"></i></div> + </div> + </div> +</template> + +<script> +import Highcharts from 'highcharts' +export default { + props: ['baseUrl', 'siteId', 'token', 'projectUrl'], + data() { + return { + period: 'month', + lastCount: '12', + field: 'nb_visits', + data: [] + } + }, + computed: { + matomoUrl() { + return `${this.baseUrl}/index.php?module=API`+ + `&method=Actions.getPageTitle`+ + `&idSite=${this.siteId}`+ + `&pageName=${this.projectUrl}`+ // we use the projectUrl as pageName in order to easily get stats for every project on saas instance + `&period=${this.period}`+ + `&date=last${this.lastCount}`+ + `&format=JSON`+ + `&token_auth=${this.token}` + } + }, + watch: { + field: function() { + this.drawData() + } + }, + methods: { + loadData() { + $.getJSON(this.matomoUrl, (data) => { + this.data = data + this.drawData() + }) + }, + drawData() { + const chart = Highcharts.chart(this.$refs.chartContainer, { + chart: { type: 'spline' }, + title: false, + xAxis: { + categories: Object.keys(this.data).map((el) => el) + }, + yAxis: { title: false }, + series: [{ + name: t(`js.charts.field.${this.field}`), + data: Object.values(this.data).map((el) => el.length ? el[0][this.field] : 0) + }] + }); + } + }, + mounted() { + this.loadData() + } +} +</script> + +<style lang="scss" scoped> + .matomo-visits { + background-color: white; + padding-top: 1rem; + margin-bottom: 2rem; + } + h4 { + text-align: center; + margin-bottom: 1.5rem; + color: black; + } + .input-last { width: 60px; } + .form-group { + margin-right: 1rem; + } + label { + font-weight: normal; + } + .loader { + padding: 50px 0; + text-align: center; + i { font-size: 30px; } + } + +</style> \ No newline at end of file diff --git a/assets/js/admin/charts.js b/assets/js/admin/charts.js new file mode 100644 index 0000000000000000000000000000000000000000..73762c120cb11e7cb956c1bb67dcb082abc59b81 --- /dev/null +++ b/assets/js/admin/charts.js @@ -0,0 +1,16 @@ +import Highcharts from 'highcharts' + +// Make it availabel globally for the highcharts-bundle +window.Highcharts = Highcharts + +import Vue from '../vendor/vue-custom' +import MatomoVisits from './MatomoVisits' + +document.addEventListener('DOMContentLoaded', function() { + if ($('.matomo-visits-container').length > 0) { + new Vue({ + el: '.matomo-visits-container', + components: { MatomoVisits } + }) + } +}) \ No newline at end of file diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 23ad18cc7cc90e075006b1331770412a6e3570c2..fcc29f21356ef1b6043790555f671c72d767cb66 100755 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -13,6 +13,9 @@ twig: oauth_google: '%oauth_google_id%' oauth_facebook: '%oauth_facebook_id%' helper: '@App\Services\TwigHelperService' + matomo_url: '%env(MATOMO_URL)%' + matomo_site_id: '%env(MATOMO_SITE_ID)%' + matomo_token: '%env(MATOMO_USER_TOKEN)%' paths: "%kernel.root_dir%/../web": RootDir form_themes: diff --git a/config/services.yaml b/config/services.yaml index e1ce9f7b9d3a439fa9c86c2ec3d7d143aad4e04b..f312cd9eef718ee7e5433e3ca8c2d13f7709d89d 100755 --- a/config/services.yaml +++ b/config/services.yaml @@ -55,6 +55,11 @@ services: arguments: $rootDB: '%root_db%' + App\Services\TwigHelperService: + arguments: + $baseUrl: '%base_url%' + $useAsSaas: '%use_as_saas%' + # Commands App\Command\RemoveAbandonnedProjectsCommand: arguments: diff --git a/docs/matomo.md b/docs/matomo.md new file mode 100644 index 0000000000000000000000000000000000000000..41bae4425ab67aecb82869a924464591b793bc73 --- /dev/null +++ b/docs/matomo.md @@ -0,0 +1,20 @@ +# Matomo Integration + +If you want to track visitors the open source software Matomo, then you need to + +### Provide environement variable +in `.env.local` +``` +MATOMO_URL=https://my_matomo_server.org/ +MATOMO_SITE_ID=12 +MATOMO_USER_TOKEN=anonymous +``` +You can also use a dedicate user token if you prefer + +### Configure Cross Origin + +In your Matomo instance, go to Administration > System > General settings and fill the Cross Origin Section. More info at https://matomo.org/faq/how-to/faq_18694/ + +### Allow visibility for anonymous user + +If you use the anonymous token, then go to Administration > System > Users, and grant anonymous user the "view" permission for the website diff --git a/package.json b/package.json index be1af9b1b1c0dfd4fbd8af5d2b7686e3ad2d58df..75956803d2a8ba45ab1bb994a42e24cc5aea5597 100755 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "core-js": "^3.8.1", "gogocarto-js": "^1.7.3", + "highcharts": "^9.1.0", "leaflet": "^1.7.1", "leaflet-shades": "^0.1.4", "natives": "^1.1.6", diff --git a/src/Services/TwigHelperService.php b/src/Services/TwigHelperService.php index 1f3c420debd1e79c9f2640b82a9e460b489f77f6..d822863b97289e76b4b28ee3a6bc7675a8c07ca4 100755 --- a/src/Services/TwigHelperService.php +++ b/src/Services/TwigHelperService.php @@ -7,10 +7,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class TwigHelperService { - public function __construct(DocumentManager $dm, TranslatorInterface $t) + public function __construct(DocumentManager $dm, TranslatorInterface $t, $baseUrl, $useAsSaas) { $this->dm = $dm; $this->t = $t; + $this->baseUrl = $baseUrl; + $this->useAsSaas = $useAsSaas; } public function config() @@ -23,6 +25,16 @@ class TwigHelperService return $this->t; } + public function mainUrl() + { + if ($url = $this->config()->getCustomDomain()) + return explode('://', $url)[1]; + elseif ($this->useAsSaas) + return $this->config()->getDbName() . '.' . $this->baseUrl; + else + return $this->baseUrl; + } + public function listAbouts() { return $this->dm->get('About')->findAllOrderedByPosition(); diff --git a/templates/admin/blocks/block_charts.html.twig b/templates/admin/blocks/block_charts.html.twig index ef3bce0bec25587b101230b04f9e701919452b8b..892ec6f643f610ec7d12e31d4de781d992f383a5 100755 --- a/templates/admin/blocks/block_charts.html.twig +++ b/templates/admin/blocks/block_charts.html.twig @@ -2,8 +2,6 @@ {% block block %} - <script src="//code.highcharts.com/4.1.8/highcharts.js"></script> - <script src="//code.highcharts.com/4.1.8/modules/exporting.js"></script> <script type="text/javascript"> {{ chart(collabResolveChart) }} {{ chart(userInteractChart) }} @@ -12,7 +10,19 @@ </script> <div class="chart-container row"> - <div id="userInteractChart" class="col-md-12 chart-item"></div> + <div id="userInteractChart" class="col-md-12 chart-item"></div> + + {% if matomo_url and matomo_site_id and matomo_token %} + <div class="matomo-visits-container col-md-12"> + <matomo-visits + base-url="{{ matomo_url}}" + site-id="{{ matomo_site_id }}" + token="{{ matomo_token }}" + project-url="{{ helper.mainUrl }}"> + </matomo-visits> + </div> + {% endif %} + <div id="contribsAddResolvedPie" class="col-lg-4 col-md-6 chart-item small"></div> <div id="contribsEditResolvedPie" class="col-lg-4 col-md-6 chart-item small"></div> <div id="collabResolveChart" class="col-lg-4 col-md-12 chart-item small"></div> diff --git a/templates/custom-head.html.twig b/templates/custom-head.html.twig index 1e5e1c1f7d2cd25e2e990ed9bab6f7d3e17584df..d52035ec4f16084f817603420615fe0bc727a74f 100755 --- a/templates/custom-head.html.twig +++ b/templates/custom-head.html.twig @@ -38,4 +38,25 @@ <link rel="stylesheet" href="{{ asset('fonts/fontawesome-5/css/all.css') }}" /> {{ config.iconImport|raw }} -{{ config.fontImport | raw }} \ No newline at end of file +{{ config.fontImport | raw }} + +{# Matomo user tracking, if enabled in .env file #} +{% if matomo_url and matomo_site_id and matomo_token %} +<script> + var _paq = window._paq = window._paq || []; + /* tracker methods like "setDocumentTitle" should be called before "trackPageView" */ + _paq.push(["setDocumentTitle", "{{ helper.mainUrl }}"]); // we use the projectUrl as pageName in order to easily get stats for every project on saas instance + _paq.push(["setCookieDomain", "*.{{ base_url }}"]); + _paq.push(["setDomains", ["*.{{ base_url }}"]]); + _paq.push(['trackPageView']); + + _paq.push(['enableLinkTracking']); + (function() { + var u="{{ matomo_url }}"; + _paq.push(['setTrackerUrl', u+'matomo.php']); + _paq.push(['setSiteId', '{{ matomo_site_id }}']); + var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); + })(); +</script> +{% endif %} \ No newline at end of file diff --git a/translations/admin+intl-icu.fr.yaml b/translations/admin+intl-icu.fr.yaml index e1013030379f46cb9a4ff0c342eb32e73e31fc98..a1fcb5befda2f724ab434dc751e07d2f46739adf 100755 --- a/translations/admin+intl-icu.fr.yaml +++ b/translations/admin+intl-icu.fr.yaml @@ -1306,6 +1306,19 @@ js: # Below keys are available to javascript api: placeholder: "Sélectionnez une ou plusieurs catégories" + charts: + period: + day: Jour + week: Semaine + month: Mois + year: Année + field: + nb_visits: Nombre de visites + avg_time_on_page: Temps moyen sur la page (secondes) + title_visitors: Statistiques des visiteurs + display: Afficher + last: Les derniers + reload: Recharger mapping_ontology: # templates/admin/core_custom/custom-fields/mapping-ontology.html.twig id: "Identifiant unique (dans l'ancienne base de donnée)" name: "Titre de la fiche" diff --git a/yarn.lock b/yarn.lock index 55c0fb2e86b871907965502dc86a9863962848f7..4d8c32f64eee174ed53461c345c364b485bbcdd6 100755 --- a/yarn.lock +++ b/yarn.lock @@ -4444,6 +4444,11 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +highcharts@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-9.1.0.tgz#2cdb38e2e03530b4fde022bb05fbce5b34651e39" + integrity sha512-K7HUuKhEylZ1pMdzGR35kPgUmpp0MDNpaWhEMkGiC5Jfzg/endtTLHJN2lsFqEO+xoN7AykBK98XaJPEpsrLyA== + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"