@@ -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 ) ;
0 commit comments