feat(seer-activity): Support webhooks for Sentry Apps#118255
feat(seer-activity): Support webhooks for Sentry Apps#118255leeandher wants to merge 15 commits into
Conversation
|
Example payloads: With alert rule UI component{
"action": "triggered",
"installation": {
"uuid": "717a85e5-236c-45c0-8b4e-ba01f5391395"
},
"data": {
"issue": {
"url": "https://leeandher.ngrok.io/api/0/organizations/acme/issues/42/",
"webUrl": "https://leeandher.ngrok.io/organizations/acme/issues/42/",
"id": "42",
"shareId": null,
"shortId": "ERROR-GEN-Q",
"title": "TypeError: you aren't my type",
"culprit": "test-transaction-0-03c84287-ece6-4c3d-b9ed-bc763a93e6d8",
"permalink": "https://leeandher.ngrok.io/organizations/acme/issues/42/",
"logger": "edge-function",
"level": "warning",
"status": "unresolved",
"statusDetails": {},
"substatus": "new",
"isPublic": false,
"platform": "javascript",
"project": {
"id": "2",
"name": "error-gen",
"slug": "error-gen",
"platform": "javascript-nextjs"
},
"type": "default",
"metadata": {
"title": "TypeError: you aren't my type",
"sdk": {
"name": "edge-function",
"name_normalized": "other"
},
"initial_priority": 50
},
"numComments": 0,
"assignedTo": null,
"isBookmarked": false,
"isSubscribed": false,
"subscriptionDetails": null,
"hasSeen": false,
"annotations": [],
"issueType": "error",
"issueCategory": "error",
"priority": "medium",
"priorityLockedAt": null,
"seerFixabilityScore": 0.49159157276153564,
"seerAutofixLastTriggered": null,
"seerExplorerAutofixLastTriggered": "2026-06-23T16:58:38.666953Z",
"isUnhandled": false,
"count": "1",
"userCount": 24,
"firstSeen": "2026-06-23T16:25:11.820000Z",
"lastSeen": "2026-06-23T16:25:11.820000Z"
},
"activity": {
"type": "seer_solution_completed",
"details": {
"summary": "Configure Sentry inbound filters or alert rules to ignore synthetic test errors from error-generator.sentry.dev"
}
},
"alert": {
"id": 10,
"title": "I'm keeping an eye on Seer 👀",
"sentry_app_id": 2,
"url": "https://leeandher.ngrok.io/organizations/acme/monitors/alerts/10/",
"settings": [
{
"name": "title",
"value": "A Title"
},
{
"name": "description",
"value": "A Description"
}
]
}
},
"actor": {
"type": "application",
"id": "sentry",
"name": "Sentry"
}
}
Just the alert rule action{
"action": "triggered",
"installation": {
"uuid": "209fb71d-d932-437c-b985-ffee8a6f3fa4"
},
"data": {
"issue": {
"url": "https://leeandher.ngrok.io/api/0/organizations/acme/issues/42/",
"webUrl": "https://leeandher.ngrok.io/organizations/acme/issues/42/",
"id": "42",
"shareId": null,
"shortId": "ERROR-GEN-Q",
"title": "TypeError: you aren't my type",
"culprit": "test-transaction-0-03c84287-ece6-4c3d-b9ed-bc763a93e6d8",
"permalink": "https://leeandher.ngrok.io/organizations/acme/issues/42/",
"logger": "edge-function",
"level": "warning",
"status": "unresolved",
"statusDetails": {},
"substatus": "new",
"isPublic": false,
"platform": "javascript",
"project": {
"id": "2",
"name": "error-gen",
"slug": "error-gen",
"platform": "javascript-nextjs"
},
"type": "default",
"metadata": {
"title": "TypeError: you aren't my type",
"sdk": {
"name": "edge-function",
"name_normalized": "other"
},
"initial_priority": 50
},
"numComments": 0,
"assignedTo": null,
"isBookmarked": false,
"isSubscribed": false,
"subscriptionDetails": null,
"hasSeen": false,
"annotations": [],
"issueType": "error",
"issueCategory": "error",
"priority": "medium",
"priorityLockedAt": null,
"seerFixabilityScore": 0.49159157276153564,
"seerAutofixLastTriggered": null,
"seerExplorerAutofixLastTriggered": "2026-06-23T16:58:38.666953Z",
"isUnhandled": false,
"count": "1",
"userCount": 24,
"firstSeen": "2026-06-23T16:25:11.820000Z",
"lastSeen": "2026-06-23T16:25:11.820000Z"
},
"activity": {
"type": "seer_solution_completed",
"details": {
"summary": "Configure Sentry inbound filters or alert rules to ignore synthetic test errors from error-generator.sentry.dev"
}
},
"alert": {
"id": 10,
"title": "I'm keeping an eye on Seer 👀",
"sentry_app_id": 1,
"url": "https://leeandher.ngrok.io/organizations/acme/monitors/alerts/10/"
}
},
"actor": {
"type": "application",
"id": "sentry",
"name": "Sentry"
}
}
|
|
🚨 Warning: This pull request contains Frontend and Backend changes! It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently. Have questions? Please ask in the |
saponifi3d
left a comment
There was a problem hiding this comment.
the return all that looks good. biggest callout here in the review is to DRY up the constants as much as we can.
| ]); | ||
|
|
||
| const ACTIVITY_TRIGGER_SUPPORTED_ACTIONS = new Set<ActionType>([ | ||
| const SEER_ACTIVITY_SUPPORTED_ACTIONS = new Set<ActionType>([ |
There was a problem hiding this comment.
🤔 should we limit these changes to just seer activities or support activities generically for these notifications? (another way to phrase this question might be; could a set_resolved activity also send a webhook? if so, could we just add that support now while we're adding it for seer?)
There was a problem hiding this comment.
It could! Though the error message here is specific for seer activities and their incompatible actions, so I was mostly just correcting the constant name. I can look at following up for set_resolved activities once this lands though, possibly
| target_identifier = invocation.action.config.get("target_identifier") | ||
| if target_identifier == "webhooks": | ||
| send_legacy_webhooks_for_invocation(invocation) | ||
| return _handle_legacy_webhooks(invocation) |
There was a problem hiding this comment.
nit: dont need the return?
There was a problem hiding this comment.
This return we do need, otherwise we'll fallthrough to send_sentry_app_webhook for actions who's config has an identifier of "webhooks". In the previous path, that was part of an if/else so we didn't have to, but since I moved it, we need the early exit now.
There was a problem hiding this comment.
was able to remove the other return calls though, that weren't doing anything
| if features.has( | ||
| "organizations:workflow-engine-evaluate-seer-activities", organization | ||
| ): |
There was a problem hiding this comment.
i think you addressed this in the cusor comment below, but currently
GroupEvent -> legacy_webhook, sentry app webhook
Activity ->sentry app via activity type reg (if FF on), legacy_webhook
Is this the right config?
There was a problem hiding this comment.
I believe so, if I'm following. Prior, the webhook handler just dropped all Activity workflow events. Now, we check only drop Activity workflow events if
- the target_identifier is "webhooks", since those are plugin-based
- they dont have the feature flag
| class ActivityAlertType(StrEnum): | ||
| SEER_RCA_STARTED = "seer_root_cause_started" | ||
| SEER_RCA_COMPLETED = "seer_root_cause_completed" | ||
| SEER_SOLUTION_STARTED = "seer_solution_started" | ||
| SEER_SOLUTION_COMPLETED = "seer_solution_completed" | ||
| SEER_CODING_STARTED = "seer_coding_started" | ||
| SEER_CODING_COMPLETED = "seer_coding_completed" | ||
| SEER_PR_CREATED = "seer_pr_created" | ||
|
|
||
|
|
||
| ACTIVITY_TYPE_TO_ACTIVITY_ALERT_TYPE: dict[int, ActivityAlertType] = { | ||
| ActivityType.SEER_RCA_STARTED.value: ActivityAlertType.SEER_RCA_STARTED, | ||
| ActivityType.SEER_RCA_COMPLETED.value: ActivityAlertType.SEER_RCA_COMPLETED, | ||
| ActivityType.SEER_SOLUTION_STARTED.value: ActivityAlertType.SEER_SOLUTION_STARTED, | ||
| ActivityType.SEER_SOLUTION_COMPLETED.value: ActivityAlertType.SEER_SOLUTION_COMPLETED, | ||
| ActivityType.SEER_CODING_STARTED.value: ActivityAlertType.SEER_CODING_STARTED, | ||
| ActivityType.SEER_CODING_COMPLETED.value: ActivityAlertType.SEER_CODING_COMPLETED, | ||
| ActivityType.SEER_PR_CREATED.value: ActivityAlertType.SEER_PR_CREATED, | ||
| } |
There was a problem hiding this comment.
er are these two's enum values the same? ig what's the difference between ActivityAlertType and ActivityType?
There was a problem hiding this comment.
ActivityType is an integer enum that we would serve no purpose being exposed in a webhook since 29 doesn't mean anything to customers. The ACTIVITY_TYPE_TO_ACTIVITY_ALERT_TYPE maps these integers to a str that will appear in the webhook that customers can actually use, in this case seer_root_cause_started.
I was able to simplify it from Josh' suggestion though, so it's a bit less verbose now
| install=install, | ||
| data=data, | ||
| ) | ||
| send_and_save_webhook_request(install.sentry_app, request_data) |
There was a problem hiding this comment.
I think you'll need to separate the invocation out into its own task. Because of the circuit breaker logic we have for webhooks, we can't send any webhooks in the main app or they'll error out. See what we did for metric alerts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7bb345b. Configure here.
| silo_mode=SiloMode.CELL, | ||
| silenced_exceptions=_SENTRY_APP_WEBHOOK_SILENCED, | ||
| ) | ||
| def send_activity_alert_webhook( |
There was a problem hiding this comment.
might need to export this task 🤔

Allows for alert rule UI components and regular webhooks.
It diverges from the existing issue alert payload though, since we do not have an event to serialize. I tried to keep as much as I could in logical parity.
The new data payload looks like this:
{ 'issue': { 'url': str, # serialized the API URL for the group on the event serializer 'webUrl': str, # serialized the Web URL for the group on the event serializer **serialized_group }, 'activity': { 'type': str, # in readable english, e.g. seer_pr_created 'details': dict[str, Any] # depends on activity, i.e. { "pull_requests: [{"url": "github.com...", "repo": "org/repo"}, ...] or { "summary": str } }, 'alert': { 'id': int, # workflow ID 'title': str # workflow.name, e.g. Notify #feed via Slack 'sentry_app_id': int # was on `issue_alert` key for alert rule ui components, keeping for compatibility 'url': str # seemed useful, to link back to the settings page 'settings' : NotRequired[list[dict[str, Any]]] # only set when alert rule ui components are set } }with a
sentry-hook-resourceheader ofactivity_alert. I kept theactionkey astriggered, but its using a different enum since the event type is nowactivity_alert.triggered, notevent_alert.triggered.Sorta related changes:
seer_base.pytobase.pyfor extracting models, but needed to do function level imports to avoid cycles