Skip to content

Commit cdbf53b

Browse files
authored
feat(cohere): Support data_collection filtering for inputs and outputs (#7289)
Respect the data_collection.gen_ai.inputs/outputs config option when capturing chat/embed request and response data, falling back to the legacy send_default_pii + include_prompts behavior when not set. Fixes PY-2747 Fixes #7282
1 parent 55e7f98 commit cdbf53b

2 files changed

Lines changed: 343 additions & 9 deletions

File tree

sentry_sdk/integrations/cohere.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
import sentry_sdk
1818
from sentry_sdk.integrations import DidNotEnable, Integration
1919
from sentry_sdk.scope import should_send_default_pii
20-
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise
20+
from sentry_sdk.utils import (
21+
capture_internal_exceptions,
22+
event_from_exception,
23+
has_data_collection_enabled,
24+
reraise,
25+
)
2126

2227
try:
2328
from cohere import (
@@ -83,6 +88,14 @@ def setup_once() -> None:
8388
BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True)
8489

8590

91+
def _should_record(integration: "CohereIntegration", category: str) -> bool:
92+
client = sentry_sdk.get_client()
93+
if has_data_collection_enabled(client.options):
94+
return bool(client.options["data_collection"]["gen_ai"][category])
95+
96+
return should_send_default_pii() and integration.include_prompts
97+
98+
8699
def _capture_exception(exc: "Any") -> None:
87100
event, hint = event_from_exception(
88101
exc,
@@ -179,7 +192,7 @@ def new_chat(*args: "Any", **kwargs: "Any") -> "Any":
179192
reraise(*exc_info)
180193

181194
with capture_internal_exceptions():
182-
if should_send_default_pii() and integration.include_prompts:
195+
if _should_record(integration, "inputs"):
183196
set_data_normalized(
184197
span,
185198
SPANDATA.AI_INPUT_MESSAGES,
@@ -215,8 +228,7 @@ def new_iterator() -> "Iterator[StreamedChatResponse]":
215228
collect_chat_response_fields(
216229
span,
217230
x.response,
218-
include_pii=should_send_default_pii()
219-
and integration.include_prompts,
231+
include_pii=_should_record(integration, "outputs"),
220232
)
221233
yield x
222234
_end_span(span)
@@ -226,8 +238,7 @@ def new_iterator() -> "Iterator[StreamedChatResponse]":
226238
collect_chat_response_fields(
227239
span,
228240
res,
229-
include_pii=should_send_default_pii()
230-
and integration.include_prompts,
241+
include_pii=_should_record(integration, "outputs"),
231242
)
232243
_end_span(span)
233244
else:
@@ -265,9 +276,7 @@ def new_embed(*args: "Any", **kwargs: "Any") -> "Any":
265276
)
266277

267278
with span_ctx as span:
268-
if "texts" in kwargs and (
269-
should_send_default_pii() and integration.include_prompts
270-
):
279+
if "texts" in kwargs and _should_record(integration, "inputs"):
271280
if isinstance(kwargs["texts"], str):
272281
set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]])
273282
elif (

tests/integrations/cohere/test_cohere.py

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)