Skip to content

Commit 45b13a7

Browse files
authored
fix(sqlalchemy): Fix broken nesting under begin_nested after rollback (#697)
1 parent ad28065 commit 45b13a7

File tree

3 files changed

+109
-2
lines changed

3 files changed

+109
-2
lines changed

sentry_sdk/integrations/sqlalchemy.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,14 @@ def _handle_error(context, *args):
8484

8585
if span is not None:
8686
span.set_status("internal_error")
87+
88+
# _after_cursor_execute does not get called for crashing SQL stmts. Judging
89+
# from SQLAlchemy codebase it does seem like any error coming into this
90+
# handler is going to be fatal.
91+
ctx_mgr = getattr(
92+
conn, "_sentry_sql_span_manager", None
93+
) # type: ContextManager[Any]
94+
95+
if ctx_mgr is not None:
96+
conn._sentry_sql_span_manager = None
97+
ctx_mgr.__exit__(None, None, None)

tests/conftest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,28 @@ def maybe_monkeypatched_threading(request):
313313
assert request.param is None
314314

315315
return request.param
316+
317+
318+
@pytest.fixture
319+
def render_span_tree():
320+
def inner(event):
321+
assert event["type"] == "transaction"
322+
323+
by_parent = {}
324+
for span in event["spans"]:
325+
by_parent.setdefault(span["parent_span_id"], []).append(span)
326+
327+
def render_span(span):
328+
yield "- op={!r}: description={!r}".format(
329+
span.get("op"), span.get("description")
330+
)
331+
for subspan in by_parent.get(span["span_id"]) or ():
332+
for line in render_span(subspan):
333+
yield " {}".format(line)
334+
335+
root_span = event["contexts"]["trace"]
336+
337+
# Return a list instead of a multiline string because black will know better how to format that
338+
return "\n".join(render_span(root_span))
339+
340+
return inner

tests/integrations/sqlalchemy/test_sqlalchemy.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
from sqlalchemy import Column, ForeignKey, Integer, String
1+
import sys
2+
import pytest
3+
4+
from sqlalchemy import Column, ForeignKey, Integer, String, create_engine
5+
from sqlalchemy.exc import IntegrityError
26
from sqlalchemy.ext.declarative import declarative_base
37
from sqlalchemy.orm import relationship, sessionmaker
4-
from sqlalchemy import create_engine
58

9+
import sentry_sdk
610
from sentry_sdk import capture_message
711
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
812

@@ -63,3 +67,70 @@ class Address(Base):
6367
"type": "default",
6468
},
6569
]
70+
71+
72+
@pytest.mark.skipif(
73+
sys.version_info < (3,), reason="This sqla usage seems to be broken on Py2"
74+
)
75+
def test_transactions(sentry_init, capture_events, render_span_tree):
76+
77+
sentry_init(
78+
integrations=[SqlalchemyIntegration()], _experiments={"record_sql_params": True}
79+
)
80+
events = capture_events()
81+
82+
Base = declarative_base() # noqa: N806
83+
84+
class Person(Base):
85+
__tablename__ = "person"
86+
id = Column(Integer, primary_key=True)
87+
name = Column(String(250), nullable=False)
88+
89+
class Address(Base):
90+
__tablename__ = "address"
91+
id = Column(Integer, primary_key=True)
92+
street_name = Column(String(250))
93+
street_number = Column(String(250))
94+
post_code = Column(String(250), nullable=False)
95+
person_id = Column(Integer, ForeignKey("person.id"))
96+
person = relationship(Person)
97+
98+
engine = create_engine("sqlite:///:memory:")
99+
Base.metadata.create_all(engine)
100+
101+
Session = sessionmaker(bind=engine) # noqa: N806
102+
session = Session()
103+
104+
with sentry_sdk.start_span(transaction="test_transaction", sampled=True):
105+
with session.begin_nested():
106+
session.query(Person).first()
107+
108+
for _ in range(2):
109+
with pytest.raises(IntegrityError):
110+
with session.begin_nested():
111+
session.add(Person(id=1, name="bob"))
112+
session.add(Person(id=1, name="bob"))
113+
114+
with session.begin_nested():
115+
session.query(Person).first()
116+
117+
(event,) = events
118+
119+
assert (
120+
render_span_tree(event)
121+
== """\
122+
- op=None: description=None
123+
- op='db': description='SAVEPOINT sa_savepoint_1'
124+
- op='db': description='SELECT person.id AS person_id, person.name AS person_name \\nFROM person\\n LIMIT ? OFFSET ?'
125+
- op='db': description='RELEASE SAVEPOINT sa_savepoint_1'
126+
- op='db': description='SAVEPOINT sa_savepoint_2'
127+
- op='db': description='INSERT INTO person (id, name) VALUES (?, ?)'
128+
- op='db': description='ROLLBACK TO SAVEPOINT sa_savepoint_2'
129+
- op='db': description='SAVEPOINT sa_savepoint_3'
130+
- op='db': description='INSERT INTO person (id, name) VALUES (?, ?)'
131+
- op='db': description='ROLLBACK TO SAVEPOINT sa_savepoint_3'
132+
- op='db': description='SAVEPOINT sa_savepoint_4'
133+
- op='db': description='SELECT person.id AS person_id, person.name AS person_name \\nFROM person\\n LIMIT ? OFFSET ?'
134+
- op='db': description='RELEASE SAVEPOINT sa_savepoint_4'\
135+
"""
136+
)

0 commit comments

Comments
 (0)