@@ -70,6 +70,10 @@ pub struct SpanComponents {
7070 #[ serde( skip_serializing_if = "Option::is_none" ) ]
7171 pub root_span_id : Option < String > ,
7272
73+ /// Direct parent span IDs for this span.
74+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
75+ pub span_parents : Option < Vec < String > > ,
76+
7377 /// Event data to propagate to child spans (e.g., prompt versions, metadata)
7478 #[ serde( skip_serializing_if = "Option::is_none" ) ]
7579 pub propagated_event : Option < Map < String , Value > > ,
@@ -85,6 +89,7 @@ impl SpanComponents {
8589 row_id : None ,
8690 span_id : None ,
8791 root_span_id : None ,
92+ span_parents : None ,
8893 propagated_event : None ,
8994 }
9095 }
@@ -194,6 +199,12 @@ impl SpanComponents {
194199 if let Some ( ref event) = self . propagated_event {
195200 json_obj. insert ( "propagated_event" . to_string ( ) , Value :: Object ( event. clone ( ) ) ) ;
196201 }
202+ if let Some ( ref span_parents) = self . span_parents {
203+ json_obj. insert (
204+ "span_parents" . to_string ( ) ,
205+ Value :: Array ( span_parents. iter ( ) . cloned ( ) . map ( Value :: String ) . collect ( ) ) ,
206+ ) ;
207+ }
197208
198209 if !json_obj. is_empty ( ) {
199210 let json_str = serde_json:: to_string ( & json_obj) . unwrap ( ) ;
@@ -365,6 +376,25 @@ impl SpanComponents {
365376 root_span_id : json_obj
366377 . remove ( "root_span_id" )
367378 . and_then ( |v| v. as_str ( ) . map ( String :: from) ) ,
379+ span_parents : match json_obj. remove ( "span_parents" ) {
380+ None => None ,
381+ Some ( Value :: Array ( values) ) => Some (
382+ values
383+ . into_iter ( )
384+ . map ( |value| match value {
385+ Value :: String ( value) => Ok ( value) ,
386+ _ => Err ( BraintrustError :: InvalidConfig (
387+ "span_parents must be an array of strings" . to_string ( ) ,
388+ ) ) ,
389+ } )
390+ . collect :: < Result < Vec < _ > > > ( ) ?,
391+ ) ,
392+ Some ( _) => {
393+ return Err ( BraintrustError :: InvalidConfig (
394+ "span_parents must be an array of strings" . to_string ( ) ,
395+ ) )
396+ }
397+ } ,
368398 propagated_event : json_obj
369399 . remove ( "propagated_event" )
370400 . and_then ( |v| v. as_object ( ) . cloned ( ) ) ,
@@ -404,6 +434,7 @@ impl SpanComponents {
404434 compute_object_metadata_args : self . compute_object_metadata_args . clone ( ) ,
405435 span_id,
406436 root_span_id,
437+ span_parents : self . span_parents . clone ( ) ,
407438 propagated_event : self . propagated_event . clone ( ) ,
408439 } )
409440 }
@@ -417,6 +448,7 @@ impl SpanComponents {
417448 compute_object_metadata_args,
418449 span_id,
419450 root_span_id,
451+ span_parents,
420452 propagated_event,
421453 } => Some ( Self {
422454 object_type : * object_type,
@@ -425,6 +457,7 @@ impl SpanComponents {
425457 row_id : None ,
426458 span_id : Some ( span_id. clone ( ) ) ,
427459 root_span_id : Some ( root_span_id. clone ( ) ) ,
460+ span_parents : span_parents. clone ( ) ,
428461 propagated_event : propagated_event. clone ( ) ,
429462 } ) ,
430463 _ => None ,
@@ -529,6 +562,7 @@ mod tests {
529562 components. object_id = Some ( "550e8400-e29b-41d4-a716-446655440000" . to_string ( ) ) ;
530563 components. span_id = Some ( "0123456789abcdef" . to_string ( ) ) ;
531564 components. root_span_id = Some ( "0123456789abcdef0123456789abcdef" . to_string ( ) ) ;
565+ components. span_parents = Some ( vec ! [ "parent-a" . to_string( ) ] ) ;
532566
533567 let encoded = components. to_str ( ) ;
534568 let decoded = SpanComponents :: parse ( & encoded) . unwrap ( ) ;
@@ -537,6 +571,7 @@ mod tests {
537571 assert_eq ! ( decoded. object_id, components. object_id) ;
538572 assert_eq ! ( decoded. span_id, components. span_id) ;
539573 assert_eq ! ( decoded. root_span_id, components. root_span_id) ;
574+ assert_eq ! ( decoded. span_parents, components. span_parents) ;
540575 }
541576
542577 #[ test]
@@ -584,6 +619,7 @@ mod tests {
584619 components. object_id = Some ( "project-123" . to_string ( ) ) ;
585620 components. span_id = Some ( "span-456" . to_string ( ) ) ;
586621 components. root_span_id = Some ( "root-789" . to_string ( ) ) ;
622+ components. span_parents = Some ( vec ! [ "parent-a" . to_string( ) ] ) ;
587623
588624 let mut propagated = Map :: new ( ) ;
589625 propagated. insert (
@@ -600,13 +636,15 @@ mod tests {
600636 object_id,
601637 span_id,
602638 root_span_id,
639+ span_parents,
603640 propagated_event,
604641 ..
605642 } => {
606643 assert_eq ! ( object_type, SpanObjectType :: ProjectLogs ) ;
607644 assert_eq ! ( object_id, Some ( "project-123" . to_string( ) ) ) ;
608645 assert_eq ! ( span_id, "span-456" ) ;
609646 assert_eq ! ( root_span_id, "root-789" ) ;
647+ assert_eq ! ( span_parents, Some ( vec![ "parent-a" . to_string( ) ] ) ) ;
610648 assert ! ( propagated_event. is_some( ) ) ;
611649 let event = propagated_event. unwrap ( ) ;
612650 assert_eq ! (
@@ -629,6 +667,7 @@ mod tests {
629667 span_id : "span-456" . to_string ( ) ,
630668 root_span_id : "root-789" . to_string ( ) ,
631669 compute_object_metadata_args : None ,
670+ span_parents : Some ( vec ! [ "parent-a" . to_string( ) ] ) ,
632671 propagated_event : Some ( propagated) ,
633672 } ;
634673
@@ -638,6 +677,48 @@ mod tests {
638677 assert_eq ! ( components. object_id, Some ( "exp-123" . to_string( ) ) ) ;
639678 assert_eq ! ( components. span_id, Some ( "span-456" . to_string( ) ) ) ;
640679 assert_eq ! ( components. root_span_id, Some ( "root-789" . to_string( ) ) ) ;
680+ assert_eq ! ( components. span_parents, Some ( vec![ "parent-a" . to_string( ) ] ) ) ;
641681 assert ! ( components. propagated_event. is_some( ) ) ;
642682 }
683+
684+ #[ test]
685+ fn test_parse_v3_json_remainder_preserves_span_parents ( ) {
686+ let payload = serde_json:: json!( {
687+ "object_type" : SpanObjectType :: ProjectLogs as u8 ,
688+ "object_id" : "project-123" ,
689+ "row_id" : "row-123" ,
690+ "span_id" : "span-123" ,
691+ "root_span_id" : "root-123" ,
692+ "span_parents" : [ "parent-a" ] ,
693+ } ) ;
694+ let encoded = BASE64 . encode (
695+ [
696+ vec ! [ ENCODING_VERSION_V3 , SpanObjectType :: ProjectLogs as u8 , 0 ] ,
697+ serde_json:: to_vec ( & payload) . unwrap ( ) ,
698+ ]
699+ . concat ( ) ,
700+ ) ;
701+
702+ let decoded = SpanComponents :: parse ( & encoded) . unwrap ( ) ;
703+
704+ assert_eq ! ( decoded. span_parents, Some ( vec![ "parent-a" . to_string( ) ] ) ) ;
705+ }
706+
707+ #[ test]
708+ fn test_parse_rejects_invalid_span_parents ( ) {
709+ let payload = serde_json:: json!( {
710+ "object_type" : SpanObjectType :: ProjectLogs as u8 ,
711+ "span_parents" : [ 123 ] ,
712+ } ) ;
713+ let encoded = BASE64 . encode (
714+ [
715+ vec ! [ ENCODING_VERSION_V4 , SpanObjectType :: ProjectLogs as u8 , 0 ] ,
716+ serde_json:: to_vec ( & payload) . unwrap ( ) ,
717+ ]
718+ . concat ( ) ,
719+ ) ;
720+
721+ let err = SpanComponents :: parse ( & encoded) . unwrap_err ( ) ;
722+ assert ! ( matches!( err, BraintrustError :: InvalidConfig ( _) ) ) ;
723+ }
643724}
0 commit comments