Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/sentry/sentry_apps/services/app/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
RpcSentryAppService,
SentryAppInstallationFilterArgs,
)
from sentry.sentry_apps.services.app.model import SentryAppUpdateArgs
from sentry.sentry_apps.services.app.serial import (
serialize_sentry_app,
serialize_sentry_app_component,
Expand Down Expand Up @@ -333,6 +334,33 @@ def get_published_sentry_apps_for_organization(
)
return [serialize_sentry_app(app) for app in published_apps]

def get_sentry_apps_for_organization(self, *, organization_id: int) -> list[RpcSentryApp]:
"""
Get active Sentry Apps for a given organization
"""
sentry_apps = SentryApp.objects.filter(
owner_id=organization_id, application__isnull=False
).exclude(status=SentryAppStatus.DELETION_IN_PROGRESS)
return [serialize_sentry_app(app) for app in sentry_apps]

def update_sentry_app(
self,
*,
id: int,
attrs: SentryAppUpdateArgs,
) -> RpcSentryApp | None:
try:
sentry_app = SentryApp.objects.get(id=id)
except SentryApp.DoesNotExist:
return None

if len(attrs):
for k, v in attrs.items():
setattr(sentry_app, k, v)
sentry_app.save()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: ORM Bypass Causes Data Validation Failures

The update_sentry_app method uses direct setattr() which bypasses Django's ORM field handling, leading to incorrect data transformations and validation issues. Additionally, the save() call is conditional, meaning if no attributes are provided, the method may return stale data without refreshing the object.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


return serialize_sentry_app(sentry_app)

def get_internal_integrations(
self, *, organization_id: int, integration_name: str
) -> list[RpcSentryApp]:
Expand Down
6 changes: 6 additions & 0 deletions src/sentry/sentry_apps/services/app/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,9 @@ class SentryAppInstallationFilterArgs(TypedDict, total=False):
status: int
api_token_id: int
api_installation_token_id: str


class SentryAppUpdateArgs(TypedDict, total=False):
events: list[str]
name: str
# TODO add whatever else as needed
12 changes: 11 additions & 1 deletion src/sentry/sentry_apps/services/app/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
RpcSentryAppService,
SentryAppInstallationFilterArgs,
)
from sentry.sentry_apps.services.app.model import RpcSentryAppComponentContext
from sentry.sentry_apps.services.app.model import RpcSentryAppComponentContext, SentryAppUpdateArgs
from sentry.silo.base import SiloMode
from sentry.users.services.user import RpcUser

Expand Down Expand Up @@ -192,6 +192,16 @@ def get_published_sentry_apps_for_organization(
) -> list[RpcSentryApp]:
pass

@rpc_method
@abc.abstractmethod
def get_sentry_apps_for_organization(self, *, organization_id: int) -> list[RpcSentryApp]:
pass

@rpc_method
@abc.abstractmethod
def update_sentry_app(self, *, id: int, attrs: SentryAppUpdateArgs) -> RpcSentryApp | None:
pass

@rpc_method
@abc.abstractmethod
def get_internal_integrations(
Expand Down
41 changes: 41 additions & 0 deletions tests/sentry/sentry_apps/services/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,47 @@ def test_get_installation_org_id_by_token_id() -> None:
assert result is None


@django_db_all(transaction=True)
@all_silo_test
def test_get_sentry_apps_for_organization() -> None:
org = Factories.create_organization()
other_org = Factories.create_organization()

# Create internal integrations
Factories.create_internal_integration(
name="Test Integration",
organization_id=org.id,
)
Factories.create_internal_integration(
name="Test Integration",
organization_id=other_org.id,
)
result = app_service.get_sentry_apps_for_organization(organization_id=org.id)
assert len(result) == 1
assert result[0].owner_id == org.id


@django_db_all(transaction=True)
@all_silo_test
def test_update_sentry_app() -> None:
org = Factories.create_organization()

sentry_app = Factories.create_internal_integration(
name="Test Integration",
organization_id=org.id,
events=[
"issue.resolved",
"issue.unresolved",
"issue.ignored",
"issue.assigned",
"error.created",
],
)
result = app_service.update_sentry_app(id=sentry_app.id, attrs=dict(events=["error.created"]))
assert result
assert result.events == ["error.created"]


@django_db_all(transaction=True)
@all_silo_test
def test_get_internal_integrations() -> None:
Expand Down
Loading