Skip to content

Improved telemetry by adding anonymous usage reports - #4687

Open
gabriel-bolbotina wants to merge 21 commits into
masterfrom
prototype/telemetry
Open

gabriel-bolbotina wants to merge 21 commits into
masterfrom
prototype/telemetry

Conversation

@gabriel-bolbotina

@gabriel-bolbotina gabriel-bolbotina commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

This enhancement has been done to better understand our app and which features are valuable to our users, as well as continuing on improving the experience overall. It also helps prioritise development and simplifies UX/ UI design. It is an OPT-OUT enhancement that stores no personal data.

Summary of changes:

  • if enabled, the app now sends a weekly anonymous usage snapshot to our telemetry endpoint
  • all analytics logic lives in main.cpp as a static function trySubmitUsageSnapshot + signal connections for each feature that is analysed
  • Added OPT-OUT button in the app settings, which removes all the analysed data
  • on app install each devices receives a telemetry id, that is not linked to device UUID or user account
  • no username, no file paths nor other personal information are being stored

Temporary parts of code for testing:

  • snapshot submission button
  • always showing the anonymous telemetry dialog upon opening the app

Remaining things to be added/ decided:

  • external GNSS device list (for anonymised telemetry) -> v2?
  • increase and monitor the ping interval for internet connection

Resolves: https://github.com/MerginMaps/product/issues/127

@gabriel-bolbotina gabriel-bolbotina changed the title Add anonymous weekly usage telemetry Improved telemetry by adding anonymous usage reports Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38541450983

Coverage decreased (-0.7%) to 58.842%

Details

  • Coverage decreased (-0.7%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 319 coverage regressions across 6 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

319 previously-covered lines in 6 files lost coverage.

File Lines Losing Coverage Coverage
mm/app/main.cpp 136 37.86%
mm/app/appsettings.cpp 90 59.47%
mm/app/activeproject.cpp 42 70.77%
mm/app/mapsketchingcontroller.cpp 32 0.0%
mm/app/ios/iosutils.cpp 18 15.63%
mm/app/layerfeaturesmodel.cpp 1 81.77%

Coverage Stats

Coverage Status
Relevant Lines: 16031
Covered Lines: 9433
Line Coverage: 58.84%
Coverage Strength: 92.46 hits per line

💛 - Coveralls

@gabriel-bolbotina gabriel-bolbotina self-assigned this Aug 28, 2026
@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 72451 dmg Expires: 26/11/2026 #7245
linux Build 📬 Mergin Maps 72711 x86_64 Expires: 26/11/2026 #7271
win64 Build 📬 Mergin Maps 64471 win64 Expires: 26/11/2026 #6447
Android Build 📬 Mergin Maps 855511 APK [armeabi-v7a] Expires: 26/11/2026 #8555
📬 Mergin Maps 855511 APK [armeabi-v7a] Google Play Store #8555
Android Build 📬 Mergin Maps 855551 APK [arm64-v8a] Expires: 26/11/2026 #8555
📬 Mergin Maps 855551 APK [arm64-v8a] Google Play Store #8555
iOS Build 📬 Build number: 26.08.949711 #9497

@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 72471 dmg Expires: 26/11/2026 #7247
linux Build 📬 Mergin Maps 72731 x86_64 Expires: 26/11/2026 #7273
win64 Build 📬 Mergin Maps 64491 win64 Expires: 26/11/2026 #6449
Android Build 📬 Mergin Maps 855751 APK [arm64-v8a] Expires: 26/11/2026 #8557
📬 Mergin Maps 855751 APK [arm64-v8a] Google Play Store #8557
Android Build 📬 Mergin Maps 855711 APK [armeabi-v7a] Expires: 26/11/2026 #8557
📬 Mergin Maps 855711 APK [armeabi-v7a] Google Play Store #8557
iOS Build 📬 Build number: 26.08.949911 #9499

Comment thread app/main.cpp Outdated
gabriel-bolbotina and others added 15 commits September 23, 2026 16:00
Add usageReportEnabled property to AppSettings with QSettings
persistence under usage_report/enabled. On opt-out, all accumulated
usage_report/* data is cleared but the enabled flag is preserved.

Wire the toggle to the settings page UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add USAGE_REPORT_KEY CMake variable and mmconfig.h.in define.

Add trySubmitUsageSnapshot() function in main.cpp that:
- checks if a weekly snapshot is due
- collects static data (device, app, server info)
- merges accumulated dynamic data from QSettings
- sends a single HTTP POST to the EU endpoint
- resets dynamic data on success, silently ignores errors

Called once after app initialization. No signal connections yet —
dynamic data accumulation will be wired in the next commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire feature class signals in main.cpp to lambdas that write usage
data to QSettings under usage_report/data/*.

Signals connected:
- FilterController::hasFiltersActivatedChanged → filtering
- AutosyncController::projectSyncRequested → autosync
- ProjectWizard::projectCreated → created_project
- AndroidUtils/IosUtils::photoCaptured/photoFromGallery → image counts
- MerginUserInfo::activeWorkspaceChanged → workspace_switches
- PositionKit::positionProviderChanged → external GPS tracking
- ActiveProject::projectRoleChanged → highest_role
- ActiveProject::loadingStarted/projectReloaded → project load time

New signals added:
- MapSketchingController::sketched() for map sketching detection
- AndroidUtils/IosUtils::photoCaptured()/photoFromGallery() to
  distinguish camera capture from gallery attachment

Connectivity ping timer: HEAD request every 5 minutes, counts
success/failure in QSettings.

Note: map measuring, map sketching, photo sketching, reuse last
value, and bulk editing are QML-instantiated objects — these will
be tracked via AppSettings helpers in the next commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add trackUsageFeature() and incrementUsageCounter() to AppSettings
for features whose C++ controllers are QML-instantiated per-use
objects with no persistent instance to connect to in main.cpp.

Tracked from QML:
- map_measuring (MMMapController)
- map_sketching (MMMapController)
- photo_sketching (MMFormPhotoSketchingPageDialog)
- reuse_last_value (MMFormStackController, on form save)
- bulk_editing (main.qml, on multi-edit with >1 feature)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added a temporary endpoint
Remaned distinct_id with device_id
Added layer and feature search analytics
Updated filtering analytics
@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 73251 dmg Expires: 22/12/2026 #7325
linux Build 📬 Mergin Maps 73511 x86_64 Expires: 22/12/2026 #7351
win64 Build 📬 Mergin Maps 65271 win64 Expires: 22/12/2026 #6527
Android Build 📬 Mergin Maps 863751 APK [arm64-v8a] Expires: 22/12/2026 #8637
📬 QR code Google Play Store #8637
Android Build 📬 Mergin Maps 863711 APK [armeabi-v7a] Expires: 22/12/2026 #8637
📬 QR code Google Play Store #8637
iOS Build 📬 Build number: 26.09.957711 #9577

Moved the logic out of main
Small changes in the usage reporter controller
@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 73371 dmg Expires: 24/12/2026 #7337
linux Build 📬 Mergin Maps 73631 x86_64 Expires: 24/12/2026 #7363
win64 Build 📬 Mergin Maps 65391 win64 Expires: 24/12/2026 #6539
Android Build 📬 Mergin Maps 864911 APK [armeabi-v7a] Expires: 24/12/2026 #8649
📬 QR code Google Play Store #8649
Android Build 📬 Mergin Maps 864951 APK [arm64-v8a] Expires: 24/12/2026 #8649
📬 QR code Google Play Store #8649
iOS Build 📬 Build number: 26.09.958911 #9589

@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 73581 dmg Expires: 29/12/2026 #7358
linux Build 📬 Mergin Maps 73841 x86_64 Expires: 29/12/2026 #7384
win64 Build 📬 Mergin Maps 65601 win64 Expires: 29/12/2026 #6560
Android Build 📬 Mergin Maps 867051 APK [arm64-v8a] Expires: 29/12/2026 #8670
📬 QR code Google Play Store #8670
Android Build 📬 Mergin Maps 867011 APK [armeabi-v7a] Expires: 29/12/2026 #8670
📬 QR code Google Play Store #8670
iOS Build 📬 Build number: 26.09.961011 #9610

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build ❌ Build failed or not found. #7383
linux Build ❌ Build failed or not found. #7409
win64 Build 📬 Mergin Maps 65851 win64 Expires: 03/01/2027 #6585
Android Build 📬 Mergin Maps 869511 APK [armeabi-v7a] Expires: 03/01/2027 #8695
📬 QR code Google Play Store #8695
Android Build 📬 Mergin Maps 869551 APK [arm64-v8a] Expires: 03/01/2027 #8695
📬 QR code Google Play Store #8695
iOS Build 📬 Build number: 26.10.963511 #9635

@gabriel-bolbotina
gabriel-bolbotina marked this pull request as ready for review October 12, 2026 12:47
@gabriel-bolbotina
gabriel-bolbotina requested review from xkello and removed request for Withalion October 12, 2026 12:48
Updated connectivity ping endpoint
@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 74101 dmg Expires: 12/01/2027 #7410
linux Build 📬 Mergin Maps 74361 x86_64 Expires: 12/01/2027 #7436
win64 Build 📬 Mergin Maps 66121 win64 Expires: 12/01/2027 #6612
Android Build 📬 Mergin Maps 872211 APK [armeabi-v7a] Expires: 12/01/2027 #8722
📬 QR code Google Play Store #8722
Android Build 📬 Mergin Maps 872251 APK [arm64-v8a] Expires: 12/01/2027 #8722
📬 QR code Google Play Store #8722
iOS Build 📬 Build number: 26.10.966211 #9662

@xkello xkello left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gabriel-bolbotina looks good. Just some small things here and there.

Comment thread app/main.cpp
Comment on lines +648 to +749
// Temporary: immediate snapshot trigger from QML test button
QObject::connect( as, &AppSettings::immediateUsageSnapshotRequested, &usageReportController, &UsageReportController::forceSubmitSnapshot );

// Gather dynamic usage data via signal connections
UsageReportController &urc = usageReportController;

// Auto-sync
QObject::connect( &activeProject, &ActiveProject::autosyncControllerChanged, &lambdaContext, [&urc]( AutosyncController * ac )
{
if ( !ac ) return;
QObject::connect( ac, &AutosyncController::projectSyncRequested, ac, [&urc]( SyncOptions::RequestOrigin origin )
{
if ( origin == SyncOptions::AutomaticRequest )
urc.trackFeature( QStringLiteral( "autosync" ) );
} );
} );

// Created project
QObject::connect( &pw, &ProjectWizard::projectCreated, &lambdaContext, [&urc]( const QString &, const QString & )
{
urc.trackFeature( QStringLiteral( "created_project" ) );
} );

// Photo captured vs attached
QObject::connect( &androidUtils, &AndroidUtils::photoCaptured, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "captured_images" ) );
} );
QObject::connect( &androidUtils, &AndroidUtils::photoFromGallery, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "attached_images" ) );
} );
QObject::connect( &iosUtils, &IosUtils::photoCaptured, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "captured_images" ) );
} );
QObject::connect( &iosUtils, &IosUtils::photoFromGallery, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "attached_images" ) );
} );

// Workspace switches
QObject::connect( ma->userInfo(), &MerginUserInfo::activeWorkspaceChanged, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "workspace_switches" ) );
} );

// External GPS provider
QObject::connect( pk, &PositionKit::positionProviderChanged, &lambdaContext, [&urc]( AbstractPositionProvider * provider )
{
if ( !provider ) return;

// simulated provider has type "internal", so check it by id first
const QString type = provider->type();
QString connectionType;
if ( provider->id() == QLatin1String( "simulated" ) )
connectionType = QStringLiteral( "mock" );
else if ( type == QLatin1String( "external_bt" ) )
connectionType = QStringLiteral( "bluetooth" );
else if ( type == QLatin1String( "external_ip" ) )
connectionType = QStringLiteral( "network" );
else if ( type == QLatin1String( "external_trimble" ) )
connectionType = QStringLiteral( "trimble" );
else
return; // internal

urc.trackFeature( QStringLiteral( "external_gps" ) );
urc.setData( QStringLiteral( "external_connection_type" ), connectionType );
urc.setData( QStringLiteral( "external_brand" ), UsageReportController::externalProviderBrand( type, provider->name() ) );
} );

// Highest project role
QObject::connect( &activeProject, &ActiveProject::projectRoleChanged, &lambdaContext, [&urc, as, &activeProject]()
{
if ( !as->usageReportEnabled() ) return;

const QString role = activeProject.projectRole();
auto roleRank = []( const QString & r ) -> int
{
if ( r == QLatin1String( "owner" ) ) return 5;
if ( r == QLatin1String( "admin" ) ) return 4;
if ( r == QLatin1String( "writer" ) ) return 3;
if ( r == QLatin1String( "reader" ) ) return 2;
if ( r == QLatin1String( "guest" ) ) return 1;
return 0;
};

QSettings s;
const QString current = s.value( QStringLiteral( "usage_report/data/highest_role" ) ).toString();
if ( roleRank( role ) > roleRank( current ) )
urc.setData( QStringLiteral( "highest_role" ), role );
} );

// Project load time
QObject::connect( &activeProject, &ActiveProject::loadingStarted, &lambdaContext, [&urc]()
{
urc.startLoadTimer();
} );

urc.startPingTimer();

QObject::connect( &activeProject, &ActiveProject::projectReloaded, &lambdaContext, [merginApi = ma.get(), &activeProject, &urc]()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be moved to the usagereportcontroller.cpp?

Comment thread app/main.cpp
Comment on lines +727 to +732
if ( r == QLatin1String( "owner" ) ) return 5;
if ( r == QLatin1String( "admin" ) ) return 4;
if ( r == QLatin1String( "writer" ) ) return 3;
if ( r == QLatin1String( "reader" ) ) return 2;
if ( r == QLatin1String( "guest" ) ) return 1;
return 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the role editor. Is that intentional?

Comment on lines +45 to +49
onSearchTextChanged: {
featuresModel.searchExpression = searchBar.text
if ( searchBar.text.length > 0 )
AppSettings.trackUsageFeature( "features_search" )
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this write on every keystroke? Could probably work the same with writing once per search.

QSettings s;
const QString fullKey = QStringLiteral( "usage_report/data/" ) + key;
s.setValue( fullKey, s.value( fullKey, 0 ).toInt() + amount );
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't these do the same as trackUsageFeature and incrementUsageCounter in appsettings.cpp? Could we expose them and drop the appsettings version?

Comment thread app/activeproject.cpp
mAutosyncController.reset();
}

emit autosyncControllerChanged( mAutosyncController.get() );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signal wasn't emitted before so it could have some unwanted effects outside of telemetry. Might need some special attention when testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants