Skip to content

feat(events)!: Let event automations run for any resource - #2407

Open
sentry-junior[bot] wants to merge 3 commits into
mainfrom
feat/event-first-triggers
Open

sentry-junior[bot] wants to merge 3 commits into
mainfrom
feat/event-first-triggers

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Slice B of item 7 in #2362 (event-first triggers). Refs #2362.

Needs re-review. The existing approval is on head 2a7aca0f8. Commits 310bc0b34 and b999350c9 apply the fixes from two design reviews and change the published plugin contract (see Breaking changes). Please review again before merge.

An event automation trigger can now leave out identifier and resourceType. It then runs for every resource of its namespace that its filters match. Two examples: "any Sentry metric_alert.critical where projects includes javascript", and "any pull_request.opened where repo is getsentry/junior". Saved triggers that have an identifier work as before. Watches do not change. This PR does not convert saved rows (slice C) or drop the column (slice D).

Event reach

Each source now declares every event type once, in events, with its Event reach (defined in TERMINOLOGY.md) and optional guidance:

events: {
  "deployment.failed": { reach: "all" },
  "pull_request.opened": { reach: { anyOf: ["repo"] }, guidance: "…" },
  "message.posted": { reach: "resource" },
}
  • "all": a trigger with no identifier can run for every resource, with no filter.
  • { anyOf }: a trigger with no identifier must set at least one of these filters.
  • "resource": the trigger needs an identifier.

Registration fails when an event type has no declaration, or when an anyOf filter is not set on that event. Writing less now grants less. The name still fits after slice D removes identifiers.

Source Events Reach
GitHub deployment.* all. Status facts only, no reaction, no lookups.
GitHub release.published, issue.*, pull_request.* { anyOf: ["repo"] }. Text from repos that can be private; comments and reviews add a reaction.
Sentry issue.created, metric_alert.* all. One install is one org.
Vercel deployment.* all. Status facts only.
Linear issue.created { anyOf: ["teamKey", "issueIdentifier"] }.
Slack message.posted resource.
Core workspace_snapshot.*, timer.fired resource.

How it stays safe and correct

  • Lazy lookups only for matching repos. neededMatchKeys takes the event data the plugin already knows. GitHub passes repo for pull request and check suite webhooks. Core skips a row with no identifier whose filters on that data fail, so a trigger for one repo loads no PR facts for other repos.
  • Runtime gate. SQL selects rows by team, namespace, identifier = event.identifier or identifier is null, and event type (record->'trigger'->'events' @> [type]). In JS, a saved identifier must equal the row column, and a row with no identifier must have a trigger that the current catalog accepts. Rows that do not parse are logged and skipped (safeParse), so one bad row cannot break ingest for a namespace. Gate rejections log event_automation.gate.rejected with the automation id and reason.
  • No widening by omission. updateEventAutomation keeps the saved identifier and resourceType when they are left out. Only an explicit null removes them.
  • Filter case. A match field can declare case: "lower" | "upper". Core saves filter values in that case (create, update, dashboard, and Watches), so repo: "getsentry/Junior" matches. GitHub repo, commitSha, environment, Sentry projects, and Vercel commitSha are lower; Linear teamKey and issueIdentifier are upper.
  • One shape. Reach is given once per namespace in the catalog, in searchEventTypes output (namespaces[].reach), and in the dashboard event catalog API (one entry per namespace with displayName, events, anyResourceFields, resourceTypes). Core merges the filters of every resource type once when it builds the catalog.
  • Plugins publish one copy of an event for each scope, and all copies share one event key and the same data. The dispatch key removes the extra copies, and the run limit counts a key once.
  • The dashboard list uses eventTriggerProblems, the same check as the tools and the editor. The editor shows the service display name, says which filter an event needs with the filter description, and asks for a "Trigger name" instead of a "Resource name" when there is no identifier.
  • Docs: a required filter limits scope and noise. It is not access control. Nothing checks that the creator can see the repo or project, so broad Sentry and GitHub deployment triggers can post private project or repo names to the destination channel. Each plugin page lists its broad events and their filters.

Breaking changes

  • Plugin API: PluginEvents.anyResourceEvents and PluginEventType.guidance are removed. PluginEvents.events is required and must declare every supported event with reach and optional guidance. sourceEventGuidance and anyResourceEventSchema are removed; eventReachSchema, pluginEventDeclarationSchema, and normalizeEventMatch are added. Match fields accept case.
  • eventTriggerProblems / describeEventTrigger / eventTriggerChoices: EventTriggerSource is now { resourceTypes, events, anyResourceFields }.
  • EventPublisher.neededMatchKeys accepts optional data.
  • Tool output and dashboard API: identifier and resourceType can be null in event automation results. searchEventTypes drops per-resource-type anyResourceEvents and adds namespaces. /api/automations/event-catalog returns one entry per namespace instead of one per resource type.

Rollback

After any automation with no identifier is saved, the previous release cannot read it: its schema requires identifier, and those rows fail to parse. Before rolling back, pause or delete rows where kind = 'event' and identifier is null.

Next (slice C)

  • Each plugin owns an identifier-to-filters converter, so core never parses identifier formats.
  • Slack channel needs a channelId filter before slice D.
  • Sentry metric alerts with too many projects drop projects; slice C keeps their identifiers.
  • Match broad rows once per event key after slice D.
  • Dashboard availability memo or stored column. The catalog is still rebuilt on each getEventCatalog() call; it merges filters once per build.

via David Cramer.

--

View Session [Sentry]

@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Oct 30, 2026 5:33am UTC

Request Review

An event automation trigger can now leave out the identifier and the
resource type. It then runs for every resource of its namespace that its
filters match, such as any Sentry metric alert for one project.

Each plugin declares in `anyResourceEvents` which events can run this way,
and the filter an event needs. GitHub issue, pull request, and release
events need `repo`. Linear issues need `teamKey`. Sentry and Vercel events
and GitHub deployments need no filter. Slack channel messages keep their
identifier.

- Match `identifier = event.identifier or identifier is null` and load
  lazy filters for rows with no identifier.
- Count each event key once in the run limit, so many copies of one event
  do not block the automation.
- Give guidance by namespace and event, and reject plugins whose resource
  types disagree.
- Run the shared trigger check for the dashboard list instead of a SQL copy.
- Allow a null identifier for event rows.

Refs #2362

Co-Authored-By: David Cramer <david@sentry.io>
@sentry-evals

sentry-evals Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Sentry Evals

junior-behavioral

Run Status Passed Failed Errored Cost Tokens Duration
Head Error 76 4 2 $0.57 0 1,074.23s

Base commit 8c45aeb has no eval run to compare against.

junior-guardian

Run Status Passed Failed Errored Cost Tokens Duration
Head Passed 94 0 0 $0.03 0 23.70s

Base commit 8c45aeb has no eval run to compare against.

junior-integration

Run Status Passed Failed Errored Cost Tokens Duration
Head Failed 101 1 0 $0.61 0 511.10s

Base commit 8c45aeb has no eval run to compare against.

@dcramer

dcramer commented Oct 30, 2026

Copy link
Copy Markdown
Member

What's the co figuration look like now? Like field name = value. What are the fields for a few scenarios?

@sentry-junior

sentry-junior Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

A trigger is now namespace + events + match. identifier and resourceType are optional and go together: set both for one exact resource, or leave both out to run for every resource that the filters match. Each match entry is field = value. A list value means "any of these". For a list field, such as projects or authors, the filter matches when the event's list includes the value.

Any new PR in one repo, opened by someone with write access

{ "namespace": "github", "events": ["pull_request.opened"],
  "match": { "repo": "getsentry/junior", "openerRole": ["admin", "maintain", "write"] } }

repo is required for GitHub pull_request.*, issue.* and release.published. Without it, you get: This trigger needs a "repo" filter (match.repo), because "pull_request.opened" cannot run for every resource.

Any PR I co-authored, in two repos, not drafts

{ "namespace": "github", "events": ["pull_request.opened", "pull_request.ready_for_review"],
  "match": { "repo": ["getsentry/junior", "getsentry/junior-prod"], "authors": "dcramer", "isDraft": false } }

Any failed GitHub deployment to production, in any repo (no filter required)

{ "namespace": "github", "events": ["deployment.failed", "deployment.error"],
  "match": { "environment": "production" } }

Any critical Sentry metric alert that covers the javascript project (no filter required; one install is one org)

{ "namespace": "sentry", "events": ["metric_alert.critical"],
  "match": { "projects": "javascript" } }

Any new high-priority Sentry issue in two projects

{ "namespace": "sentry", "events": ["issue.created"],
  "match": { "projects": ["junior", "sentry"], "priority": "high" } }

Any new Linear issue in one team (teamKey required)

{ "namespace": "linear", "events": ["issue.created"], "match": { "teamKey": "ENG" } }

Any production Vercel deploy of one project (no filter required)

{ "namespace": "vercel", "events": ["deployment.succeeded", "deployment.error"],
  "match": { "projectId": "prj_abc123", "target": "production" } }

Still exact-resource (unchanged): one PR, or a Slack channel, which always needs its identifier:

{ "namespace": "github", "resourceType": "pull_request", "identifier": "getsentry/junior#2407",
  "events": ["pull_request.merged"] }

Filter fields per namespace:

  • GitHub:
    • repo on all events.
    • pullRequest, authors (list), headBranch, isDraft, openerUsername, openerEmail and openerRole on PR events.
    • commitSha on PR check events and deployments.
    • environment, deploymentId, workflowId and workflowPath on deployments.
    • issue on issue events.
  • Sentry:
    • projects (list) on issues and metric alerts.
    • level, priority, issueCategory and issueId on issues.
    • alertRuleId on metric alerts.
  • Linear: teamKey and issueIdentifier.
  • Vercel: projectId, deploymentId, target (preview/production/staging) and commitSha.

Every selected event must set each filter field. For example, headBranch with pull_request.comment.created is rejected, because that webhook has no head branch. searchEventTypes lists the fields, the events that set each one, and anyResourceEvents with any requiredFilter.

Each plugin now declares every event type once in `events`, with its
reach ("all", { anyOf: [filters] }, or "resource") and optional
guidance. Registration fails when an event has no declaration. This
replaces `anyResourceEvents`, where `{}` meant "run for every resource",
and per-resource-type `guidance`.

- Pass known event data (repo) to neededMatchKeys, so a broad trigger
  for one repo loads no lazy PR data for other repos.
- Runtime gate: identifier column and trigger must agree, bad rows are
  logged and skipped, and rejected rows log a warning.
- updateEventAutomation keeps the saved identifier and resource type
  when they are left out; only an explicit null removes them.
- Match fields declare `case`, and core saves filter values in it.
- The catalog, searchEventTypes, and the dashboard API give reach once
  per namespace, and filters are merged once per catalog build.
- Docs say that a required filter is not access control.

Refs #2362

Co-Authored-By: David Cramer <david@sentry.io>
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

9 screenshot changes — 7 changed · 2 added · 0 removed

Review screenshots in Frameshift

Automation Editor Conflict · Desktop
Automation Editor Conflict · Desktop
Changed
Automation Editor Conflict · Mobile
Automation Editor Conflict · Mobile
Changed
Automation Editor Public · Desktop
Automation Editor Public · Desktop
Changed
Automation Editor Public · Mobile
Automation Editor Public · Mobile
Changed
Automation Event Editor · Desktop
Automation Event Editor · Desktop
Changed
Automation Event Editor · Mobile
Automation Event Editor · Mobile
Changed
Show 1 more changed screenshot
Conversation Attachment · Desktop
Conversation Attachment · Desktop
Changed

Check the namespace reach of the event catalog with a shorter assertion, so the file-length check passes.
@sentry-junior sentry-junior Bot changed the title feat(events): Let event automations run for any resource feat(events)!: Let event automations run for any resource Oct 30, 2026

This branch was successfully deployed

1 active deployment
Preview – junior-docs — b999350c Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant