Skip to content

feat(seer-activity): Support webhooks for Sentry Apps#118255

Open
leeandher wants to merge 15 commits into
masterfrom
leanderrodrigues/iswf-2907-allow-webhooks-with-a-different-source-key-to-still-fire-for
Open

feat(seer-activity): Support webhooks for Sentry Apps#118255
leeandher wants to merge 15 commits into
masterfrom
leanderrodrigues/iswf-2907-allow-webhooks-with-a-different-source-key-to-still-fire-for

Conversation

@leeandher

Copy link
Copy Markdown
Member

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-resource header of activity_alert. I kept the action key as triggered, but its using a different enum since the event type is now activity_alert.triggered, not event_alert.triggered.


Sorta related changes:

  • moved a util from seer_base.py to base.py for extracting models, but needed to do function level imports to avoid cycles
  • corrected the unsupported handler to not reference the notification platform.
  • added lifecycle support for this new path for sentry apps + added to the request buffer for debugging.

@leeandher leeandher requested a review from a team as a code owner June 23, 2026 17:32
@linear-code

linear-code Bot commented Jun 23, 2026

Copy link
Copy Markdown

ISWF-2907

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jun 23, 2026
@leeandher

leeandher commented Jun 23, 2026

Copy link
Copy Markdown
Member Author

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"
  }
}

@leeandher leeandher requested a review from a team as a code owner June 23, 2026 19:27
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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 #discuss-dev-infra channel.

@saponifi3d saponifi3d left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the return all that looks good. biggest callout here in the review is to DRY up the constants as much as we can.

Comment thread src/sentry/notifications/notification_action/activity_registry/base.py Outdated
]);

const ACTIVITY_TRIGGER_SUPPORTED_ACTIONS = new Set<ActionType>([
const SEER_ACTIVITY_SUPPORTED_ACTIONS = new Set<ActionType>([

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤔 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?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: dont need the return?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

was able to remove the other return calls though, that weren't doing anything

Comment on lines +65 to +67
if features.has(
"organizations:workflow-engine-evaluate-seer-activities", organization
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Comment on lines +32 to +50
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,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

er are these two's enum values the same? ig what's the difference between ActivityAlertType and ActivityType?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Comment thread src/sentry/notifications/notification_action/activity_registry/sentry_app.py Outdated
Comment thread src/sentry/notifications/notification_action/activity_registry/sentry_app.py Outdated
install=install,
data=data,
)
send_and_save_webhook_request(install.sentry_app, request_data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@leeandher leeandher requested a review from a team as a code owner June 24, 2026 16:28
Comment thread src/sentry/sentry_apps/tasks/sentry_apps.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

might need to export this task 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants