Skip to content

feat(events)!: Convert event automation identifiers to filters - #2442

Closed
sentry-junior[bot] wants to merge 8 commits into
mainfrom
feat/event-automation-filter-conversion
Closed

sentry-junior[bot] wants to merge 8 commits into
mainfrom
feat/event-automation-filter-conversion

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

What this does

Today an event automation can point at one thing in two ways:

  • an identifier, such as getsentry/junior#7, or
  • filters, such as repo: getsentry/junior, pullRequest: 7.

#2407 added filters. This PR starts to move everyone to filters, so later we can delete identifiers (#2362).

When someone creates or edits an event automation and gives an identifier, Junior now saves filters that match exactly the same events. The automation runs exactly as before. It does not start to run for more things.

This PR does not change automations that are already saved. They keep running as they are. A separate draft PR, #2447, converts them later.

Refs #2362

What it looks like

Someone asks: "tell me when a review is posted on #7".

The agent still sends this to createEventAutomation:

{ "namespace": "github", "resourceType": "pull_request", "identifier": "getsentry/junior#7", "events": ["pull_request.review.commented"] }

Junior now saves this:

{ "namespace": "github", "events": ["pull_request.review.commented"], "match": { "repo": "getsentry/junior", "pullRequest": 7 } }

The dashboard and the automation details show the filters.

What each plugin converts:

Plugin Saved as filters Stays an identifier
GitHub repo, issue or PR number, release tag, deployment environment identifiers in a form that GitHub webhooks never send
Linear team key, issue key (ENG-123) identifiers in a form that Linear webhooks never send
Vercel project, target, commit identifiers in a form that Vercel webhooks never send
Slack channel none
Sentry none all of them

When Junior keeps an identifier, the automation still works. Its details show a short note that says why.

Sentry keeps identifiers because each Sentry identifier contains an org name, and Junior has no way to check that org. If Junior converted an identifier with an old or wrong org, an automation that never ran could start to run for the wrong things.

Changes to updating an automation

This is the part that changes behavior. It only affects updateEventAutomation, the tool that the agent uses.

Before this PR, a new match replaced all saved filters. After conversion, the filters are the thing that keeps an automation pointed at one PR. So "also skip drafts" could quietly turn "PR #7" into "every PR in the repo".

Now match changes only the filters that it names:

Update Saved before Saved after
match: { isDraft: false } repo, pullRequest: 7 repo, pullRequest: 7, isDraft: false
match: { pullRequest: null } repo, pullRequest: 7 repo (every PR in the repo)
match: { pullRequest: [7, 8] } repo, pullRequest: 7 repo, pullRequest: [7, 8]
match: {} anything no change
match: { repo: null } repo, pullRequest: 7 rejected: GitHub PR events need repo

The identifier rules from #2407 stay the same: leave out identifier to keep it, and set it to null to drop it.

The dashboard editor shows every filter and saves the whole trigger, so it works the same as before.

Junior now also rejects a trigger that can never run. An example is identifier getsentry/junior#7 with filter pullRequest: 5. The error names the filter. Before, Junior saved the trigger, and it never ran.

What might break

  • Agent updates that clear filters. match: {} used to remove all filters. Now it does nothing. To remove a filter, set it to null.
  • Agent updates that replace filters. A new match used to drop the filters that it left out. Now they stay.
  • Triggers that contradict themselves now fail to save with an error, instead of saving and never running.
  • Rollback. No release has feat(events)!: Let event automations run for any resource #2407 yet. Release 0.266.0 and earlier cannot read an automation with no identifier: one such row breaks the automation list for its whole Slack workspace. Once this ships and people create or edit automations, rolling back past feat(events)!: Let event automations run for any resource #2407 has that problem. feat(events)!: Let event automations run for any resource #2407 alone already has the same limit. Each change also saves a version with the old trigger, so a person can restore it from version history.
  • Plugins. Plugins get a new optional hook, identifierMatch, that turns an identifier into filters. Plugins that do not add it keep identifiers. It is temporary, and a later change removes it with identifiers.

Smaller changes

Checks (head e9711e4c3)

--

View Session [Sentry]

Each plugin can now turn its saved identifiers into the filters that
select the same events (`identifierMatch` in plugin-api). Core uses it to
save new and edited event automation triggers with no identifier, and the
heartbeat converts saved rows one at a time through the locked, versioned
save. A trigger converts only when every selected event gets the same
filters, the filters agree with saved filters, and the result passes the
catalog check. Otherwise the row keeps its identifier and keeps running,
so no trigger gets broader.

Slack channel automations now save a `channelId` filter for their
destination channel. Sentry project identifiers do not convert for metric
alert events, because those events can leave out `projects`. An omitted
`match` on update now keeps the saved filters.

Watches keep exact identifiers. The identifier column stays for slice D.

Refs #2362

Co-Authored-By: David Cramer <david@sentry.io>
@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 7:36pm UTC

Request Review

@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 79 1 2 $0.73 0 569.36s

Base commit faca571 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 26.22s

Base commit faca571 has no eval run to compare against.

junior-integration

Run Status Passed Failed Errored Cost Tokens Duration
Head Failed 104 1 0 $0.63 0 365.42s

Base commit faca571 has no eval run to compare against.

junior-router

Run Status Passed Failed Errored Cost Tokens Duration
Head Passed 12 0 0 $0.00 0 3.20s

Base commit faca571 has no eval run to compare against.

A trigger that cannot convert to filters now saves the reason in
`identifierReason` through the same locked, versioned save. The heartbeat
checks at most 50 rows per tick that have an identifier and no saved
reason, so it checks each row once. It skips rows whose identifier column
and saved trigger disagree, because a save would make them broad. Edits
that change the trigger set the reason again.

The trigger description shows the reason as a note, not a problem, in the
dashboard details, Slack details, and listEventAutomations. Rows that keep
their identifier stay active.

The update tool description now says plainly that leaving out match keeps
the saved filters, and only `{}` removes them.

Refs #2362

Co-Authored-By: David Cramer <david@sentry.io>
@sentry-junior sentry-junior Bot changed the title feat(events): Convert event automation identifiers to filters feat(events)!: Convert event automation identifiers to filters Oct 30, 2026
@blacksmith-sh

This comment has been minimized.

Updates now change match filters key by key. A left-out filter keeps its
saved value and null removes it, so an update cannot drop the scope that a
conversion moved into match. A write whose saved filter never matches its
identifier is rejected instead of saved as a trigger that never runs.

Move the heartbeat backfill of saved rows to a follow-up, because no
release can read rows with no identifier yet. Drop the Sentry converter,
because the plugin has no saved install org to check identifiers against.
Pass the one pull request to the lazy GitHub gate, mark identifierMatch as
temporary, and test identifiers that do not rebuild and events that need
different filters.

Refs #2362

Co-Authored-By: David Cramer <david@sentry.io>
The version schema accepts editedBy "junior" and the dashboard shows
Junior as the editor. Nothing writes it yet. The reader ships first, so
the follow-up that converts saved triggers can roll back to this release
and keep version history readable.

Refs #2362

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

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

4 screenshot changes — 4 changed · 0 added · 0 removed

Review screenshots in Frameshift

Automation Versions · Desktop
Automation Versions · Desktop
Changed
Automation Versions · Mobile
Automation Versions · Mobile
Changed
Conversation Child Agent Drawer · Desktop
Conversation Child Agent Drawer · Desktop
Changed
Conversation Child Agent Drawer · Mobile
Conversation Child Agent Drawer · Mobile
Changed

A repository identifier and a repo filter now save the same trigger with
no identifier, so the eval checks the saved trigger.

Refs #2362
@dcramer

dcramer commented Oct 30, 2026

Copy link
Copy Markdown
Member

the pr description is far too technical and it doesnt explain to me what we're actually doing, what the actual final interface/configurations look like, and what might break

@dcramer

dcramer commented Oct 30, 2026

Copy link
Copy Markdown
Member

is there value in supporting the identifier the way we are? like since its just duplicating matches and it'd be legacy? rying to understand the API interface. maybe we tihnk the simplicity is required to prevent agent errors or something?

what about idempotency keys and deduplication how will that function?

dcramer pushed a commit that referenced this pull request Oct 30, 2026
Since #2440 (`15ee5c130`), an automation run no longer writes its saved
visibility to the Destination row. Only a live Slack signal does. A run
into a channel with no row stores `unknown`, and readers treat it as
private.

In production, the Slack thread where a person created the Automation
records the channel. The eval fixture inserted Automations without that
thread, so their channels stayed `unknown`. The fixture reads results as
`WEB_VIEWER_EMAIL`, who is not a participant. So
`readConversationAccessFromSql` returned `canViewPrivateContent: false`,
and the reporting API hid tool payloads for every scheduled and event
automation run. Tool calls came back with no input and no output
(`result null [4B]` on every tool). `finishAutomationRun` results and
tool arguments were empty. This broke these cases on unrelated branches
(#2435, #2442, fix/flaky-behavioral-evals, chore/remove-slash-command):

- Integration: `Event automation delivery` (no-match silence ×2, blocked
automation) and `Scheduled Delivery` (no message outcome)
- Behavioral: `Scheduled Delivery` (post in this channel, provider that
Junior does not have) and `Sentry Skill Workflows` (creator-bound
scheduled Sentry work)

There is no product change. Fail-closed `unknown` is intended (#2440).
Automations are created from a live Slack turn in their channel, so
production channels have a known visibility.

## Fix

`packages/junior-evals/src/fixture/insert.ts`:
`insertScheduledAutomation` and `insertEventAutomation` now store the
creation thread through the Conversation store with `visibility:
"public"`, as the live Slack event would. No assertions, cases, or
timeouts change.

## Checks

- `pnpm lint`, `pnpm typecheck`, `pnpm --filter @sentry/junior-evals
test` pass.
- A throwaway harness check (not committed) used the real insert
functions. A dispatch-shaped write into a bare `slackChannel()` was
redacted (`unknown`). Channels from either insert function were readable
(`public`).
- The 7 affected eval cases did not run locally: the sandbox has no
`cloudflared` for eval egress. CI must confirm them.

<!-- junior-request-attribution:start -->
via **David Cramer**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A5d3914babce549420a1ad7b950c41799 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A5d3914babce549420a1ad7b950c41799)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A5d3914babce549420a1ad7b950c41799/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
…-filter-conversion

# Conflicts:
#	packages/junior-dashboard/e2e/automation-editor.spec.ts
#	packages/junior-dashboard/src/client/pages/automations/AutomationDetails.tsx
#	packages/junior-dashboard/src/client/pages/automations/AutomationVersionsPage.tsx
@dcramer

dcramer commented Oct 30, 2026

Copy link
Copy Markdown
Member

the pr description here needs to be simplified. its somewhat easy to understand but its an essay

@dcramer

dcramer commented Nov 2, 2026

Copy link
Copy Markdown
Member

gonna need a rebase

@sentry-junior sentry-junior Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The end state in #2362 is right: an event automation trigger is namespace + events + match, and only Watches keep exact identifiers. The middle step in this PR is the weak part. It keeps identifier as agent input, saves something else, and adds rules to make the two forms agree.

  • Two inputs for one scope. The agent sends identifier: "getsentry/junior#7" and gets back identifier: null plus match: { repo, pullRequest: 7 }. Then the two forms conflict. Example: the saved trigger is converted, and the agent retargets with identifier: "getsentry/junior#8". changedMatch keeps the saved pullRequest: 7, triggerWithoutIdentifier finds a conflict, and the update fails with "the pullRequest filter never matches". This happens even when match is left out. Simpler: cut over now for automation input. Remove identifier/resourceType from the event trigger in createAutomation/updateAutomation, and add match next to identifier in subscribableResourceSchema. Plugins already have the structured parts when they build the identifier (gitHubPullRequestResource({ repo, number })), so no regex parse is needed. Then identifierMatch and its parse code are needed only for the #2447 backfill, not for every create and update. Agents keep the "copy it from the tool result" path, so the cutover should not add agent errors.
  • Out of date with main. #2496 replaced update-event-automation.ts with automations/tools/update.ts. That tool's schema says "Complete replacement trigger". The key-by-key match merge must move there, and that description must change, or the two contradict. The PR also conflicts with main, and integration / shard 1/2 and evals fail.
  • Sentry keeps the identifier path alive. If Sentry triggers never convert, slice D cannot drop the column, and "temporary" identifierMatch stays. Decide now. One option: new Sentry triggers use org/projects filters taken from tool results, and only old Sentry rows keep identifiers until they are checked.

On dedupe: it still works. The dispatch key and the run-limit key are both hash(automationId, namespace, eventKey), and every fanned-out copy shares one eventKey. So a filter-only row that matches the PR copy and the repo copy runs once. One cost changes: converted rows stop using the indexed identifier IN (...) lookup. Every copy of every event now reads and checks in JS each active row with no identifier in that team, namespace, and event type. At today's row counts that is fine.

@sentry-junior

sentry-junior Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of #2572, which does a hard cutover on agent input: event triggers are namespace + events + match, and subscribable results carry match, so identifiers are not accepted and converted. See the review: #2442 (review). The branch stays because #2447 is stacked on it.

@sentry-junior sentry-junior Bot closed this Nov 2, 2026
dcramer added a commit that referenced this pull request Nov 2, 2026
…rs (#2572)

Hard cutover on agent input for event automation triggers, as proposed
in the [#2442
review](#2442 (review)).
An event trigger from `createAutomation` / `updateAutomation` is now
`namespace + events + match`. Watches keep exact identifiers. Refs
#2362.

**Tool input**
- `identifier` and `resourceType` are gone from the event trigger schema
of both tools.
- `updateAutomation` changes `match` key by key: a value sets a filter,
`null` removes it, and a filter left out keeps its saved value. So an
update can't broaden a trigger by leaving a filter out. A namespace
change starts with no filters.
- `searchEventTypes` now describes reach in terms of `match`. `reach:
"resource"` events (timers, workspace snapshots) are only usable by
Watches.

**Subscribable results**
- `SubscribableResource` gets an optional `match`. It is missing for
Watch-only resources.
- GitHub fills it from the parts it already has: PR `{repo,
pullRequest}`, issue `{repo, issue}`, repo `{repo}`, deployment `{repo,
commitSha, environment?}`, release `{repo, tag?}`. Vercel deployment
fills `{projectId, target?, commitSha?}`. No identifier parsing.
- Linear and Sentry have no subscribable builders. Their match fields
(`teamKey`/`issueIdentifier`, `projects`/`issueId`/`alertRuleId`)
already cover single-resource scoping, so Sentry does not block dropping
the identifier.

**Slack channel messages**
- `message.posted` moves from `reach: "resource"` to `{ anyOf:
["channelId"] }`. The tools set `channelId` from the destination
channel, and `requireSlackChannelMessageScope` still rejects other
channels.

**Saved triggers with an identifier**
- They keep running, and dispatch is unchanged. Tool edits in the same
namespace keep the saved identifier.
- Known gap until #2447 converts saved rows: if an agent adds a filter
that conflicts with a saved identifier, such as `pullRequest: 8` on a
row saved as `#7`, the trigger stops matching. Core
can't detect that without the plugin conversion that #2447 adds.
- The dashboard editor still edits identifiers on saved rows. It should
switch over with #2447.

**Dedupe:** no change. Runs are keyed on `(automationId, namespace,
eventKey)`, so a filter-only automation that matches the PR copy and the
repo copy of one webhook still runs once.

**Verification**
- `pnpm typecheck` and `pnpm lint` pass.
- Automation integration suites pass (66 tests), as do the GitHub plugin
suite (266) and the Vercel plugin suite (33).
- In the full `@sentry/junior` run, these failed in this sandbox:
`agent-dispatch-work`, `annotation-cards`, `durable-queue`,
`event-wake-delay`, `bot-image-hydration`, and the generated-title
assertion in `event-automations`. They also fail on unmodified `main` in
the same sandbox (mostly 5s timeouts), so I'm treating them as
environment failures. CI is the real check.
- I didn't run the evals locally. Updated assertions are in
`event-automations/management`, `credentials`, and
`guardian/scheduled-work`.

<!-- junior-request-attribution:start -->
via @dcramer.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC0B595QDZLL%3A1793654465.198809 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/slack%3AC0B595QDZLL%3A1793654465.198809)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC0B595QDZLL%3A1793654465.198809/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: David Cramer <23610+dcramer@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview – junior-docs — 9987344f 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

Labels

risk: high PR risk score: high trigger-evals Run evals on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant