Skip to content

Commit

Permalink
Add stats API
Browse files Browse the repository at this point in the history
  • Loading branch information
Gustry committed Jul 2, 2024
1 parent 92843ab commit 01c6946
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 34 deletions.
16 changes: 12 additions & 4 deletions cadastre/cadastre_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
(at your option) any later version.
"""
import configparser
import os
import os.path
import tempfile
Expand All @@ -25,8 +24,10 @@
from typing import Optional

from qgis.core import (
Qgis,
QgsApplication,
QgsLayoutExporter,
QgsMessageLog,
QgsPrintLayout,
QgsProject,
QgsReadWriteContext,
Expand All @@ -53,7 +54,9 @@
from cadastre.dialogs.options_dialog import CadastreOptionDialog
from cadastre.dialogs.parcelle_dialog import CadastreParcelleDialog
from cadastre.dialogs.search_dialog import CadastreSearchDialog
from cadastre.plausible import Plausible
from cadastre.processing.provider import CadastreProvider
from cadastre.tools import metadata_config


class CadastreMenu:
Expand Down Expand Up @@ -237,9 +240,7 @@ def initGui(self):
self.open_about_dialog()

# Display some messages depending on version number
self.mConfig = configparser.ConfigParser()
metadataFile = plugin_dir + "/metadata.txt"
self.mConfig.read(metadataFile, encoding='utf-8')
self.mConfig = metadata_config()

# Project load or create : refresh search and identify tool
self.iface.projectRead.connect(self.onProjectRead)
Expand All @@ -254,6 +255,13 @@ def initGui(self):

self.cadastre_search_dialog.visibilityChanged.connect(self.updateSearchButton)

# noinspection PyBroadException
try:
plausible = Plausible(server=False)
plausible.request_stat_event()
except Exception as e:
QgsMessageLog.logMessage("Error while calling the stats API : \"{}\"".format(e), 'cadastre', Qgis.Warning)

def open_import_dialog(self):
"""
Import dialog
Expand Down
133 changes: 133 additions & 0 deletions cadastre/plausible.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
__copyright__ = 'Copyright 2024, 3Liz'
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'

import json
import os
import platform

from qgis.core import Qgis, QgsNetworkAccessManager
from qgis.PyQt.QtCore import QByteArray, QDateTime, QUrl
from qgis.PyQt.QtNetwork import QNetworkReply, QNetworkRequest

from cadastre.logger import Logger
from cadastre.tools import to_bool, version

MIN_SECONDS = 3600
ENV_SKIP_STATS = "3LIZ_SKIP_STATS"

PLAUSIBLE_DOMAIN_PROD_SERVER = "plugin.server.lizmap.com"
PLAUSIBLE_DOMAIN_PROD_DESKTOP = "plugin.desktop.org"
PLAUSIBLE_URL_PROD = "https://bourbon.3liz.com/api/event"

# For testing purpose, to test.
# Similar to QGIS dashboard https://feed.qgis.org/metabase/public/dashboard/df81071d-4c75-45b8-a698-97b8649d7228
# We only collect data listed in the list below
# and the country according to IP address.
# The IP is not stored by Plausible Community Edition https://github.com/plausible/analytics
# Plausible is GDPR friendly https://plausible.io/data-policy
# The User-Agent is set by QGIS Desktop itself


class Plausible:

def __init__(self, server: bool):
""" Constructor. """
self.server = server
self.previous_date = None

def request_stat_event(self) -> bool:
""" Request to send an event to the API. """
if to_bool(os.getenv(ENV_SKIP_STATS), default_value=False):
# Disabled by environment variable
return False

if to_bool(os.getenv("CI"), default_value=False):
# If running on CI, do not send stats
return False

if version() in ('master', 'dev'):
return False

current = QDateTime().currentDateTimeUtc()
if self.previous_date and self.previous_date.secsTo(current) < MIN_SECONDS:
# Not more than one request per hour
# It's done at plugin startup anyway
return False

if self._send_stat_event():
self.previous_date = current
return True

return False

def _send_stat_event(self) -> bool:
""" Send stats event to the API. """
# Qgis.QGIS_VERSION → 3.34.6-Prizren
# noinspection PyUnresolvedReferences
qgis_version_full = Qgis.QGIS_VERSION.split('-')[0]
# qgis_version_full → 3.34.6
qgis_version_branch = '.'.join(qgis_version_full.split('.')[0:2])
# qgis_version_branch → 3.34

python_version_full = platform.python_version()
# python_version_full → 3.10.12
python_version_branch = '.'.join(python_version_full.split('.')[0:2])
# python_version_branch → 3.10

data = {
"props": {
# Plugin version
"plugin-version": version(),
# QGIS
"qgis-version-full": qgis_version_full,
"qgis-version-branch": qgis_version_branch,
# Python
"python-version-full": python_version_full,
"python-version-branch": python_version_branch,
},
"url": PLAUSIBLE_URL_PROD,
}

is_lizcloud = False
if self.server:
data["name"] = "cadastre-server"
is_lizcloud = "lizcloud" in os.getenv("QGIS_SERVER_APPLICATION_NAME", "").lower()
if is_lizcloud:
plausible_domain = os.getenv("QGIS_SERVER_PLAUSIBLE_DOMAIN_NAME", PLAUSIBLE_DOMAIN_PROD_SERVER)
else:
plausible_domain = PLAUSIBLE_DOMAIN_PROD_SERVER
else:
data["name"] = "cadastre-desktop"
plausible_domain = PLAUSIBLE_DOMAIN_PROD_DESKTOP

data["domain"] = plausible_domain

request = QNetworkRequest()
# noinspection PyArgumentList
request.setUrl(QUrl(PLAUSIBLE_URL_PROD))

# Only turn ON for debug purpose, temporary !
extra_debug = False
if extra_debug:
request.setRawHeader(b"X-Debug-Request", b"true")
request.setRawHeader(b"X-Forwarded-For", b"127.0.0.1")
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")

# noinspection PyArgumentList
r: QNetworkReply = QgsNetworkAccessManager.instance().post(request, QByteArray(str.encode(json.dumps(data))))
if not self.server:
return True

if not is_lizcloud:
return True

logger = Logger()
message = (
f"Request HTTP OS process '{os.getpid()}' sent to '{PLAUSIBLE_URL_PROD}' with domain '{plausible_domain} : ")
if r.error() == QNetworkReply.NoError:
logger.info(message + "OK")
else:
logger.warning(message + r.error())

return True
11 changes: 10 additions & 1 deletion cadastre/server/cadastre_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from qgis.server import QgsServerInterface

from cadastre.logger import Logger
from cadastre.plausible import Plausible
from cadastre.server.cadastre_service import CadastreService
from cadastre.server.tools import version
from cadastre.tools import version


class CadastreServer:
Expand All @@ -24,6 +25,14 @@ def __init__(self, server_iface: QgsServerInterface) -> None:

Logger.info(f'Init server version "{version()}"')

# noinspection PyBroadException
try:
plausible = Plausible(server=True)
plausible.request_stat_event()
except Exception as e:
Logger.log_exception(e)
Logger.critical('Error while calling the API stats')

cache_dir_str = os.getenv('QGIS_CADASTRE_CACHE_DIR')
if not cache_dir_str:
# Create cache in /tmp/org.qgis.cadastre
Expand Down
29 changes: 0 additions & 29 deletions cadastre/server/tools.py

This file was deleted.

41 changes: 41 additions & 0 deletions cadastre/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
__license__ = "GPL version 3"
__email__ = "info@3liz.org"

import configparser
import subprocess
import time

from pathlib import Path
from typing import Union

from qgis.utils import pluginMetadata

Expand Down Expand Up @@ -94,3 +96,42 @@ def set_window_title() -> str:
# version, current_git_hash(), next_git_tag())

return f'next {next_git_tag()}'


def to_bool(val: Union[str, int, float, bool, None], default_value: bool = True) -> bool:
""" Convert lizmap config value to boolean """
if isinstance(val, bool):
return val

if val is None or val == '':
return default_value

if isinstance(val, str):
# For string, compare lower value to True string
return val.lower() in ('yes', 'true', 't', '1')

elif not val:
# For value like False, 0, 0.0, None, empty list or dict returns False
return False

return default_value


def metadata_config() -> configparser:
"""Get the INI config parser for the metadata file.
:return: The config parser object.
:rtype: ConfigParser
"""
path = plugin_path("metadata.txt")
config = configparser.ConfigParser()
config.read(path, encoding='utf8')
return config


def version(remove_v_prefix=True) -> str:
"""Return the version defined in metadata.txt."""
v = metadata_config()["general"]["version"]
if v.startswith("v") and remove_v_prefix:
v = v[1:]
return v

0 comments on commit 01c6946

Please sign in to comment.