@@ -454,3 +454,328 @@ def test_span_origin_embed(sentry_init, capture_events):
454454
455455 assert event ["contexts" ]["trace" ]["origin" ] == "manual"
456456 assert event ["spans" ][0 ]["origin" ] == "auto.ai.cohere"
457+
458+
459+ # data_collection config, send_default_pii, include_prompts, expect_inputs, expect_outputs
460+ DATA_COLLECTION_CASES = [
461+ pytest .param (
462+ {"gen_ai" : {"inputs" : True , "outputs" : True }},
463+ False ,
464+ False ,
465+ True ,
466+ True ,
467+ id = "gen-ai-inputs-and-outputs-enabled-override-legacy-off" ,
468+ ),
469+ pytest .param (
470+ {"gen_ai" : {"inputs" : False , "outputs" : False }},
471+ True ,
472+ True ,
473+ False ,
474+ False ,
475+ id = "gen-ai-inputs-and-outputs-disabled-override-legacy-on" ,
476+ ),
477+ pytest .param (
478+ {"gen_ai" : {"inputs" : True , "outputs" : False }},
479+ False ,
480+ False ,
481+ True ,
482+ False ,
483+ id = "gen-ai-inputs-enabled-outputs-disabled" ,
484+ ),
485+ pytest .param (
486+ {"gen_ai" : {"inputs" : False , "outputs" : True }},
487+ False ,
488+ False ,
489+ False ,
490+ True ,
491+ id = "gen-ai-outputs-enabled-inputs-disabled" ,
492+ ),
493+ pytest .param (
494+ {"gen_ai" : {}},
495+ False ,
496+ False ,
497+ True ,
498+ True ,
499+ id = "gen-ai-inputs-and-outputs-omitted-default-to-enabled" ,
500+ ),
501+ pytest .param (
502+ None ,
503+ True ,
504+ True ,
505+ True ,
506+ True ,
507+ id = "no-gen-ai-config-legacy-pii-and-include-prompts-enabled" ,
508+ ),
509+ pytest .param (
510+ None ,
511+ False ,
512+ True ,
513+ False ,
514+ False ,
515+ id = "no-gen-ai-config-legacy-pii-disabled" ,
516+ ),
517+ ]
518+
519+
520+ def _init_with_data_collection (
521+ sentry_init , data_collection , send_default_pii , include_prompts , span_streaming
522+ ):
523+ kwargs = dict (
524+ integrations = [CohereIntegration (include_prompts = include_prompts )],
525+ traces_sample_rate = 1.0 ,
526+ send_default_pii = send_default_pii ,
527+ trace_lifecycle = "stream" if span_streaming else "static" ,
528+ )
529+ if data_collection is not None :
530+ kwargs ["_experiments" ] = {"data_collection" : data_collection }
531+
532+ sentry_init (** kwargs )
533+
534+
535+ @pytest .mark .parametrize ("span_streaming" , [True , False ])
536+ @pytest .mark .parametrize (
537+ "data_collection, send_default_pii, include_prompts, expect_inputs, expect_outputs" ,
538+ DATA_COLLECTION_CASES ,
539+ )
540+ def test_nonstreaming_chat_data_collection (
541+ sentry_init ,
542+ capture_events ,
543+ capture_items ,
544+ data_collection ,
545+ send_default_pii ,
546+ include_prompts ,
547+ expect_inputs ,
548+ expect_outputs ,
549+ span_streaming ,
550+ ):
551+ _init_with_data_collection (
552+ sentry_init , data_collection , send_default_pii , include_prompts , span_streaming
553+ )
554+
555+ client = Client (api_key = "z" )
556+ HTTPXClient .request = mock .Mock (
557+ return_value = httpx .Response (
558+ 200 ,
559+ json = {
560+ "text" : "the model response" ,
561+ "generation_id" : "gen-1" ,
562+ "citations" : [
563+ {
564+ "start" : 0 ,
565+ "end" : 3 ,
566+ "text" : "the" ,
567+ "document_ids" : ["doc-1" ],
568+ }
569+ ],
570+ "meta" : {
571+ "billed_units" : {
572+ "output_tokens" : 10 ,
573+ "input_tokens" : 20 ,
574+ }
575+ },
576+ },
577+ )
578+ )
579+
580+ if span_streaming :
581+ items = capture_items ("span" )
582+ else :
583+ events = capture_events ()
584+
585+ with start_transaction (name = "cohere tx" ):
586+ client .chat (
587+ model = "some-model" ,
588+ chat_history = [ChatMessage (role = "SYSTEM" , message = "some context" )],
589+ message = "hello" ,
590+ preamble = "be concise" ,
591+ )
592+
593+ if span_streaming :
594+ sentry_sdk .flush ()
595+ assert len (items ) == 1
596+ attributes = items [0 ].payload ["attributes" ]
597+ else :
598+ attributes = events [0 ]["spans" ][0 ]["data" ]
599+
600+ assert attributes [SPANDATA .AI_MODEL_ID ] == "some-model"
601+ assert attributes ["gen_ai.usage.input_tokens" ] == 20
602+ assert attributes ["gen_ai.usage.output_tokens" ] == 10
603+ assert attributes ["ai.generation_id" ] == "gen-1"
604+
605+ if expect_inputs :
606+ assert '{"role": "user", "content": "hello"}' in str (
607+ attributes [SPANDATA .AI_INPUT_MESSAGES ]
608+ )
609+ assert attributes [SPANDATA .AI_PREAMBLE ] == "be concise"
610+ else :
611+ assert SPANDATA .AI_INPUT_MESSAGES not in attributes
612+ assert SPANDATA .AI_PREAMBLE not in attributes
613+
614+ if expect_outputs :
615+ assert "the model response" in str (attributes [SPANDATA .AI_RESPONSES ])
616+ assert "doc-1" in str (attributes ["ai.citations" ])
617+ else :
618+ assert SPANDATA .AI_RESPONSES not in attributes
619+ assert "ai.citations" not in attributes
620+
621+
622+ @pytest .mark .parametrize ("span_streaming" , [True , False ])
623+ @pytest .mark .parametrize (
624+ "data_collection, send_default_pii, include_prompts, expect_inputs, expect_outputs" ,
625+ DATA_COLLECTION_CASES ,
626+ )
627+ def test_streaming_chat_data_collection (
628+ sentry_init ,
629+ capture_events ,
630+ capture_items ,
631+ data_collection ,
632+ send_default_pii ,
633+ include_prompts ,
634+ expect_inputs ,
635+ expect_outputs ,
636+ span_streaming ,
637+ ):
638+ _init_with_data_collection (
639+ sentry_init , data_collection , send_default_pii , include_prompts , span_streaming
640+ )
641+
642+ client = Client (api_key = "z" )
643+ HTTPXClient .send = mock .Mock (
644+ return_value = httpx .Response (
645+ 200 ,
646+ content = "\n " .join (
647+ [
648+ json .dumps ({"event_type" : "text-generation" , "text" : "the model " }),
649+ json .dumps ({"event_type" : "text-generation" , "text" : "response" }),
650+ json .dumps (
651+ {
652+ "event_type" : "stream-end" ,
653+ "finish_reason" : "COMPLETE" ,
654+ "response" : {
655+ "text" : "the model response" ,
656+ "generation_id" : "gen-1" ,
657+ "citations" : [
658+ {
659+ "start" : 0 ,
660+ "end" : 3 ,
661+ "text" : "the" ,
662+ "document_ids" : ["doc-1" ],
663+ }
664+ ],
665+ "meta" : {
666+ "billed_units" : {
667+ "output_tokens" : 10 ,
668+ "input_tokens" : 20 ,
669+ }
670+ },
671+ },
672+ }
673+ ),
674+ ]
675+ ),
676+ )
677+ )
678+
679+ if span_streaming :
680+ items = capture_items ("span" )
681+ else :
682+ events = capture_events ()
683+
684+ with start_transaction (name = "cohere tx" ):
685+ list (
686+ client .chat_stream (
687+ model = "some-model" ,
688+ chat_history = [ChatMessage (role = "SYSTEM" , message = "some context" )],
689+ message = "hello" ,
690+ preamble = "be concise" ,
691+ )
692+ )
693+
694+ if span_streaming :
695+ sentry_sdk .flush ()
696+ assert len (items ) == 1
697+ attributes = items [0 ].payload ["attributes" ]
698+ else :
699+ attributes = events [0 ]["spans" ][0 ]["data" ]
700+
701+ assert attributes [SPANDATA .AI_MODEL_ID ] == "some-model"
702+ assert attributes ["gen_ai.usage.input_tokens" ] == 20
703+ assert attributes ["gen_ai.usage.output_tokens" ] == 10
704+
705+ if expect_inputs :
706+ assert '{"role": "user", "content": "hello"}' in str (
707+ attributes [SPANDATA .AI_INPUT_MESSAGES ]
708+ )
709+ assert attributes [SPANDATA .AI_PREAMBLE ] == "be concise"
710+ else :
711+ assert SPANDATA .AI_INPUT_MESSAGES not in attributes
712+ assert SPANDATA .AI_PREAMBLE not in attributes
713+
714+ if expect_outputs :
715+ assert "the model response" in str (attributes [SPANDATA .AI_RESPONSES ])
716+ assert "doc-1" in str (attributes ["ai.citations" ])
717+ else :
718+ assert SPANDATA .AI_RESPONSES not in attributes
719+ assert "ai.citations" not in attributes
720+
721+
722+ @pytest .mark .parametrize ("span_streaming" , [True , False ])
723+ @pytest .mark .parametrize (
724+ "data_collection, send_default_pii, include_prompts, expect_inputs, expect_outputs" ,
725+ DATA_COLLECTION_CASES ,
726+ )
727+ def test_embed_data_collection (
728+ sentry_init ,
729+ capture_events ,
730+ capture_items ,
731+ data_collection ,
732+ send_default_pii ,
733+ include_prompts ,
734+ expect_inputs ,
735+ expect_outputs ,
736+ span_streaming ,
737+ ):
738+ _init_with_data_collection (
739+ sentry_init , data_collection , send_default_pii , include_prompts , span_streaming
740+ )
741+
742+ client = Client (api_key = "z" )
743+ HTTPXClient .request = mock .Mock (
744+ return_value = httpx .Response (
745+ 200 ,
746+ json = {
747+ "response_type" : "embeddings_floats" ,
748+ "id" : "1" ,
749+ "texts" : ["hello" ],
750+ "embeddings" : [[1.0 , 2.0 , 3.0 ]],
751+ "meta" : {
752+ "billed_units" : {
753+ "input_tokens" : 10 ,
754+ }
755+ },
756+ },
757+ )
758+ )
759+
760+ if span_streaming :
761+ items = capture_items ("span" )
762+ else :
763+ events = capture_events ()
764+
765+ with start_transaction (name = "cohere tx" ):
766+ client .embed (texts = ["hello" ], model = "text-embedding-3-large" )
767+
768+ if span_streaming :
769+ sentry_sdk .flush ()
770+ assert len (items ) == 1
771+ attributes = items [0 ].payload ["attributes" ]
772+ else :
773+ attributes = events [0 ]["spans" ][0 ]["data" ]
774+
775+ assert attributes [SPANDATA .AI_MODEL_ID ] == "text-embedding-3-large"
776+ assert attributes ["gen_ai.usage.input_tokens" ] == 10
777+
778+ if expect_inputs :
779+ assert "hello" in str (attributes [SPANDATA .AI_INPUT_MESSAGES ])
780+ else :
781+ assert SPANDATA .AI_INPUT_MESSAGES not in attributes
0 commit comments