Skip to content

Commit c203bb6

Browse files
fix(clickhouse): Don't eat the generator data
Currently, the Clickhouse integration consumes any data passed as a generator when reading it for insertion as `db_params`. Instead, since generators cannot be cloned, we need to wrap the generator to add the params as we iterate over it. Fixes #4657
1 parent f76b786 commit c203bb6

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-2
lines changed

sentry_sdk/integrations/clickhouse_driver.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ def _inner(*args: P.args, **kwargs: P.kwargs) -> T:
9595
span.set_data("db.query_id", query_id)
9696

9797
if params and should_send_default_pii():
98-
span.set_data("db.params", params)
98+
# Don't store generators directly - they'll be handled in _wrap_send_data
99+
if not isinstance(params, Generator):
100+
span.set_data("db.params", params)
99101

100102
# run the original code
101103
ret = f(*args, **kwargs)
@@ -146,7 +148,24 @@ def _inner_send_data(
146148

147149
if should_send_default_pii():
148150
db_params = span._data.get("db.params", [])
149-
db_params.extend(data)
151+
152+
if isinstance(data, (list, tuple)):
153+
db_params.extend(data)
154+
155+
else: # data is a generic iterator
156+
orig_data = data
157+
158+
# Wrap the generator to add items to db.params as they are yielded.
159+
# This allows us to send the params to Sentry without needing to allocate
160+
# memory for the entire generator at once.
161+
def wrapped_generator():
162+
for item in orig_data:
163+
db_params.append(item)
164+
yield item
165+
166+
# Replace the original iterator with the wrapped one.
167+
data = wrapped_generator()
168+
150169
span.set_data("db.params", db_params)
151170

152171
return original_send_data(

tests/integrations/clickhouse_driver/test_clickhouse_driver.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,38 @@ def test_clickhouse_client_spans(
342342
assert event["spans"] == expected_spans
343343

344344

345+
def test_clickhouse_spans_with_generator(sentry_init, capture_events):
346+
sentry_init(
347+
integrations=[ClickhouseDriverIntegration()],
348+
send_default_pii=True,
349+
traces_sample_rate=1.0,
350+
)
351+
events = capture_events()
352+
353+
# Use a generator to test that the integration obtains values from the generator,
354+
# without consuming the generator.
355+
values = ({"x": i} for i in range(3))
356+
357+
with start_transaction(name="test_clickhouse_transaction"):
358+
client = Client("localhost")
359+
client.execute("DROP TABLE IF EXISTS test")
360+
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
361+
client.execute("INSERT INTO test (x) VALUES", values)
362+
res = client.execute("SELECT x FROM test")
363+
364+
# Verify that the integration did not consume the generator
365+
assert res == [(0,), (1,), (2,)]
366+
367+
(event,) = events
368+
spans = event["spans"]
369+
370+
[span] = [
371+
span for span in spans if span["description"] == "INSERT INTO test (x) VALUES"
372+
]
373+
374+
assert span["data"]["db.params"] == [{"x": 0}, {"x": 1}, {"x": 2}]
375+
376+
345377
def test_clickhouse_client_spans_with_pii(
346378
sentry_init, capture_events, capture_envelopes
347379
) -> None:

0 commit comments

Comments
 (0)