Skip to content

Commit 8bae618

Browse files
Merge pull request #737 from fernandotonon/feat/updater-background-450-451
feat(updater): background check + telemetry funnel (#450, #451)
2 parents 797e2d7 + dcf699f commit 8bae618

14 files changed

Lines changed: 576 additions & 61 deletions

.github/workflows/deploy.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -895,9 +895,9 @@ jobs:
895895
needs: [build-n-cache-assimp-linux, build-n-cache-ogre-linux]
896896
runs-on: ubuntu-latest
897897
# Hard cap on the whole job. The full sweep takes ~12 min normally
898-
# (build + xvfb + ~25 suites + coverage + sonar). Cap at 60 so a
898+
# (build + xvfb + ~25 suites + coverage + sonar). Cap at 90 so a
899899
# cold ccache build on a large PR still finishes under the limit.
900-
timeout-minutes: 60
900+
timeout-minutes: 90
901901
permissions: read-all
902902
env:
903903
LD_LIBRARY_PATH: gcc_64/lib/:/usr/local/lib/:/usr/local/lib/OGRE/:/usr/local/lib/pkgconfig/:/lib/x86_64-linux-gnu/
@@ -1043,8 +1043,21 @@ jobs:
10431043
# (ccache wraps the compiler, so build-wrapper's intercept is
10441044
# transparent to the cache layer).
10451045
1046-
# Run build-wrapper to capture compilation into a named directory
1047-
build-wrapper-linux-x86-64 --out-dir build-wrapper-output make -C build -j$(nproc)
1046+
# Build only test targets (BUILD_QT_MESH_EDITOR=OFF) so the compile
1047+
# phase leaves room for the ~25-suite test sweep within the job cap.
1048+
build-wrapper-linux-x86-64 --out-dir build-wrapper-output \
1049+
make -C build -j$(nproc) \
1050+
UnitTests \
1051+
qtmesh_test_common \
1052+
qtmesh_updater \
1053+
qtmesh_ps1core_stub \
1054+
qtmesh_ps1core_libretro \
1055+
MaterialEditorQML_test \
1056+
MaterialEditorQML_qml_test \
1057+
MaterialEditorQML_perf_test \
1058+
CloudAccountMenuButton_test \
1059+
ProjectPackager_test \
1060+
MaterialEditorQML_qml_test_runner
10481061
10491062
echo "=== ccache statistics ==="
10501063
ccache -s || true

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

src/main.cpp

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

3438
#ifndef Q_OS_WIN
3539
#include <unistd.h>
@@ -183,6 +187,16 @@ int main(int argc, char *argv[])
183187

184188
setSentrySessionTags(mcpWithGuiMode ? "gui+mcp" : "gui");
185189

190+
#ifdef ENABLE_AUTO_UPDATER
191+
for (int i = 1; i < argc; ++i) {
192+
if (QString::fromUtf8(argv[i]) == QLatin1String("--no-update-check")) {
193+
UpdaterController::setSessionBackgroundChecksDisabled(true);
194+
UpdaterTelemetry::breadcrumb(QStringLiteral("updater.background.skip"),
195+
QStringLiteral("session_disabled"));
196+
}
197+
}
198+
#endif
199+
186200
// Register QML types
187201
qmlRegisterSingletonType<MaterialEditorQML>("MaterialEditorQML", 1, 0, "MaterialEditorQML",
188202
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {

src/mainwindow.cpp

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "SentryReporter.h"
3131
#ifdef ENABLE_AUTO_UPDATER
3232
#include "updater/UpdaterController.h"
33+
#include "updater/UpdaterTelemetry.h"
3334
#endif
3435
#include <QDialog>
3536
#include <QProgressDialog>
@@ -678,13 +679,13 @@ void MainWindow::initToolBar()
678679

679680
#ifdef ENABLE_AUTO_UPDATER
680681
connect(UpdaterController::instance(), &UpdaterController::showDialogRequested,
681-
this, &MainWindow::showUpdaterDialog);
682+
this, [this](bool runCheck) { showUpdaterDialog(runCheck); });
683+
connect(UpdaterController::instance(), &UpdaterController::backgroundUpdateAvailable,
684+
this, [this](const QString& version) { showUpdateToast(version); });
682685
#ifndef QTMESH_UNIT_TESTS
683-
if (UpdaterController::instance()->checkOnStartup()) {
684-
QTimer::singleShot(3000, this, []() {
685-
UpdaterController::instance()->checkForUpdates();
686-
});
687-
}
686+
QTimer::singleShot(5000, this, []() {
687+
UpdaterController::instance()->checkForUpdatesInBackground();
688+
});
688689
#endif
689690
#endif
690691

@@ -4521,6 +4522,56 @@ void MainWindow::showUpdaterDialog(bool runCheck)
45214522
engine->load(QUrl(QStringLiteral("qrc:/UpdaterDialog/UpdaterDialog.qml")));
45224523
}
45234524

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