Skip to content

Improved telemetry by adding anonymous usage reports - #4687

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

gabriel-bolbotina wants to merge 27 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
  • Update: Analytics logic lives in UsageReportController (counters, role ranking, GNSS provider mapping, weekly snapshot, retry timer). main.cpp only wires feature signals to it.
  • 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
  • added external GNSS device list for anonymised telemetry, it uses a regex to get the device brand from the external GNSS name
  • Before pinging, the app checks the system's reachability. If the device is Disconnected it counts as offline, with no request. In every other state we ping to confirm internet access.

Temporary parts of code for testing:

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

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 42239994214

Coverage decreased (-1.0%) to 59.135%

Details

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

Uncovered Changes

No uncovered changes found.

Coverage Regressions

764 previously-covered lines in 6 files lost coverage.

File Lines Losing Coverage Coverage
mm/core/merginapi.cpp 481 75.11%
mm/app/main.cpp 150 40.13%
mm/app/appsettings.cpp 82 61.33%
mm/app/mapsketchingcontroller.cpp 32 0.0%
mm/app/ios/iosutils.cpp 18 15.63%
mm/app/position/providers/simulatedpositionprovider.cpp 1 91.67%

Coverage Stats

Coverage Status
Relevant Lines: 16366
Covered Lines: 9678
Line Coverage: 59.13%
Coverage Strength: 96.32 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 Resolved
@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

@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
@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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I moved the logic, role ranking, provider mapping, snapshot, timers, to the usage report controller. I suggest that we keep only the connections in main, as the class already has ActiveProject, PositionKit, platform utils and so on, and the controller shouldn't depend on all of them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the PR description to contain this separation

Comment thread app/main.cpp Resolved
Comment thread app/qml/layers/MMFeaturesListPage.qml Resolved
Comment thread app/usagereportcontroller.cpp Resolved
Comment thread app/activeproject.cpp Resolved
@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 74491 dmg Expires: 24/01/2027 #7449
linux Build 📬 Mergin Maps 74741 x86_64 Expires: 24/01/2027 #7474
win64 Build 📬 Mergin Maps 66491 win64 Expires: 24/01/2027 #6649
Android Build 📬 Mergin Maps 875911 APK [armeabi-v7a] Expires: 24/01/2027 #8759
📬 QR code Google Play Store #8759
Android Build 📬 Mergin Maps 875951 APK [arm64-v8a] Expires: 24/01/2027 #8759
📬 QR code Google Play Store #8759
iOS Build 📬 Build number: 26.10.969911 #9699

gabriel-bolbotina and others added 12 commits November 3, 2026 10:55
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
gabriel-bolbotina and others added 10 commits November 3, 2026 10:55
Moved the logic out of main
Small changes in the usage reporter controller
Updated connectivity ping endpoint
Added workspace info
Added retry timer when the snapshot can't be sent
Refactored the usage reporter controller
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 74781 dmg Expires: 01/02/2027 #7478
linux Build 📬 Mergin Maps 75031 x86_64 Expires: 01/02/2027 #7503
win64 Build 📬 Mergin Maps 66781 win64 Expires: 01/02/2027 #6678
Android Build 📬 Mergin Maps 878851 APK [arm64-v8a] Expires: 01/02/2027 #8788
📬 QR code Google Play Store #8788
Android Build 📬 Mergin Maps 878811 APK [armeabi-v7a] Expires: 01/02/2027 #8788
📬 QR code Google Play Store #8788
iOS Build 📬 Build number: 26.11.972911 #9729

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 74791 dmg Expires: 01/02/2027 #7479
linux Build 📬 Mergin Maps 75041 x86_64 Expires: 01/02/2027 #7504
win64 Build 📬 Mergin Maps 66791 win64 Expires: 01/02/2027 #6679
Android Build 📬 Mergin Maps 878911 APK [armeabi-v7a] Expires: 01/02/2027 #8789
📬 QR code Google Play Store #8789
Android Build 📬 Mergin Maps 878951 APK [arm64-v8a] Expires: 01/02/2027 #8789
📬 QR code Google Play Store #8789
iOS Build 📬 Build number: 26.11.973011 #9730

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 74801 dmg Expires: 01/02/2027 #7480
linux Build 📬 Mergin Maps 75051 x86_64 Expires: 01/02/2027 #7505
win64 Build 📬 Mergin Maps 66801 win64 Expires: 01/02/2027 #6680
Android Build 📬 Mergin Maps 879051 APK [arm64-v8a] Expires: 01/02/2027 #8790
📬 QR code Google Play Store #8790
Android Build 📬 Mergin Maps 879011 APK [armeabi-v7a] Expires: 01/02/2027 #8790
📬 QR code Google Play Store #8790
iOS Build 📬 Build number: 26.11.973111 #9731

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