Skip to content

Commit d7e77b2

Browse files
fiskusclaude
andcommitted
Follow the settings modules' shape — which exposes a first-run bug
`path_in` was a free function of my own invention. The persisted-settings modules here all do the same thing already: a private `Self::file_path(data_dir)` associated function over a module-level FILE_NAME, with load/save on the type. Adopted that, so identity reads like its siblings. Following the convention exposed a real defect. Every one of those modules calls `create_dir_all` before writing and mine did not — and identity is loaded at main.rs:80, *before* `init_file_logging` at :91, which is the only thing that brings the data directory into being. So on a genuinely fresh install the write failed, `load` returned None, and the first session of every install went unattributed, with the identity appearing only from the second launch. That is precisely the launch every funnel starts from, so the bug would have quietly undercut the metric this unit exists to enable. Fixed by `create_dir_all` in `save`, and pinned by a test that loads from a data directory that does not exist yet — asserting both that an id appears and that the next launch reports the same one. Kept as a departure, now stated next to the convention it departs from: `load` returns Option where the settings return Result-with-defaults, because identity has no meaningful default and a fabricated one is worse than none. just lint 0, cargo fmt --check 0, 301 quilt-sync tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0117548 commit d7e77b2

3 files changed

Lines changed: 100 additions & 62 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ fn main() {
7777
let telemetry = telemetry::Telemetry::new(
7878
&package_info.version,
7979
sinks,
80-
telemetry::install_id::load(&data_dir),
80+
telemetry::InstallId::load(&data_dir),
8181
);
8282

8383
// This is for runtime registering

quilt-sync/src-tauri/src/telemetry/install_id.rs

Lines changed: 98 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -48,60 +48,68 @@ impl InstallId {
4848
pub fn as_str(&self) -> &str {
4949
&self.0
5050
}
51-
}
5251

53-
fn path_in(data_dir: &Path) -> PathBuf {
54-
data_dir.join(FILE_NAME)
55-
}
52+
fn file_path(data_dir: &Path) -> PathBuf {
53+
data_dir.join(FILE_NAME)
54+
}
5655

57-
/// Write `id` so a crash mid-write cannot leave a truncated value.
58-
///
59-
/// Rename within a directory is atomic, so a reader sees either the old file or
60-
/// the whole new one — never half of one. Without this, an interrupted write
61-
/// would leave a partial id that reads as a *different* install forever.
62-
fn persist(path: &Path, id: &str) -> std::io::Result<()> {
63-
let staging = path.with_extension("tmp");
64-
std::fs::write(&staging, id)?;
65-
std::fs::rename(&staging, path)
66-
}
56+
/// Write atomically: temp file + rename.
57+
///
58+
/// `create_dir_all` first because this is the **earliest** write into the
59+
/// data directory — earlier than the logger, which is otherwise what brings
60+
/// it into being. Without it a genuinely fresh install fails to persist and
61+
/// its first session goes unattributed, which is the one launch that starts
62+
/// every funnel.
63+
///
64+
/// Rename within a directory is atomic, so a reader sees either the old file
65+
/// or the whole new one, never half of one. An interrupted write would
66+
/// otherwise leave a partial value that reads as a *different* install
67+
/// forever.
68+
fn save(&self, data_dir: &Path) -> std::io::Result<()> {
69+
std::fs::create_dir_all(data_dir)?;
70+
let path = Self::file_path(data_dir);
71+
let staging = path.with_extension("tmp");
72+
std::fs::write(&staging, &self.0)?;
73+
std::fs::rename(&staging, &path)
74+
}
6775

68-
/// The identity for this install, minting one on first run.
69-
///
70-
/// Returns `None` rather than an unpersisted value, and that distinction is the
71-
/// point: an id that is not on disk would be a *new* id next launch, inflating
72-
/// the install count precisely when disks are unhappy — which is the worst
73-
/// moment to also lose confidence in the metric. Better to report no identity
74-
/// for a run than a false one.
75-
///
76-
/// A read that fails for any reason other than absence yields `None` too, and
77-
/// does **not** mint a replacement: overwriting an existing-but-unreadable id
78-
/// would discard a real install's history to satisfy one run.
79-
pub fn load(data_dir: &Path) -> Option<InstallId> {
80-
let path = path_in(data_dir);
81-
82-
match std::fs::read_to_string(&path) {
83-
Ok(contents) => {
84-
let existing = contents.trim();
85-
if !existing.is_empty() {
86-
return Some(InstallId(existing.to_string()));
76+
/// The identity for this install, minting and persisting one on first run.
77+
///
78+
/// `Option` rather than the `Result`-with-defaults the settings modules
79+
/// return, because identity has no meaningful default: a fabricated one is
80+
/// worse than none. Returning `None` rather than an unpersisted value is the
81+
/// same distinction — an id that is not on disk would be a *new* id next
82+
/// launch, inflating the install count precisely when disks are unhappy,
83+
/// which is the worst moment to also lose confidence in the metric.
84+
///
85+
/// A read that fails for any reason other than absence yields `None` too, and
86+
/// does **not** mint a replacement: overwriting an existing-but-unreadable id
87+
/// would discard a real install's history to satisfy one run.
88+
pub fn load(data_dir: &Path) -> Option<Self> {
89+
match std::fs::read_to_string(Self::file_path(data_dir)) {
90+
Ok(contents) => {
91+
let existing = contents.trim();
92+
if !existing.is_empty() {
93+
return Some(Self(existing.to_string()));
94+
}
95+
// An empty file carries no history to protect, so it is treated
96+
// as absence and replaced.
97+
debug!("install id file is empty, minting a replacement");
98+
}
99+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
100+
Err(err) => {
101+
warn!("could not read install id, reporting none this run: {err}");
102+
return None;
87103
}
88-
// An empty file carries no history to protect, so it is treated as
89-
// absence and replaced.
90-
debug!("install id file is empty, minting a replacement");
91104
}
92-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
93-
Err(err) => {
94-
warn!("could not read install id, reporting none this run: {err}");
105+
106+
let minted = Self(Uuid::new_v4().to_string());
107+
if let Err(err) = minted.save(data_dir) {
108+
warn!("could not persist install id, reporting none this run: {err}");
95109
return None;
96110
}
111+
Some(minted)
97112
}
98-
99-
let minted = Uuid::new_v4().to_string();
100-
if let Err(err) = persist(&path, &minted) {
101-
warn!("could not persist install id, reporting none this run: {err}");
102-
return None;
103-
}
104-
Some(InstallId(minted))
105113
}
106114

107115
#[cfg(test)]
@@ -114,10 +122,10 @@ mod tests {
114122
fn mints_and_persists_on_first_run() {
115123
let dir = TempDir::new().expect("tempdir");
116124

117-
let first = load(dir.path()).expect("an id on first run");
125+
let first = InstallId::load(dir.path()).expect("an id on first run");
118126

119127
assert!(
120-
path_in(dir.path()).exists(),
128+
InstallId::file_path(dir.path()).exists(),
121129
"the id must be on disk, or the next launch invents a new install"
122130
);
123131
assert!(
@@ -127,14 +135,36 @@ mod tests {
127135
);
128136
}
129137

138+
/// A genuinely fresh install: the data directory does not exist yet, because
139+
/// the identity is loaded before the logger — which is otherwise the thing
140+
/// that creates it. Without `create_dir_all` this returned `None`, so the
141+
/// *first* session of every install went unattributed, and the identity only
142+
/// appeared from the second launch. That is the one launch every funnel starts
143+
/// from.
144+
#[test]
145+
fn mints_on_a_first_run_whose_data_dir_does_not_exist_yet() {
146+
let parent = TempDir::new().expect("tempdir");
147+
let data_dir = parent.path().join("not-created-yet");
148+
assert!(!data_dir.exists(), "the premise of this test");
149+
150+
let id = InstallId::load(&data_dir).expect("an id on a truly fresh install");
151+
152+
assert!(InstallId::file_path(&data_dir).exists());
153+
assert_eq!(
154+
InstallId::load(&data_dir).as_ref(),
155+
Some(&id),
156+
"and it must be the same identity on the next launch"
157+
);
158+
}
159+
130160
/// The property the whole design rests on: the same install reports the same
131161
/// identity every launch.
132162
#[test]
133163
fn is_stable_across_loads() {
134164
let dir = TempDir::new().expect("tempdir");
135165

136-
let first = load(dir.path()).expect("first");
137-
let second = load(dir.path()).expect("second");
166+
let first = InstallId::load(dir.path()).expect("first");
167+
let second = InstallId::load(dir.path()).expect("second");
138168

139169
assert_eq!(first, second);
140170
}
@@ -147,8 +177,8 @@ mod tests {
147177
let other = TempDir::new().expect("tempdir");
148178

149179
assert_ne!(
150-
load(one.path()).expect("one"),
151-
load(other.path()).expect("other")
180+
InstallId::load(one.path()).expect("one"),
181+
InstallId::load(other.path()).expect("other")
152182
);
153183
}
154184

@@ -157,19 +187,22 @@ mod tests {
157187
#[test]
158188
fn trims_a_hand_edited_file() {
159189
let dir = TempDir::new().expect("tempdir");
160-
std::fs::write(path_in(dir.path()), " an-id-with-space\n").expect("write");
190+
std::fs::write(InstallId::file_path(dir.path()), " an-id-with-space\n").expect("write");
161191

162-
assert_eq!(load(dir.path()).expect("id").as_str(), "an-id-with-space");
192+
assert_eq!(
193+
InstallId::load(dir.path()).expect("id").as_str(),
194+
"an-id-with-space"
195+
);
163196
}
164197

165198
/// An empty file carries no history, so it is replaced rather than honoured
166199
/// as an identity of "".
167200
#[test]
168201
fn replaces_an_empty_file() {
169202
let dir = TempDir::new().expect("tempdir");
170-
std::fs::write(path_in(dir.path()), " ").expect("write");
203+
std::fs::write(InstallId::file_path(dir.path()), " ").expect("write");
171204

172-
let id = load(dir.path()).expect("a replacement id");
205+
let id = InstallId::load(dir.path()).expect("a replacement id");
173206

174207
assert!(!id.as_str().is_empty());
175208
assert!(Uuid::parse_str(id.as_str()).is_ok());
@@ -181,10 +214,14 @@ mod tests {
181214
#[test]
182215
fn honours_an_opaque_existing_value() {
183216
let dir = TempDir::new().expect("tempdir");
184-
std::fs::write(path_in(dir.path()), "not-a-uuid-but-still-an-install").expect("write");
217+
std::fs::write(
218+
InstallId::file_path(dir.path()),
219+
"not-a-uuid-but-still-an-install",
220+
)
221+
.expect("write");
185222

186223
assert_eq!(
187-
load(dir.path()).expect("id").as_str(),
224+
InstallId::load(dir.path()).expect("id").as_str(),
188225
"not-a-uuid-but-still-an-install"
189226
);
190227
}
@@ -195,10 +232,11 @@ mod tests {
195232
#[test]
196233
fn reports_none_when_it_cannot_persist() {
197234
let dir = TempDir::new().expect("tempdir");
198-
std::fs::create_dir(path_in(dir.path())).expect("occupy the path with a directory");
235+
std::fs::create_dir(InstallId::file_path(dir.path()))
236+
.expect("occupy the path with a directory");
199237

200238
assert_eq!(
201-
load(dir.path()),
239+
InstallId::load(dir.path()),
202240
None,
203241
"an unpersistable id must be reported as absent, never as a new install"
204242
);

quilt-sync/src-tauri/src/telemetry/mixpanel.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ mod tests {
314314
// Constructed through `load` so the test cannot drift from the real
315315
// shape: nothing else may mint an identity.
316316
let dir = tempfile::TempDir::new().expect("tempdir");
317-
crate::telemetry::install_id::load(dir.path()).expect("an id")
317+
InstallId::load(dir.path()).expect("an id")
318318
}
319319

320320
/// The identity rides on the wire, and it rides on an event that has no

0 commit comments

Comments
 (0)