Skip to content

Commit 2f4be09

Browse files
feat(updater): background check + telemetry funnel (#450, #451)
Add silent startup checks with 24h rate limiting, portable-only gating, update toast, and --no-update-check. Centralize privacy-safe Sentry breadcrumbs in UpdaterTelemetry and document funnel queries. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 797e2d7 commit 2f4be09

13 files changed

Lines changed: 528 additions & 53 deletions

docs/AUTO_UPDATER_DESIGN.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,27 @@ Version comparison reuses `UpdateVersion::compare()` (#442) — normalises optio
130130
| [#442](https://github.com/fernandotonon/QtMeshEditor/issues/442) | Semver compare — **done** (`UpdateVersion`) |
131131
| [#443](https://github.com/fernandotonon/QtMeshEditor/issues/443) | Wire `InstallFlavor` into UX |
132132
| [#444–445](https://github.com/fernandotonon/QtMeshEditor/issues/444) | Download + verify pipeline — **in progress** |
133-
| [#446–448](https://github.com/fernandotonon/QtMeshEditor/issues/446) | Relauncher + per-platform install |
134-
| **New PR** | `deploy.yml` minisign signing (section above) |
133+
| [#446–448](https://github.com/fernandotonon/QtMeshEditor/issues/446) | Relauncher + per-platform install — **done** |
134+
| [#450–451](https://github.com/fernandotonon/QtMeshEditor/issues/450) | Background check + Sentry funnel breadcrumbs |
135+
136+
---
137+
138+
## Sentry funnel (ops)
139+
140+
Breadcrumbs use the `updater.*` category prefix. Payloads intentionally exclude URLs, filesystem paths, and artifact filenames — only version strings, channel, enum-like error classes, and progress percent.
141+
142+
Example Discover queries (self-hosted Sentry):
143+
144+
| Funnel step | Query |
145+
|-------------|-------|
146+
| Background checks started | `message.category:updater.background.start` |
147+
| Updates found (background) | `message.category:updater.background.available` |
148+
| User opened dialog | `message.category:updater.dialog.state update_available` |
149+
| Download finished | `message.category:updater.download.complete` |
150+
| Verify OK | `message.category:updater.verify.success` |
151+
| Install relaunch | `message.category:updater.install.relaunch` |
152+
153+
Session opt-out: `--no-update-check` disables background checks; `--no-telemetry` / Sentry consent disables all breadcrumbs via `UpdaterTelemetry::breadcrumb()`.
135154

136155
---
137156

qml/UpdateToast.qml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import QtQuick
2+
import QtQuick.Controls
3+
import QtQuick.Window
4+
import PropertiesPanel 1.0
5+
6+
import Updater 1.0
7+
8+
Window {
9+
id: toast
10+
width: 340
11+
height: 72
12+
flags: Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint
13+
color: "transparent"
14+
visible: false
15+
16+
property string versionText: ""
17+
18+
signal viewUpdateClicked()
19+
20+
function showForVersion(version) {
21+
versionText = version
22+
reposition()
23+
visible = true
24+
show()
25+
raise()
26+
dismissTimer.restart()
27+
}
28+
29+
function reposition() {
30+
const screen = toast.screen
31+
if (!screen)
32+
return
33+
const margin = 16
34+
x = screen.virtualX + screen.width - width - margin
35+
y = screen.virtualY + screen.height - height - margin
36+
}
37+
38+
onScreenChanged: reposition
39+
40+
Timer {
41+
id: dismissTimer
42+
interval: 12000
43+
onTriggered: toast.close()
44+
}
45+
46+
Rectangle {
47+
anchors.fill: parent
48+
radius: 6
49+
color: PropertiesPanelController.panelColor
50+
border.color: PropertiesPanelController.borderColor
51+
border.width: 1
52+
53+
RowLayout {
54+
anchors.fill: parent
55+
anchors.margins: 12
56+
spacing: 12
57+
58+
Text {
59+
Layout.fillWidth: true
60+
wrapMode: Text.WordWrap
61+
color: PropertiesPanelController.textColor
62+
font.pixelSize: 12
63+
text: toast.versionText.length > 0
64+
? qsTr("Version %1 is available").arg(toast.versionText)
65+
: qsTr("An update is available")
66+
}
67+
68+
Rectangle {
69+
Layout.preferredWidth: viewLabel.implicitWidth + 16
70+
Layout.preferredHeight: 28
71+
radius: 3
72+
color: viewMouse.containsMouse
73+
? Qt.lighter(PropertiesPanelController.highlightColor, 1.12)
74+
: PropertiesPanelController.highlightColor
75+
Text {
76+
id: viewLabel
77+
anchors.centerIn: parent
78+
text: qsTr("View update")
79+
color: "white"
80+
font.pixelSize: 12
81+
}
82+
MouseArea {
83+
id: viewMouse
84+
anchors.fill: parent
85+
hoverEnabled: true
86+
cursorShape: Qt.PointingHandCursor
87+
onClicked: {
88+
dismissTimer.stop()
89+
UpdaterController.openUpdateDialog()
90+
toast.close()
91+
}
92+
}
93+
}
94+
}
95+
}
96+
}

src/main.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
#include "CLIPipeline.h"
3131
#include "AppConsoleLog.h"
3232
#include "AppLaunchHandler.h"
33+
#ifdef ENABLE_AUTO_UPDATER
34+
#include "updater/UpdaterController.h"
35+
#endif
3336

3437
#ifndef Q_OS_WIN
3538
#include <unistd.h>
@@ -183,6 +186,14 @@ int main(int argc, char *argv[])
183186

184187
setSentrySessionTags(mcpWithGuiMode ? "gui+mcp" : "gui");
185188

189+
#ifdef ENABLE_AUTO_UPDATER
190+
for (int i = 1; i < argc; ++i) {
191+
if (QString::fromUtf8(argv[i]) == QLatin1String("--no-update-check")) {
192+
UpdaterController::setSessionBackgroundChecksDisabled(true);
193+
}
194+
}
195+
#endif
196+
186197
// Register QML types
187198
qmlRegisterSingletonType<MaterialEditorQML>("MaterialEditorQML", 1, 0, "MaterialEditorQML",
188199
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {

src/mainwindow.cpp

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -678,13 +678,13 @@ void MainWindow::initToolBar()
678678

679679
#ifdef ENABLE_AUTO_UPDATER
680680
connect(UpdaterController::instance(), &UpdaterController::showDialogRequested,
681-
this, &MainWindow::showUpdaterDialog);
681+
this, [this](bool runCheck) { showUpdaterDialog(runCheck); });
682+
connect(UpdaterController::instance(), &UpdaterController::backgroundUpdateAvailable,
683+
this, [this](const QString& version) { showUpdateToast(version); });
682684
#ifndef QTMESH_UNIT_TESTS
683-
if (UpdaterController::instance()->checkOnStartup()) {
684-
QTimer::singleShot(3000, this, []() {
685-
UpdaterController::instance()->checkForUpdates();
686-
});
687-
}
685+
QTimer::singleShot(5000, this, []() {
686+
UpdaterController::instance()->checkForUpdatesInBackground();
687+
});
688688
#endif
689689
#endif
690690

@@ -4521,6 +4521,53 @@ void MainWindow::showUpdaterDialog(bool runCheck)
45214521
engine->load(QUrl(QStringLiteral("qrc:/UpdaterDialog/UpdaterDialog.qml")));
45224522
}
45234523

4524+
void MainWindow::showUpdateToast(const QString& version)
4525+
{
4526+
if (m_updateToastEngine) {
4527+
if (auto* toast = m_updateToastWindow) {
4528+
QMetaObject::invokeMethod(toast, "showForVersion", Q_ARG(QVariant, version));
4529+
}
4530+
return;
4531+
}
4532+
4533+
m_updateToastEngine = new QQmlApplicationEngine(this);
4534+
m_updateToastEngine->addImportPath(QStringLiteral("qrc:/"));
4535+
qmlRegisterSingletonType<PropertiesPanelController>(
4536+
"PropertiesPanel", 1, 0, "PropertiesPanelController",
4537+
[](QQmlEngine* eng, QJSEngine*) -> QObject* {
4538+
return PropertiesPanelController::qmlInstance(eng, nullptr);
4539+
});
4540+
qmlRegisterSingletonType<UpdaterController>(
4541+
"Updater", 1, 0, "UpdaterController",
4542+
[](QQmlEngine* eng, QJSEngine*) -> QObject* {
4543+
return UpdaterController::qmlInstance(eng, nullptr);
4544+
});
4545+
4546+
connect(m_updateToastEngine, &QQmlApplicationEngine::objectCreated, this,
4547+
[this, version](QObject* obj, const QUrl&) {
4548+
if (!obj) {
4549+
m_updateToastEngine->deleteLater();
4550+
m_updateToastEngine = nullptr;
4551+
return;
4552+
}
4553+
4554+
m_updateToastWindow = obj;
4555+
if (auto* window = qobject_cast<QQuickWindow*>(obj)) {
4556+
QQuickWindow::setGraphicsApi(QSGRendererInterface::Software);
4557+
connect(window, &QQuickWindow::destroyed, this, [this]() {
4558+
m_updateToastWindow = nullptr;
4559+
if (m_updateToastEngine) {
4560+
m_updateToastEngine->deleteLater();
4561+
m_updateToastEngine = nullptr;
4562+
}
4563+
});
4564+
}
4565+
QMetaObject::invokeMethod(obj, "showForVersion", Q_ARG(QVariant, version));
4566+
});
4567+
4568+
m_updateToastEngine->load(QUrl(QStringLiteral("qrc:/UpdateToast/UpdateToast.qml")));
4569+
}
4570+
45244571
void MainWindow::on_actionVerify_Update_triggered()
45254572
{
45264573
showUpdaterDialog(true);

src/mainwindow.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,11 @@ public slots:
242242

243243
#ifdef ENABLE_AUTO_UPDATER
244244
void showUpdaterDialog(bool runCheck = true);
245+
void showUpdateToast(const QString& version);
245246
QObject* m_updaterWindow = nullptr;
246247
QQmlApplicationEngine* m_updaterEngine = nullptr;
248+
QObject* m_updateToastWindow = nullptr;
249+
QQmlApplicationEngine* m_updateToastEngine = nullptr;
247250
#endif
248251
};
249252

src/qml_resources.qrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
<file alias="UpdaterDialog.qml">../qml/UpdaterDialog.qml</file>
5959
<file alias="UpdaterSettingsPanel.qml">../qml/UpdaterSettingsPanel.qml</file>
6060
</qresource>
61+
<qresource prefix="/UpdateToast">
62+
<file alias="UpdateToast.qml">../qml/UpdateToast.qml</file>
63+
</qresource>
6164
<qresource prefix="/PreferencesDialog">
6265
<file alias="PreferencesDialog.qml">../qml/PreferencesDialog.qml</file>
6366
</qresource>

src/updater/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ set(QTMESH_UPDATER_SOURCES
88
UpdaterWorker.cpp
99
UpdaterController.cpp
1010
UpdaterInstaller.cpp
11+
UpdaterTelemetry.cpp
1112
)
1213

1314
add_library(qtmesh_updater STATIC ${QTMESH_UPDATER_SOURCES})

0 commit comments

Comments
 (0)