@@ -391,6 +391,108 @@ def export(self):
391391 exporter .close ()
392392
393393
394+ @patch ("httpx.Client" )
395+ def test_backend_span_exporter_truncates_large_input_for_openai_tracing (mock_client ):
396+ class DummyItem :
397+ tracing_api_key = None
398+
399+ def __init__ (self ):
400+ self .exported_payload : dict [str , Any ] = {
401+ "object" : "trace.span" ,
402+ "span_data" : {
403+ "type" : "generation" ,
404+ "input" : "x" * (BackendSpanExporter ._OPENAI_TRACING_MAX_FIELD_BYTES + 5000 ),
405+ },
406+ }
407+
408+ def export (self ):
409+ return self .exported_payload
410+
411+ mock_response = MagicMock ()
412+ mock_response .status_code = 200
413+ mock_client .return_value .post .return_value = mock_response
414+
415+ exporter = BackendSpanExporter (api_key = "test_key" )
416+ item = DummyItem ()
417+ exporter .export ([cast (Any , item )])
418+
419+ sent_payload = mock_client .return_value .post .call_args .kwargs ["json" ]["data" ][0 ]
420+ sent_input = sent_payload ["span_data" ]["input" ]
421+ assert isinstance (sent_input , str )
422+ assert sent_input .endswith (exporter ._OPENAI_TRACING_STRING_TRUNCATION_SUFFIX )
423+ assert exporter ._value_json_size_bytes (sent_input ) <= exporter ._OPENAI_TRACING_MAX_FIELD_BYTES
424+ assert item .exported_payload ["span_data" ]["input" ] != sent_input
425+ exporter .close ()
426+
427+
428+ @patch ("httpx.Client" )
429+ def test_backend_span_exporter_truncates_large_non_string_input_for_openai_tracing (mock_client ):
430+ class DummyItem :
431+ tracing_api_key = None
432+
433+ def __init__ (self ):
434+ self .exported_payload : dict [str , Any ] = {
435+ "object" : "trace.span" ,
436+ "span_data" : {
437+ "type" : "generation" ,
438+ "input" : {
439+ "blob" : "x" * (BackendSpanExporter ._OPENAI_TRACING_MAX_FIELD_BYTES + 5000 )
440+ },
441+ },
442+ }
443+
444+ def export (self ):
445+ return self .exported_payload
446+
447+ mock_response = MagicMock ()
448+ mock_response .status_code = 200
449+ mock_client .return_value .post .return_value = mock_response
450+
451+ exporter = BackendSpanExporter (api_key = "test_key" )
452+ exporter .export ([cast (Any , DummyItem ())])
453+
454+ sent_payload = mock_client .return_value .post .call_args .kwargs ["json" ]["data" ][0 ]
455+ sent_input = sent_payload ["span_data" ]["input" ]
456+ assert isinstance (sent_input , dict )
457+ assert sent_input ["truncated" ] is True
458+ assert sent_input ["original_type" ] == "dict"
459+ assert exporter ._value_json_size_bytes (sent_input ) <= exporter ._OPENAI_TRACING_MAX_FIELD_BYTES
460+ exporter .close ()
461+
462+
463+ @patch ("httpx.Client" )
464+ def test_backend_span_exporter_keeps_large_input_for_custom_endpoint (mock_client ):
465+ class DummyItem :
466+ tracing_api_key = None
467+
468+ def __init__ (self ):
469+ self .exported_payload : dict [str , Any ] = {
470+ "object" : "trace.span" ,
471+ "span_data" : {
472+ "type" : "generation" ,
473+ "input" : "x" * (BackendSpanExporter ._OPENAI_TRACING_MAX_FIELD_BYTES + 5000 ),
474+ },
475+ }
476+
477+ def export (self ):
478+ return self .exported_payload
479+
480+ mock_response = MagicMock ()
481+ mock_response .status_code = 200
482+ mock_client .return_value .post .return_value = mock_response
483+
484+ exporter = BackendSpanExporter (
485+ api_key = "test_key" ,
486+ endpoint = "https://example.com/v1/traces/ingest" ,
487+ )
488+ item = DummyItem ()
489+ exporter .export ([cast (Any , item )])
490+
491+ sent_payload = mock_client .return_value .post .call_args .kwargs ["json" ]["data" ][0 ]
492+ assert sent_payload ["span_data" ]["input" ] == item .exported_payload ["span_data" ]["input" ]
493+ exporter .close ()
494+
495+
394496def test_sanitize_for_openai_tracing_api_keeps_allowed_generation_usage ():
395497 exporter = BackendSpanExporter (api_key = "test_key" )
396498 payload = {
@@ -421,3 +523,34 @@ def test_sanitize_for_openai_tracing_api_skips_non_dict_generation_usage():
421523 }
422524 assert exporter ._sanitize_for_openai_tracing_api (payload ) is payload
423525 exporter .close ()
526+
527+
528+ def test_sanitize_for_openai_tracing_api_keeps_small_input_without_mutation ():
529+ exporter = BackendSpanExporter (api_key = "test_key" )
530+ payload = {
531+ "object" : "trace.span" ,
532+ "span_data" : {
533+ "type" : "generation" ,
534+ "input" : "short input" ,
535+ "usage" : {"input_tokens" : 1 },
536+ },
537+ }
538+ assert exporter ._sanitize_for_openai_tracing_api (payload ) is payload
539+ exporter .close ()
540+
541+
542+ def test_truncate_string_for_json_limit_returns_original_when_within_limit ():
543+ exporter = BackendSpanExporter (api_key = "test_key" )
544+ value = "hello"
545+ max_bytes = exporter ._value_json_size_bytes (value )
546+ assert exporter ._truncate_string_for_json_limit (value , max_bytes ) == value
547+ exporter .close ()
548+
549+
550+ def test_truncate_string_for_json_limit_returns_empty_when_suffix_too_large ():
551+ exporter = BackendSpanExporter (api_key = "test_key" )
552+ max_bytes = (
553+ exporter ._value_json_size_bytes (exporter ._OPENAI_TRACING_STRING_TRUNCATION_SUFFIX ) - 1
554+ )
555+ assert exporter ._truncate_string_for_json_limit ("x" * 100 , max_bytes ) == ""
556+ exporter .close ()
0 commit comments