Skip to content

Commit ee59edc

Browse files
committed
fix(triggers): restore jira fieldFilters as a real feature, fix twilio-voice status collision
jira: the second-pass audit removed the `fieldFilters` subBlock on issue_updated as "dead code, never read" — true, but that meant the feature was already non-functional before removal (a UI control promising field-level filtering that did nothing), and removing it silently dropped an already-visible control from the merged block UI (flagged independently by both Greptile and Cursor). Rather than either reinstating a broken control or leaving it removed, implement the feature for real: matchEvent now checks the comma-separated field list against Jira's changelog.items[].field on issue_updated deliveries, matching Jira's actual webhook payload shape. twilio-voice: extractIdempotencyId keyed on CallSid alone, so every status callback for a call (ringing/in-progress/completed/etc) shared one idempotency key and only the first was ever processed — later CallStatus transitions were silently dropped as duplicates. Fixed to include CallStatus in the key, matching the SMS handler's existing SID:status pattern. Also converted an inline rationale comment in the SMS handler to TSDoc for consistency with the rest of this cleanup.
1 parent e729a32 commit ee59edc

6 files changed

Lines changed: 158 additions & 5 deletions

File tree

apps/sim/lib/webhooks/providers/jira.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,63 @@ describe('Jira webhook provider', () => {
9090
expect(result).toBe(false)
9191
})
9292

93+
it('matchEvent applies fieldFilters on issue_updated, matching a changed field', async () => {
94+
const result = await jiraHandler.matchEvent!({
95+
body: {
96+
webhookEvent: 'jira:issue_updated',
97+
changelog: { items: [{ field: 'status', from: 'Open', to: 'Done' }] },
98+
},
99+
requestId: 't7',
100+
providerConfig: { triggerId: 'jira_issue_updated', fieldFilters: 'status, assignee' },
101+
webhook: {},
102+
workflow: {},
103+
request: reqWithHeaders({}),
104+
})
105+
expect(result).toBe(true)
106+
})
107+
108+
it('matchEvent applies fieldFilters on issue_updated, skipping when no filtered field changed', async () => {
109+
const result = await jiraHandler.matchEvent!({
110+
body: {
111+
webhookEvent: 'jira:issue_updated',
112+
changelog: { items: [{ field: 'description', from: 'a', to: 'b' }] },
113+
},
114+
requestId: 't8',
115+
providerConfig: { triggerId: 'jira_issue_updated', fieldFilters: 'status, assignee' },
116+
webhook: {},
117+
workflow: {},
118+
request: reqWithHeaders({}),
119+
})
120+
expect(result).toBe(false)
121+
})
122+
123+
it('matchEvent ignores fieldFilters for other trigger types', async () => {
124+
const result = await jiraHandler.matchEvent!({
125+
body: { webhookEvent: 'jira:issue_created' },
126+
requestId: 't9',
127+
providerConfig: { triggerId: 'jira_issue_created', fieldFilters: 'status' },
128+
webhook: {},
129+
workflow: {},
130+
request: reqWithHeaders({}),
131+
})
132+
expect(result).toBe(true)
133+
})
134+
135+
it('matchEvent matches any field change when fieldFilters is empty', async () => {
136+
const result = await jiraHandler.matchEvent!({
137+
body: {
138+
webhookEvent: 'jira:issue_updated',
139+
changelog: { items: [{ field: 'description', from: 'a', to: 'b' }] },
140+
},
141+
requestId: 't10',
142+
providerConfig: { triggerId: 'jira_issue_updated' },
143+
webhook: {},
144+
workflow: {},
145+
request: reqWithHeaders({}),
146+
})
147+
expect(result).toBe(true)
148+
})
149+
93150
it('formatInput extracts issue data for the issue_created trigger', async () => {
94151
const { input } = await jiraHandler.formatInput!({
95152
body: {

apps/sim/lib/webhooks/providers/jira.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@ import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
1212

1313
const logger = createLogger('WebhookProvider:Jira')
1414

15+
/**
16+
* `changelog.items[].field` lists the Jira field names that changed in this
17+
* update (only present on issue_updated deliveries). Empty filter list means
18+
* no restriction — match on any field change.
19+
*/
20+
function matchesFieldFilters(body: Record<string, unknown>, fieldFilters: string): boolean {
21+
const wanted = fieldFilters
22+
.split(',')
23+
.map((f) => f.trim().toLowerCase())
24+
.filter(Boolean)
25+
if (wanted.length === 0) return true
26+
27+
const changelog = body.changelog as Record<string, unknown> | undefined
28+
const items = Array.isArray(changelog?.items) ? changelog.items : []
29+
const changedFields = items
30+
.map((item) => (isRecordLike(item) && typeof item.field === 'string' ? item.field : ''))
31+
.map((f) => f.toLowerCase())
32+
33+
return wanted.some((field) => changedFields.includes(field))
34+
}
35+
1536
export function validateJiraSignature(secret: string, signature: string, body: string): boolean {
1637
try {
1738
if (!secret || !signature || !body) {
@@ -132,6 +153,19 @@ export const jiraHandler: WebhookProviderHandler = {
132153
)
133154
return false
134155
}
156+
157+
const fieldFilters = providerConfig.fieldFilters as string | undefined
158+
if (
159+
triggerId === 'jira_issue_updated' &&
160+
fieldFilters &&
161+
!matchesFieldFilters(obj, fieldFilters)
162+
) {
163+
logger.debug(
164+
`[${requestId}] Jira issue_updated field filter did not match for trigger ${triggerId}. Skipping execution.`,
165+
{ webhookId: webhook.id, workflowId: workflow.id, fieldFilters }
166+
)
167+
return false
168+
}
135169
}
136170

137171
return true

apps/sim/lib/webhooks/providers/twilio-voice.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,36 @@ describe('twilioVoiceHandler', () => {
8888
expect(twilioVoiceHandler.extractIdempotencyId!('not-an-object')).toBeNull()
8989
expect(twilioVoiceHandler.extractIdempotencyId!([1, 2, 3])).toBeNull()
9090
})
91+
92+
it('distinguishes each CallStatus transition for the same CallSid', () => {
93+
const ringing = twilioVoiceHandler.extractIdempotencyId!({
94+
CallSid: 'CA1',
95+
CallStatus: 'ringing',
96+
})
97+
const inProgress = twilioVoiceHandler.extractIdempotencyId!({
98+
CallSid: 'CA1',
99+
CallStatus: 'in-progress',
100+
})
101+
const completed = twilioVoiceHandler.extractIdempotencyId!({
102+
CallSid: 'CA1',
103+
CallStatus: 'completed',
104+
})
105+
expect(ringing).not.toBeNull()
106+
expect(ringing).not.toBe(inProgress)
107+
expect(inProgress).not.toBe(completed)
108+
})
109+
110+
it('dedupes a retried delivery of the same CallStatus transition', () => {
111+
const first = twilioVoiceHandler.extractIdempotencyId!({
112+
CallSid: 'CA1',
113+
CallStatus: 'completed',
114+
})
115+
const retry = twilioVoiceHandler.extractIdempotencyId!({
116+
CallSid: 'CA1',
117+
CallStatus: 'completed',
118+
})
119+
expect(first).toBe(retry)
120+
})
91121
})
92122

93123
describe('formatInput', () => {

apps/sim/lib/webhooks/providers/twilio-voice.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ export const twilioVoiceHandler: WebhookProviderHandler = {
1414
return verifyTwilioAuth(ctx, 'Twilio Voice')
1515
},
1616

17+
/**
18+
* A call fires multiple status callbacks against the same CallSid as it
19+
* progresses (queued -> ringing -> in-progress -> completed/busy/failed/
20+
* no-answer/canceled), so CallStatus is part of the key to keep each
21+
* transition distinct while still deduping Twilio's retries of the same
22+
* status.
23+
*/
1724
extractIdempotencyId(body: unknown) {
1825
if (!isRecordLike(body)) return null
19-
return (body.MessageSid as string) || (body.CallSid as string) || null
26+
const sid = (body.MessageSid as string) || (body.CallSid as string)
27+
if (!sid) return null
28+
const status = ((body.CallStatus as string) ?? '').toLowerCase()
29+
return status ? `${sid}:${status}` : sid
2030
},
2131

2232
formatSuccessResponse(providerConfig: Record<string, unknown>) {

apps/sim/lib/webhooks/providers/twilio.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,18 @@ export const twilioHandler: WebhookProviderHandler = {
4747
return true
4848
},
4949

50+
/**
51+
* Status callbacks repeat for the same SID as the message progresses
52+
* (sent -> delivered -> ...), so the delivery status is part of the key to
53+
* keep each distinct callback (while still deduping Twilio's retries of
54+
* the same status). Inbound messages fire once (SmsStatus 'received'),
55+
* keyed by SID alone.
56+
*/
5057
extractIdempotencyId(body: unknown) {
5158
if (!isRecordLike(body)) return null
5259
const obj = body
5360
const sid = (obj.MessageSid as string) || (obj.CallSid as string)
5461
if (!sid) return null
55-
// Status callbacks repeat for the same SID as the message progresses
56-
// (sent -> delivered -> ...), so the delivery status is part of the key to
57-
// keep each distinct callback (while still deduping Twilio's retries of the
58-
// same status). Inbound messages fire once (SmsStatus 'received'), keyed by SID.
5962
const status = (
6063
((obj.MessageStatus as string) || (obj.SmsStatus as string)) ??
6164
''

apps/sim/triggers/jira/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,25 @@ function jiraJqlFilterField(triggerId: string, description: string): SubBlockCon
7676
}
7777
}
7878

79+
function jiraFieldFiltersField(triggerId: string): SubBlockConfig {
80+
return {
81+
id: 'fieldFilters',
82+
title: 'Field Filters',
83+
type: 'long-input',
84+
placeholder: 'status, assignee, priority',
85+
description:
86+
'Comma-separated list of Jira field names. Only trigger when one of these fields changes. Leave empty to trigger on any field change.',
87+
required: false,
88+
mode: 'trigger',
89+
condition: { field: 'selectedTriggerId', value: triggerId },
90+
}
91+
}
92+
7993
/**
8094
* Extra fields for Jira triggers (webhook secret, plus an optional JQL filter
8195
* for issue-scoped events — project/sprint/version events have no JQL analog).
96+
* `fieldFilters` is issue_updated-only since it's matched against that
97+
* event's changelog, which no other Jira webhook carries.
8298
*/
8399
export function buildJiraExtraFields(
84100
triggerId: string,
@@ -88,6 +104,9 @@ export function buildJiraExtraFields(
88104
if (jqlFilterDescription) {
89105
fields.push(jiraJqlFilterField(triggerId, jqlFilterDescription))
90106
}
107+
if (triggerId === 'jira_issue_updated') {
108+
fields.push(jiraFieldFiltersField(triggerId))
109+
}
91110
return fields
92111
}
93112

0 commit comments

Comments
 (0)