Skip to content

Commit fd908c5

Browse files
merge master
2 parents c11c0f6 + f4634af commit fd908c5

File tree

6 files changed

+176
-45
lines changed

6 files changed

+176
-45
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Enforce Draft PR
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened]
6+
7+
jobs:
8+
enforce-draft:
9+
name: Enforce Draft PR
10+
runs-on: ubuntu-24.04
11+
if: github.event.pull_request.draft == false
12+
steps:
13+
- name: Generate GitHub App token
14+
id: app-token
15+
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
16+
with:
17+
app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }}
18+
private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }}
19+
20+
- name: Convert PR to draft
21+
env:
22+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
23+
PR_URL: ${{ github.event.pull_request.html_url }}
24+
run: |
25+
gh pr ready "$PR_URL" --undo
26+
27+
- name: Label and comment
28+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
29+
with:
30+
github-token: ${{ steps.app-token.outputs.token }}
31+
script: |
32+
const pullRequest = context.payload.pull_request;
33+
const repo = context.repo;
34+
35+
// Label the PR so maintainers can filter/track violations
36+
await github.rest.issues.addLabels({
37+
...repo,
38+
issue_number: pullRequest.number,
39+
labels: ['converted-to-draft'],
40+
});
41+
42+
// Check for existing bot comment to avoid duplicates on reopen
43+
const comments = await github.rest.issues.listComments({
44+
...repo,
45+
issue_number: pullRequest.number,
46+
});
47+
const botComment = comments.data.find(c =>
48+
c.user.type === 'Bot' &&
49+
c.body.includes('automatically converted to draft')
50+
);
51+
if (botComment) {
52+
core.info('Bot comment already exists, skipping.');
53+
return;
54+
}
55+
56+
const contributingUrl = `https://github.com/${repo.owner}/${repo.repo}/blob/master/CONTRIBUTING.md`;
57+
58+
await github.rest.issues.createComment({
59+
...repo,
60+
issue_number: pullRequest.number,
61+
body: [
62+
`This PR has been automatically converted to draft. All PRs must start as drafts per our [contributing guidelines](${contributingUrl}).`,
63+
'',
64+
'**Next steps:**',
65+
'1. Ensure CI passes',
66+
'2. Fill in the PR description completely',
67+
'3. Mark as "Ready for review" when you\'re done'
68+
].join('\n')
69+
});

CONTRIBUTING.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Please search the [issue tracker](https://github.com/getsentry/sentry-python/iss
1010

1111
## Submitting Changes
1212

13+
Before submitting a pull request, please check whether the issue you're planning to address, if there is one, is assigned to anyone. If you want to help with an issue that already has an assignee, comment on it first to coordinate with the person assigned whether the contribution would make sense before opening a pull request.
14+
15+
To make a contribution:
16+
1317
- Fork the `sentry-python` repo and prepare your changes.
1418
- Add tests for your changes to `tests/`.
1519
- Run tests and make sure all of them pass.
@@ -21,6 +25,18 @@ We will review your pull request as soon as possible. Thank you for contributing
2125

2226
You are welcome to use whatever tools you prefer for making a contribution. However, any changes you propose have to be reviewed and tested by you, a human, first, before you submit a pull request with them for the Sentry team to review. If we feel like that didn't happen, we will close the PR outright. For example, we won't review visibly AI-generated PRs from an agent instructed to look for and "fix" open issues in the repo.
2327

28+
## Pull Requests
29+
30+
All PRs must be created as **drafts**. Non-draft PRs will be automatically converted to draft. Mark your PR as "Ready for review" once:
31+
32+
- CI passes
33+
- The PR description is complete (what, why, and links to relevant issues)
34+
- You've personally reviewed your own changes
35+
36+
A PR should do one thing well. Don't mix functional changes with unrelated refactors or cleanup. Smaller, focused PRs are easier to review, reason about, and revert if needed.
37+
38+
For the full set of PR standards, see the [code submission standard](https://develop.sentry.dev/sdk/getting-started/standards/code-submission/#pull-requests).
39+
2440
## Development Environment
2541

2642
### Set up Python

sentry_sdk/integrations/asyncpg.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
import contextlib
3+
import re
34
from typing import Any, TypeVar, Callable, Awaitable, Iterator
45

56
import sentry_sdk
@@ -55,6 +56,10 @@ def setup_once() -> None:
5556
T = TypeVar("T")
5657

5758

59+
def _normalize_query(query: str) -> str:
60+
return re.sub(r"\s+", " ", query).strip()
61+
62+
5863
def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]":
5964
async def _inner(*args: "Any", **kwargs: "Any") -> "T":
6065
if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None:
@@ -67,7 +72,7 @@ async def _inner(*args: "Any", **kwargs: "Any") -> "T":
6772
if len(args) > 2:
6873
return await f(*args, **kwargs)
6974

70-
query = args[1]
75+
query = _normalize_query(args[1])
7176
with record_sql_queries(
7277
cursor=None,
7378
query=query,
@@ -103,6 +108,7 @@ def _record(
103108

104109
param_style = "pyformat" if params_list else None
105110

111+
query = _normalize_query(query)
106112
with record_sql_queries(
107113
cursor=cursor,
108114
query=query,

sentry_sdk/integrations/openai.py

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -443,43 +443,9 @@ def _set_embeddings_input_data(
443443
"input"
444444
)
445445

446-
tools = kwargs.get("tools")
447-
if tools is not None and _is_given(tools) and len(tools) > 0:
448-
set_data_normalized(
449-
span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)
450-
)
451-
452446
model = kwargs.get("model")
453-
if model is not None and _is_given(model):
454-
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MODEL, model)
455-
456-
stream = kwargs.get("stream")
457-
if stream is not None and _is_given(stream):
458-
set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_STREAMING, stream)
459-
460-
max_tokens = kwargs.get("max_tokens")
461-
if max_tokens is not None and _is_given(max_tokens):
462-
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens)
463-
464-
presence_penalty = kwargs.get("presence_penalty")
465-
if presence_penalty is not None and _is_given(presence_penalty):
466-
set_data_normalized(
467-
span, SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty
468-
)
469-
470-
frequency_penalty = kwargs.get("frequency_penalty")
471-
if frequency_penalty is not None and _is_given(frequency_penalty):
472-
set_data_normalized(
473-
span, SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty
474-
)
475-
476-
temperature = kwargs.get("temperature")
477-
if temperature is not None and _is_given(temperature):
478-
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature)
479-
480-
top_p = kwargs.get("top_p")
481-
if top_p is not None and _is_given(top_p):
482-
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_TOP_P, top_p)
447+
if model is not None:
448+
span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model)
483449

484450
if (
485451
not should_send_default_pii()

tests/integrations/asyncpg/test_asyncpg.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -463,10 +463,7 @@ async def test_connection_pool(sentry_init, capture_events) -> None:
463463
{
464464
"category": "query",
465465
"data": {},
466-
"message": "SELECT pg_advisory_unlock_all();\n"
467-
"CLOSE ALL;\n"
468-
"UNLISTEN *;\n"
469-
"RESET ALL;",
466+
"message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;",
470467
"type": "default",
471468
},
472469
{
@@ -478,10 +475,7 @@ async def test_connection_pool(sentry_init, capture_events) -> None:
478475
{
479476
"category": "query",
480477
"data": {},
481-
"message": "SELECT pg_advisory_unlock_all();\n"
482-
"CLOSE ALL;\n"
483-
"UNLISTEN *;\n"
484-
"RESET ALL;",
478+
"message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;",
485479
"type": "default",
486480
},
487481
]
@@ -786,3 +780,79 @@ async def test_span_origin(sentry_init, capture_events):
786780

787781
for span in event["spans"]:
788782
assert span["origin"] == "auto.db.asyncpg"
783+
784+
785+
@pytest.mark.asyncio
786+
async def test_multiline_query_description_normalized(sentry_init, capture_events):
787+
sentry_init(
788+
integrations=[AsyncPGIntegration()],
789+
traces_sample_rate=1.0,
790+
)
791+
events = capture_events()
792+
793+
with start_transaction(name="test_transaction"):
794+
conn: Connection = await connect(PG_CONNECTION_URI)
795+
await conn.execute(
796+
"""
797+
SELECT
798+
id,
799+
name
800+
FROM
801+
users
802+
WHERE
803+
name = 'Alice'
804+
"""
805+
)
806+
await conn.close()
807+
808+
(event,) = events
809+
810+
spans = [
811+
s
812+
for s in event["spans"]
813+
if s["op"] == "db" and "SELECT" in s.get("description", "")
814+
]
815+
assert len(spans) == 1
816+
assert spans[0]["description"] == "SELECT id, name FROM users WHERE name = 'Alice'"
817+
818+
819+
@pytest.mark.asyncio
820+
async def test_before_send_transaction_sees_normalized_description(
821+
sentry_init, capture_events
822+
):
823+
def before_send_transaction(event, hint):
824+
for span in event.get("spans", []):
825+
desc = span.get("description", "")
826+
if "SELECT id, name FROM users" in desc:
827+
span["description"] = "filtered"
828+
return event
829+
830+
sentry_init(
831+
integrations=[AsyncPGIntegration()],
832+
traces_sample_rate=1.0,
833+
before_send_transaction=before_send_transaction,
834+
)
835+
events = capture_events()
836+
837+
with start_transaction(name="test_transaction"):
838+
conn: Connection = await connect(PG_CONNECTION_URI)
839+
await conn.execute(
840+
"""
841+
SELECT
842+
id,
843+
name
844+
FROM
845+
users
846+
"""
847+
)
848+
await conn.close()
849+
850+
(event,) = events
851+
spans = [
852+
s
853+
for s in event["spans"]
854+
if s["op"] == "db" and "filtered" in s.get("description", "")
855+
]
856+
857+
assert len(spans) == 1
858+
assert spans[0]["description"] == "filtered"

tests/integrations/openai/test_openai.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,7 @@ def test_embeddings_create_no_pii(
11451145
span = tx["spans"][0]
11461146
assert span["op"] == "gen_ai.embeddings"
11471147
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "openai"
1148+
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "text-embedding-3-large"
11481149

11491150
assert SPANDATA.GEN_AI_EMBEDDINGS_INPUT not in span["data"]
11501151

@@ -1226,6 +1227,7 @@ def test_embeddings_create(sentry_init, capture_events, input, request):
12261227
span = tx["spans"][0]
12271228
assert span["op"] == "gen_ai.embeddings"
12281229
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "openai"
1230+
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "text-embedding-3-large"
12291231

12301232
param_id = request.node.callspec.id
12311233
if param_id == "string":
@@ -1298,6 +1300,7 @@ async def test_embeddings_create_async_no_pii(
12981300
span = tx["spans"][0]
12991301
assert span["op"] == "gen_ai.embeddings"
13001302
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "openai"
1303+
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "text-embedding-3-large"
13011304

13021305
assert SPANDATA.GEN_AI_EMBEDDINGS_INPUT not in span["data"]
13031306

@@ -1382,6 +1385,7 @@ async def test_embeddings_create_async(sentry_init, capture_events, input, reque
13821385
span = tx["spans"][0]
13831386
assert span["op"] == "gen_ai.embeddings"
13841387
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "openai"
1388+
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "text-embedding-3-large"
13851389

13861390
param_id = request.node.callspec.id
13871391
if param_id == "string":

0 commit comments

Comments
 (0)