@@ -91,6 +91,17 @@ describe("TelemetryService", () => {
9191 } ) ;
9292 } ) ;
9393
94+ it ( "leaves top-level logs uncorrelated" , ( ) => {
95+ h . service . log ( "plain" ) ;
96+ h . service . logError ( "plain.error" , new Error ( "nope" ) ) ;
97+
98+ const [ log , logError ] = h . sink . events ;
99+ expect ( log . traceId ) . toBeUndefined ( ) ;
100+ expect ( log . parentEventId ) . toBeUndefined ( ) ;
101+ expect ( logError . traceId ) . toBeUndefined ( ) ;
102+ expect ( logError . parentEventId ) . toBeUndefined ( ) ;
103+ } ) ;
104+
94105 describe ( "trace" , ( ) => {
95106 it ( "returns the wrapped value and records durationMs on success" , async ( ) => {
96107 vi . useFakeTimers ( ) ;
@@ -165,6 +176,78 @@ describe("TelemetryService", () => {
165176 } ) ;
166177 } ) ;
167178
179+ it ( "span.log emits a correlated point-in-time event" , async ( ) => {
180+ await h . service . trace ( "op" , ( span ) => {
181+ span . log ( "checkpoint" , { ready : true } , { count : 1 } ) ;
182+ return Promise . resolve ( ) ;
183+ } ) ;
184+
185+ const [ log , parent ] = h . sink . events ;
186+ expect ( log ) . toMatchObject ( {
187+ eventName : "op.checkpoint" ,
188+ properties : { ready : "true" } ,
189+ measurements : { count : 1 } ,
190+ } ) ;
191+ expect ( log . eventId ) . not . toBe ( parent . eventId ) ;
192+ expect ( log . traceId ) . toBe ( parent . traceId ) ;
193+ expect ( log . parentEventId ) . toBe ( parent . eventId ) ;
194+ expect ( log . properties . result ) . toBeUndefined ( ) ;
195+ expect ( log . measurements . durationMs ) . toBeUndefined ( ) ;
196+ } ) ;
197+
198+ it ( "span.logError emits a correlated error log" , async ( ) => {
199+ await h . service . trace ( "op" , ( span ) => {
200+ span . logError (
201+ "failed" ,
202+ new TypeError ( "nope" ) ,
203+ { attempt : 1 } ,
204+ { retries : 2 } ,
205+ ) ;
206+ return Promise . resolve ( ) ;
207+ } ) ;
208+
209+ const [ log , parent ] = h . sink . events ;
210+ expect ( log ) . toMatchObject ( {
211+ eventName : "op.failed" ,
212+ properties : { attempt : "1" } ,
213+ measurements : { retries : 2 } ,
214+ error : { message : "nope" , type : "TypeError" } ,
215+ } ) ;
216+ expect ( log . traceId ) . toBe ( parent . traceId ) ;
217+ expect ( log . parentEventId ) . toBe ( parent . eventId ) ;
218+ expect ( log . measurements . durationMs ) . toBeUndefined ( ) ;
219+ } ) ;
220+
221+ it ( "child span logs point at the child span" , async ( ) => {
222+ await h . service . trace ( "op" , async ( span ) => {
223+ await span . phase ( "phase" , ( childSpan ) => {
224+ childSpan . log ( "checkpoint" ) ;
225+ childSpan . logError ( "oops" , new Error ( "boom" ) ) ;
226+ return Promise . resolve ( ) ;
227+ } ) ;
228+ } ) ;
229+
230+ const [ log , logError , child , parent ] = h . sink . events ;
231+ expect ( log . eventName ) . toBe ( "op.phase.checkpoint" ) ;
232+ expect ( log . traceId ) . toBe ( parent . traceId ) ;
233+ expect ( log . parentEventId ) . toBe ( child . eventId ) ;
234+ expect ( logError . eventName ) . toBe ( "op.phase.oops" ) ;
235+ expect ( logError . traceId ) . toBe ( parent . traceId ) ;
236+ expect ( logError . parentEventId ) . toBe ( child . eventId ) ;
237+ expect ( logError . error ) . toMatchObject ( { message : "boom" } ) ;
238+ expect ( child . parentEventId ) . toBe ( parent . eventId ) ;
239+ } ) ;
240+
241+ it ( "span log names containing '.' are sanitized like phase names" , async ( ) => {
242+ await h . service . trace ( "op" , ( span ) => {
243+ span . log ( "bad.name" ) ;
244+ return Promise . resolve ( ) ;
245+ } ) ;
246+
247+ const [ log ] = h . sink . events ;
248+ expect ( log . eventName ) . toBe ( "op.bad_name" ) ;
249+ } ) ;
250+
168251 it ( "flat traces (no phases) emit a single event with a fresh traceId" , async ( ) => {
169252 await h . service . trace ( "a" , ( ) => Promise . resolve ( 1 ) ) ;
170253 await h . service . trace ( "b" , ( ) => Promise . resolve ( 2 ) ) ;
@@ -197,15 +280,15 @@ describe("TelemetryService", () => {
197280 expect ( phase2 . traceId ) . toBe ( parent . traceId ) ;
198281 } ) ;
199282
200- it ( "phase children carry parentEventId pointing at the parent's eventId; logs do not" , async ( ) => {
283+ it ( "phase children carry parentEventId pointing at the parent's eventId; top-level logs do not" , async ( ) => {
201284 h . service . log ( "plain" ) ;
202285 await h . service . trace ( "op" , async ( span ) => {
203286 await span . phase ( "p1" , ( ) => Promise . resolve ( ) ) ;
204287 await span . phase ( "p2" , ( ) => Promise . resolve ( ) ) ;
205288 } ) ;
206289
207290 const [ plain , p1 , p2 , parent ] = h . sink . events ;
208- // log/logError/time events have no parentEventId field.
291+ // Top-level log/logError events have no parentEventId field.
209292 expect ( plain . parentEventId ) . toBeUndefined ( ) ;
210293 // Parent (root) has no parentEventId either.
211294 expect ( parent . parentEventId ) . toBeUndefined ( ) ;
@@ -323,6 +406,35 @@ describe("TelemetryService", () => {
323406 ) ;
324407 } ) ;
325408
409+ it ( "drops span logs called after emit" , async ( ) => {
410+ let escapedSpan : Span | undefined ;
411+ await h . service . trace ( "op" , ( span ) => {
412+ escapedSpan = span ;
413+ return Promise . resolve ( ) ;
414+ } ) ;
415+
416+ escapedSpan ?. log ( "late" ) ;
417+ escapedSpan ?. logError ( "late_error" , new Error ( "ignored" ) ) ;
418+
419+ expect ( h . sink . events ) . toHaveLength ( 1 ) ;
420+ } ) ;
421+
422+ it ( "runs phase fns called after emit but emits no phase event" , async ( ) => {
423+ let escapedSpan : Span | undefined ;
424+ await h . service . trace ( "op" , ( span ) => {
425+ escapedSpan = span ;
426+ return Promise . resolve ( ) ;
427+ } ) ;
428+
429+ const result = await escapedSpan ?. phase ( "late" , ( childSpan ) => {
430+ childSpan . log ( "ignored" ) ;
431+ return Promise . resolve ( "ran" ) ;
432+ } ) ;
433+
434+ expect ( result ) . toBe ( "ran" ) ;
435+ expect ( h . sink . events ) . toHaveLength ( 1 ) ;
436+ } ) ;
437+
326438 it ( "markAborted flips result to 'aborted' on normal return" , async ( ) => {
327439 await h . service . trace ( "op" , ( span ) => {
328440 span . markAborted ( ) ;
@@ -456,12 +568,20 @@ describe("TelemetryService", () => {
456568 h . service . log ( "a" ) ;
457569 h . service . logError ( "b" , new Error ( "ignored" ) ) ;
458570
459- expect ( await h . service . trace ( "c" , ( ) => Promise . resolve ( 42 ) ) ) . toBe ( 42 ) ;
571+ expect (
572+ await h . service . trace ( "c" , ( span ) => {
573+ span . log ( "ignored" ) ;
574+ span . logError ( "ignored_error" , new Error ( "ignored" ) ) ;
575+ return Promise . resolve ( 42 ) ;
576+ } ) ,
577+ ) . toBe ( 42 ) ;
460578
461579 const traceResult = await h . service . trace ( "d" , async ( span ) => {
462- const phaseValue = await span . phase ( "p" , ( ) =>
463- Promise . resolve ( "inner" ) ,
464- ) ;
580+ const phaseValue = await span . phase ( "p" , ( childSpan ) => {
581+ childSpan . log ( "ignored" ) ;
582+ childSpan . logError ( "ignored_error" , new Error ( "ignored" ) ) ;
583+ return Promise . resolve ( "inner" ) ;
584+ } ) ;
465585 expect ( phaseValue ) . toBe ( "inner" ) ;
466586 return "outer" ;
467587 } ) ;
@@ -571,9 +691,12 @@ describe("TelemetryService", () => {
571691 it ( "trace and span.phase complete normally when the sink throws" , async ( ) => {
572692 const service = makeService ( [ throwingSink ] ) ;
573693 const result = await service . trace ( "op" , async ( span ) => {
574- const phaseValue = await span . phase ( "p" , ( ) =>
575- Promise . resolve ( "phase" ) ,
576- ) ;
694+ span . log ( "checkpoint" ) ;
695+ span . logError ( "failure" , new Error ( "telemetry-error" ) ) ;
696+ const phaseValue = await span . phase ( "p" , ( childSpan ) => {
697+ childSpan . log ( "checkpoint" ) ;
698+ return Promise . resolve ( "phase" ) ;
699+ } ) ;
577700 expect ( phaseValue ) . toBe ( "phase" ) ;
578701 return "trace" ;
579702 } ) ;
0 commit comments