Skip to content

Commit

Permalink
[change] Send OS details in all event categories #371
Browse files Browse the repository at this point in the history
- Removed "OS details" as a separate category
- There are only three supported categories - Install, Upgrade and Heartbeat
- Refactored code
- Updated docs and comments

Closes #371

---------

Co-authored-by: Federico Capoano <f.capoano@openwisp.io>
  • Loading branch information
pandafy and nemesifier committed Apr 4, 2024
1 parent 0752812 commit 578afe1
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 143 deletions.
28 changes: 20 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1673,15 +1673,24 @@ database backend to implement a workaround for handling
Collection of Usage Metrics
---------------------------

The openwisp-utils module includes an optional sub-app ``openwisp_utils.measurements``.
This sub-app enables collection of following measurements:
The ``openwisp-utils`` module includes an optional
sub-app ``openwisp_utils.measurements``.

- Installed OpenWISP Version
- Enabled OpenWISP modules: A list of the enabled OpenWISP modules
along with their respective versions
- OS details: Information on the operating system, including its
version, kernel version, and platform
- Whether the event is related to a new installation or an upgrade
This sub-app allows the collection of the following information:

- OpenWISP Version
- List of enabled OpenWISP modules and their version
- Operating System identifier, e.g.:
Linux version, Kernel version, target platform (e.g. x86)
- Installation method, if available, e.g. `ansible-openwisp2
<https://github.com/openwisp/ansible-openwisp2>`_
or `docker-openwisp <https://github.com/openwisp/docker-openwisp>`_

The data above is collected in the following events:

- **Install**: when OpenWISP is installed the first time
- **Upgrade**: when any OpenWISP module is upgraded
- **Heartbeat**: once every 24 hours

We collect data on OpenWISP usage to gauge user engagement, satisfaction,
and upgrade patterns. This informs our development decisions, ensuring
Expand All @@ -1693,6 +1702,9 @@ analytics tool. Clean Insights allows us to responsibly gather and analyze
usage metrics without compromising user privacy. It provides us with the
means to make data-driven decisions while respecting our users' rights and trust.

We have taken great care to ensure no
sensitive or personal data is being tracked.

Quality Assurance Checks
------------------------

Expand Down
14 changes: 11 additions & 3 deletions openwisp_utils/admin_theme/system_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ def get_installed_openwisp_packages():
}


def get_openwisp_version():
def _get_openwisp2_detail(attribute_name, fallback=None):
try:
return import_string('openwisp2.__openwisp_version__')
return import_string(f'openwisp2.{attribute_name}')
except ImportError:
return None
return fallback


def get_openwisp_version():
return _get_openwisp2_detail('__openwisp_version__')


def get_openwisp_installation_method():
return _get_openwisp2_detail('__openwisp_installation_method__', 'unspecified')


def get_enabled_openwisp_modules():
Expand Down
16 changes: 8 additions & 8 deletions openwisp_utils/measurements/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,25 @@ def connect_post_migrate_signal(self):
@classmethod
def post_migrate_receiver(cls, **kwargs):
if getattr(settings, 'DEBUG', False):
# Do not send usage metrics in debug mode
# i.e. when running tests.
# Do not send usage metrics in debug mode.
# This prevents sending metrics from development setups.
return

from .tasks import send_usage_metrics

is_new_install = False
if kwargs.get('plan'):
migration, migration_rolled_back = kwargs['plan'][0]
# If the migration plan includes creating table
# for the ContentType model, then the installation is
# treated as a new installation because that is the
# first database table created by Django.
is_new_install = (
migration_rolled_back is False
and str(migration) == 'contenttypes.0001_initial'
)

# If the migration plan includes creating table
# for the ContentType model, then the installation is
# treated as a new installation.
if is_new_install:
# This is a new installation
send_usage_metrics.delay()
send_usage_metrics.delay(category='Install')
else:
send_usage_metrics.delay(upgrade_only=True)
send_usage_metrics.delay(category='Upgrade')
53 changes: 31 additions & 22 deletions openwisp_utils/measurements/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,45 @@ class Meta:
ordering = ('-created',)

@classmethod
def is_new_installation(cls):
return not cls.objects.exists()

@classmethod
def get_upgraded_modules(cls, current_versions):
def log_module_version_changes(cls, current_versions):
"""
Retrieves a dictionary of upgraded modules based on current versions.
Also updates the OpenwispVersion object with the new versions.
Args:
current_versions (dict): A dictionary containing the current versions of modules.
Returns:
dict: A dictionary containing the upgraded modules and their versions.
Returns a tuple of booleans indicating:
- whether this is a new installation,
- whether any OpenWISP modules has been upgraded.
"""
openwisp_version = cls.objects.first()
if not openwisp_version:
# If no OpenwispVersion object is present,
# it means that this is a new installation and
# we don't need to check for upgraded modules.
cls.objects.create(module_version=current_versions)
return {}
return True, False
# Check which installed modules have been upgraded by comparing
# the currently installed versions in current_versions with the
# versions stored in the OpenwispVersion object. The return value
# is a dictionary of module:version pairs that have been upgraded.
old_versions = openwisp_version.module_version
upgraded_modules = {}
for module, version in current_versions.items():
if module in old_versions and parse_version(
old_versions[module]
) < parse_version(version):
# The OS version does not follow semver,
# therefore it's handled differently.
if module in ['kernel_version', 'os_version', 'hardware_platform']:
if old_versions.get(module) != version:
upgraded_modules[module] = version
elif (
# Check if a new OpenWISP module was enabled
# on an existing installation
module not in old_versions
or (
# Check if an OpenWISP module was upgraded
module in old_versions
and parse_version(old_versions[module]) < parse_version(version)
)
):
upgraded_modules[module] = version
openwisp_version.module_version[module] = version
# Log version changes
if upgraded_modules:
# Save the new versions in a new object
OpenwispVersion.objects.create(
module_version=openwisp_version.module_version
)
return upgraded_modules
OpenwispVersion.objects.create(module_version=current_versions)
return False, True
return False, False
46 changes: 32 additions & 14 deletions openwisp_utils/measurements/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
from celery import shared_task
from openwisp_utils.admin_theme.system_info import (
get_enabled_openwisp_modules,
get_openwisp_installation_method,
get_openwisp_version,
get_os_details,
)

from ..tasks import OpenwispCeleryTask
from ..utils import retryable_request
from .models import OpenwispVersion
from .utils import _get_events, get_openwisp_module_metrics, get_os_detail_metrics
from .utils import _get_events

USER_METRIC_COLLECTION_URL = 'https://analytics.openwisp.io/cleaninsights.php'

Expand Down Expand Up @@ -40,19 +41,36 @@ def post_usage_metrics(events):


@shared_task(base=OpenwispCeleryTask)
def send_usage_metrics(upgrade_only=False):
def send_usage_metrics(category='Heartbeat'):
assert category in ['Install', 'Heartbeat', 'Upgrade']
current_versions = get_enabled_openwisp_modules()
current_versions.update({'OpenWISP Version': get_openwisp_version()})
metrics = []
metrics.extend(get_os_detail_metrics(get_os_details()))
if OpenwispVersion.is_new_installation():
metrics.extend(_get_events('Install', current_versions))
OpenwispVersion.objects.create(module_version=current_versions)
else:
upgraded_modules = OpenwispVersion.get_upgraded_modules(current_versions)
if upgrade_only and not upgraded_modules:
return
metrics.extend(_get_events('Upgrade', upgraded_modules))
if not upgrade_only:
metrics.extend(get_openwisp_module_metrics(current_versions))
current_versions.update(get_os_details())
is_install, is_upgrade = OpenwispVersion.log_module_version_changes(
current_versions
)
# Handle special conditions when the user forgot to execute the migrate
# command, and an install or upgrade operation is detected in the
# Heartbeat event. In this situation, we override the category.
if category == 'Heartbeat':
if is_install:
category = 'Install'
if is_upgrade:
category = 'Upgrade'
elif category == 'Upgrade' and not is_upgrade:
# The task was triggered with "Upgrade" category, but no
# upgrades were detected in the OpenWISP module versions.
# This occurs when the migrate command is executed but
# no OpenWISP python module was upgraded.
# We don't count these as upgrades.
return
elif category == 'Install' and not is_install:
# Similar to above, but for "Install" category
return
metrics = _get_events(category, current_versions)
metrics.extend(
_get_events(
category, {'Installation Method': get_openwisp_installation_method()}
)
)
post_usage_metrics(metrics)

0 comments on commit 578afe1

Please sign in to comment.