Skip to content

Commit 14daa72

Browse files
committed
Attribute every telemetry event to the deployment it concerns
Only three of the emitted analytics events said which Quilt deployment they happened against, so no per-stack question could be answered from the data. The host was already handed to the telemetry layer by the list/detail read paths and went nowhere useful: into a set nothing read, and into a Sentry scope tag written on the calling thread. Tracking now takes an EventContext beside the event, so each of the 33 emission sites names the deployment it concerns or explicitly declares that it concerns none. The context is a struct so later dimensions are added in one place rather than onto individual event variants. The three events that declared their own `host` field lose it — one spelling of the property for every event — with the wire form unchanged, so existing reports keep working. The never-emitted ErrorOccurred variant goes too. Package operations receive the package URI the acting surface already renders from and read its catalog, rather than telemetry re-deriving the host from local lineage: attribution then names the deployment the user acted on, costs no I/O, cannot fail, and needs no ordering constraint against an uninstall destroying the lineage a lookup would read. Crash reports get the host through the Sentry client's event hook instead of the scope. Hubs are thread-local and a worker's hub is a snapshot of the process scope taken when that thread first touched Sentry, so the old tag reached crashes on one worker and nowhere else. The hook stamps every outgoing event on any thread, and latest-host-wins replaces the first-sighting guard. Deliberately hostless: app launch, first-run setup, package creation (no remote yet), the local debug/diagnostics actions, and opening a URL in the browser — one command serves catalog links, docs, local paths and mailto:, so its URL's host is not necessarily a deployment.
1 parent f01b0f3 commit 14daa72

20 files changed

Lines changed: 659 additions & 260 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quilt-sync/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
<!-- markdownlint-disable MD013 -->
1010
# Changelog
1111

12+
## [v0.20.0-alpha4] - 2026-07-30
13+
14+
### Changed
15+
16+
- Usage analytics and crash reports now record which Quilt deployment an action concerned, so activity can be read per stack; actions that concern no deployment (app launch, first-run setup, the local debug and diagnostics actions) record none rather than an inherited one
17+
1218
## [v0.20.0-alpha3] - 2026-07-29
1319

1420
### Changed

quilt-sync/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "quilt-sync"
3-
version = "0.20.0-alpha3"
3+
version = "0.20.0-alpha4"
44
authors = ["Quilt Data, Inc."]
55
description = "Cross-platform desktop application for editing Quilt data packages"
66
documentation = "https://docs.quiltdata.com"

quilt-sync/src-tauri/src/commands.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
//! Tauri IPC commands, grouped by domain. Everything is re-exported at
22
//! `commands::*` so `main.rs`'s `generate_handler!` list and other callers
33
//! keep the flat `commands::<name>` paths.
4+
//!
5+
//! # The `uri` argument some commands take but never use
6+
//!
7+
//! A package operation is addressed by its `namespace`; several also accept the
8+
//! package `uri` the calling surface rendered from, and do nothing with it but
9+
//! read its catalog for [telemetry attribution](crate::telemetry::Telemetry::track).
10+
//! It is passed rather than resolved from local lineage on purpose: the catalog
11+
//! the user acted on is what the analytics question asks about, and deriving it
12+
//! here would add I/O and a failure path to every tracked action — including one
13+
//! that would have to run *before* an uninstall destroys the lineage it reads.
14+
//! See [`crate::telemetry::EventContext::for_uri`].
415
516
mod auth;
617
mod commit_data;

quilt-sync/src-tauri/src/commands/auth.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::notify::Notify;
2020
use crate::oauth::OAuthState;
2121
use crate::quilt;
2222
use crate::routes;
23-
use crate::telemetry::{MixpanelEvent, Telemetry, mixpanel::LoginFlow, prelude::*};
23+
use crate::telemetry::{EventContext, MixpanelEvent, Telemetry, mixpanel::LoginFlow, prelude::*};
2424

2525
// ── Login data for Leptos UI ──
2626

@@ -97,7 +97,15 @@ pub async fn erase_auth(
9797
tracing: tauri::State<'_, Telemetry>,
9898
host: String,
9999
) -> Result<String, String> {
100-
tracing.track(MixpanelEvent::AuthErased).await;
100+
// The host is the command's own argument; an unparseable one still logs
101+
// out, it just cannot be attributed.
102+
let host_parsed = Host::from_str(&host).ok();
103+
tracing
104+
.track(
105+
MixpanelEvent::AuthErased,
106+
EventContext::for_host(host_parsed.as_ref()),
107+
)
108+
.await;
101109

102110
let app_handle = app_handle.lock().await;
103111

@@ -329,15 +337,17 @@ pub(super) async fn switch_role_command(
329337
) -> Result<RolesData, Error> {
330338
let host = Host::from_str(host)?;
331339
let info = m.switch_role(&host, role).await?;
332-
let host_name = host.to_string();
333340
// Three caches, not two: the stored credentials (expired by the switch),
334341
// the S3 clients holding their own copy, and the role name the roster
335342
// quotes back at the user. The last two are `adopt_role`'s job.
336343
adopt_role(m, roles, &host, info.clone()).await;
337344
watcher.clear_role_denied_pauses().await;
338345

339346
tracing
340-
.track(MixpanelEvent::RoleSwitched { host: host_name })
347+
.track(
348+
MixpanelEvent::RoleSwitched,
349+
EventContext::for_host(Some(&host)),
350+
)
341351
.await;
342352

343353
Ok(RolesData::from(info))
@@ -384,10 +394,12 @@ async fn login_command(
384394
model::login(m, &host, code).await?;
385395

386396
tracing
387-
.track(MixpanelEvent::UserLoggedIn {
388-
host: host.to_string(),
389-
flow: LoginFlow::Legacy,
390-
})
397+
.track(
398+
MixpanelEvent::UserLoggedIn {
399+
flow: LoginFlow::Legacy,
400+
},
401+
EventContext::for_host(Some(&host)),
402+
)
391403
.await;
392404

393405
Ok(())
@@ -435,7 +447,10 @@ pub async fn login_oauth(
435447
model::open_in_web_browser(&request.authorize_url).map_err(|e| e.to_string())?;
436448

437449
tracing
438-
.track(MixpanelEvent::OAuthLoginInitiated { host: host.clone() })
450+
.track(
451+
MixpanelEvent::OAuthLoginInitiated,
452+
EventContext::for_host(Some(&host_parsed)),
453+
)
439454
.await;
440455

441456
Ok(format!("Opening browser for OAuth login to {host}"))

quilt-sync/src-tauri/src/commands/package_ops.rs

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::Serialize;
99

1010
use quilt_rs::io::remote::WorkflowIntent;
1111

12-
use quilt_uri::Host;
12+
use quilt_uri::{Host, S3PackageUri};
1313

1414
use crate::Error;
1515
use crate::autopull::Watcher;
@@ -19,7 +19,7 @@ use crate::notify::Notify;
1919
use crate::publish_settings::SharedPublishSettings;
2020
use crate::quilt;
2121
use crate::quilt::flow::PullOutcome;
22-
use crate::telemetry::MixpanelEvent;
22+
use crate::telemetry::{EventContext, MixpanelEvent};
2323

2424
async fn package_commit_command(
2525
m: &model::Model,
@@ -38,6 +38,10 @@ async fn package_commit_command(
3838
}
3939

4040
#[tauri::command]
41+
#[allow(
42+
clippy::too_many_arguments,
43+
reason = "three of these are Tauri state injections a caller never passes; the rest are the revision's own fields plus the telemetry context"
44+
)]
4145
pub async fn package_commit(
4246
m: tauri::State<'_, model::Model>,
4347
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
@@ -46,8 +50,14 @@ pub async fn package_commit(
4650
message: String,
4751
metadata: String,
4852
workflow: WorkflowIntent,
53+
uri: Option<S3PackageUri>,
4954
) -> Result<String, String> {
50-
tracing.track(MixpanelEvent::PackageCommitted).await;
55+
tracing
56+
.track(
57+
MixpanelEvent::PackageCommitted,
58+
EventContext::for_uri(uri.as_ref()),
59+
)
60+
.await;
5161

5262
let msg_init = format!("Committing package {namespace}");
5363
let msg_ok = format!("Successfully committed {namespace}");
@@ -71,8 +81,14 @@ pub async fn certify_latest(
7181
m: tauri::State<'_, model::Model>,
7282
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
7383
namespace: String,
84+
uri: Option<S3PackageUri>,
7485
) -> Result<String, String> {
75-
tracing.track(MixpanelEvent::LatestCertified).await;
86+
tracing
87+
.track(
88+
MixpanelEvent::LatestCertified,
89+
EventContext::for_uri(uri.as_ref()),
90+
)
91+
.await;
7692

7793
let msg_init = format!("Certifying latest for {namespace}");
7894
let msg_ok = format!("Successfully certified latest for {namespace}");
@@ -100,8 +116,14 @@ pub async fn reset_local(
100116
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
101117
watcher: tauri::State<'_, Watcher>,
102118
namespace: String,
119+
uri: Option<S3PackageUri>,
103120
) -> Result<String, String> {
104-
tracing.track(MixpanelEvent::LocalReset).await;
121+
tracing
122+
.track(
123+
MixpanelEvent::LocalReset,
124+
EventContext::for_uri(uri.as_ref()),
125+
)
126+
.await;
105127

106128
let msg_init = format!("Resetting local for {namespace}");
107129
let msg_ok = format!("Successfully reset local for {namespace}");
@@ -147,8 +169,14 @@ pub async fn package_push(
147169
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
148170
watcher: tauri::State<'_, Watcher>,
149171
namespace: String,
172+
uri: Option<S3PackageUri>,
150173
) -> Result<String, String> {
151-
tracing.track(MixpanelEvent::PackagePushed).await;
174+
tracing
175+
.track(
176+
MixpanelEvent::PackagePushed,
177+
EventContext::for_uri(uri.as_ref()),
178+
)
179+
.await;
152180

153181
let msg_init = format!("Pushing package {namespace}");
154182

@@ -202,6 +230,7 @@ pub async fn package_publish(
202230
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
203231
watcher: tauri::State<'_, Watcher>,
204232
namespace: String,
233+
uri: Option<S3PackageUri>,
205234
) -> Result<String, String> {
206235
let msg_init = format!("Publishing package {namespace}");
207236
let result = package_publish_command(&m, &settings, &namespace).await;
@@ -210,11 +239,26 @@ pub async fn package_publish(
210239
}
211240

212241
if let Ok((_, outcome)) = &result {
213-
tracing.track(MixpanelEvent::PackagePublished).await;
242+
tracing
243+
.track(
244+
MixpanelEvent::PackagePublished,
245+
EventContext::for_uri(uri.as_ref()),
246+
)
247+
.await;
214248
if matches!(outcome, quilt::PublishOutcome::CommittedAndPushed(_)) {
215-
tracing.track(MixpanelEvent::PackageCommitted).await;
249+
tracing
250+
.track(
251+
MixpanelEvent::PackageCommitted,
252+
EventContext::for_uri(uri.as_ref()),
253+
)
254+
.await;
216255
}
217-
tracing.track(MixpanelEvent::PackagePushed).await;
256+
tracing
257+
.track(
258+
MixpanelEvent::PackagePushed,
259+
EventContext::for_uri(uri.as_ref()),
260+
)
261+
.await;
218262
}
219263

220264
let msg_ok = match &result {
@@ -262,6 +306,10 @@ async fn package_commit_and_push_command(
262306
}
263307

264308
#[tauri::command]
309+
#[allow(
310+
clippy::too_many_arguments,
311+
reason = "three of these are Tauri state injections a caller never passes; the rest are the revision's own fields plus the telemetry context"
312+
)]
265313
pub async fn package_commit_and_push(
266314
m: tauri::State<'_, model::Model>,
267315
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
@@ -270,6 +318,7 @@ pub async fn package_commit_and_push(
270318
message: String,
271319
metadata: String,
272320
workflow: WorkflowIntent,
321+
uri: Option<S3PackageUri>,
273322
) -> Result<String, String> {
274323
let msg_init = format!("Publishing package {namespace}");
275324
let result =
@@ -279,11 +328,26 @@ pub async fn package_commit_and_push(
279328
}
280329

281330
if let Ok((_, outcome)) = &result {
282-
tracing.track(MixpanelEvent::PackagePublished).await;
331+
tracing
332+
.track(
333+
MixpanelEvent::PackagePublished,
334+
EventContext::for_uri(uri.as_ref()),
335+
)
336+
.await;
283337
if matches!(outcome, quilt::PublishOutcome::CommittedAndPushed(_)) {
284-
tracing.track(MixpanelEvent::PackageCommitted).await;
338+
tracing
339+
.track(
340+
MixpanelEvent::PackageCommitted,
341+
EventContext::for_uri(uri.as_ref()),
342+
)
343+
.await;
285344
}
286-
tracing.track(MixpanelEvent::PackagePushed).await;
345+
tracing
346+
.track(
347+
MixpanelEvent::PackagePushed,
348+
EventContext::for_uri(uri.as_ref()),
349+
)
350+
.await;
287351
}
288352

289353
let msg_ok = match &result {
@@ -315,8 +379,14 @@ pub async fn package_pull(
315379
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
316380
watcher: tauri::State<'_, Watcher>,
317381
namespace: String,
382+
uri: Option<S3PackageUri>,
318383
) -> Result<String, String> {
319-
tracing.track(MixpanelEvent::PackagePulled).await;
384+
tracing
385+
.track(
386+
MixpanelEvent::PackagePulled,
387+
EventContext::for_uri(uri.as_ref()),
388+
)
389+
.await;
320390

321391
let msg_init = format!("Pulling package {namespace}");
322392
let msg_ok = format!("Successfully pulled package {namespace}");
@@ -367,8 +437,14 @@ pub async fn package_uninstall(
367437
m: tauri::State<'_, model::Model>,
368438
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
369439
namespace: String,
440+
uri: Option<S3PackageUri>,
370441
) -> Result<String, String> {
371-
tracing.track(MixpanelEvent::PackageUninstalled).await;
442+
tracing
443+
.track(
444+
MixpanelEvent::PackageUninstalled,
445+
EventContext::for_uri(uri.as_ref()),
446+
)
447+
.await;
372448

373449
let msg_init = format!("Uninstalling package {namespace}");
374450
let msg_ok = format!("Successfully uninstalled package {namespace}");
@@ -416,7 +492,14 @@ pub async fn set_remote(
416492
bucket: String,
417493
workflow: WorkflowIntent,
418494
) -> Result<SetRemoteResponse, String> {
419-
tracing.track(MixpanelEvent::RemoteSet).await;
495+
// The origin is this command's own argument: the remote being set.
496+
let origin_host = Host::from_str(&origin).ok();
497+
tracing
498+
.track(
499+
MixpanelEvent::RemoteSet,
500+
EventContext::for_host(origin_host.as_ref()),
501+
)
502+
.await;
420503

421504
// `Notify::new` logs the init line; on success/failure we log explicitly so
422505
// the success payload can be the typed struct rather than a bare string.
@@ -459,7 +542,10 @@ pub async fn package_create(
459542
source: Option<String>,
460543
message: Option<String>,
461544
) -> Result<String, String> {
462-
tracing.track(MixpanelEvent::PackageCreated).await;
545+
// A package created here has no remote yet, so it belongs to no deployment.
546+
tracing
547+
.track(MixpanelEvent::PackageCreated, EventContext::default())
548+
.await;
463549

464550
let msg_init = format!("Creating package {namespace}");
465551
let msg_ok = format!("Successfully created package {namespace}");
@@ -490,7 +576,14 @@ pub async fn package_install_paths(
490576
uri: String,
491577
paths: Vec<String>,
492578
) -> Result<String, String> {
493-
tracing.track(MixpanelEvent::PackageInstalled).await;
579+
// Installing names its package by URI, so the catalog is already in hand.
580+
let target = S3PackageUri::try_from(uri.as_str()).ok();
581+
tracing
582+
.track(
583+
MixpanelEvent::PackageInstalled,
584+
EventContext::for_uri(target.as_ref()),
585+
)
586+
.await;
494587

495588
let msg_init = format!("Installing paths from {uri}");
496589
let msg_ok = format!("Successfully installed {} paths", paths.len());
@@ -539,8 +632,14 @@ pub async fn add_to_quiltignore(
539632
tracing: tauri::State<'_, crate::telemetry::Telemetry>,
540633
namespace: String,
541634
pattern: String,
635+
uri: Option<S3PackageUri>,
542636
) -> Result<String, String> {
543-
tracing.track(MixpanelEvent::QuiltignorePatternAdded).await;
637+
tracing
638+
.track(
639+
MixpanelEvent::QuiltignorePatternAdded,
640+
EventContext::for_uri(uri.as_ref()),
641+
)
642+
.await;
544643

545644
let msg_init = format!("Adding {pattern} to .quiltignore");
546645
let msg_ok = format!("Added {pattern} to .quiltignore");

0 commit comments

Comments
 (0)