Skip to content

Commit 98908db

Browse files
fix(triggers): dedup + not surfacing deployment status log (#2033)
* fix(triggers): dedup + not surfacing deployment status log * fix ms teams * change to microsoftteams * Revert "change to microsoftteams" This reverts commit 217f808. * fix * fix * fix provider name * fix oauth for msteams
1 parent 00d9b45 commit 98908db

File tree

12 files changed

+165
-50
lines changed

12 files changed

+165
-50
lines changed

apps/sim/app/api/cron/renew-subscriptions/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { db } from '@sim/db'
22
import { webhook as webhookTable, workflow as workflowTable } from '@sim/db/schema'
3-
import { and, eq } from 'drizzle-orm'
3+
import { and, eq, or } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { verifyCronAuth } from '@/lib/auth/internal'
66
import { createLogger } from '@/lib/logs/console/logger'
@@ -35,7 +35,15 @@ export async function GET(request: NextRequest) {
3535
})
3636
.from(webhookTable)
3737
.innerJoin(workflowTable, eq(webhookTable.workflowId, workflowTable.id))
38-
.where(and(eq(webhookTable.isActive, true), eq(webhookTable.provider, 'microsoftteams')))
38+
.where(
39+
and(
40+
eq(webhookTable.isActive, true),
41+
or(
42+
eq(webhookTable.provider, 'microsoft-teams'),
43+
eq(webhookTable.provider, 'microsoftteams')
44+
)
45+
)
46+
)
3947

4048
logger.info(
4149
`Found ${webhooksWithWorkflows.length} active Teams webhooks, checking for expiring subscriptions`

apps/sim/app/api/webhooks/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export async function POST(request: NextRequest) {
137137
const isCredentialBased = credentialBasedProviders.includes(provider)
138138
// Treat Microsoft Teams chat subscription as credential-based for path generation purposes
139139
const isMicrosoftTeamsChatSubscription =
140-
provider === 'microsoftteams' &&
140+
provider === 'microsoft-teams' &&
141141
typeof providerConfig === 'object' &&
142142
providerConfig?.triggerId === 'microsoftteams_chat_subscription'
143143

@@ -297,7 +297,7 @@ export async function POST(request: NextRequest) {
297297
}
298298
}
299299

300-
if (provider === 'microsoftteams') {
300+
if (provider === 'microsoft-teams') {
301301
const { createTeamsSubscription } = await import('@/lib/webhooks/webhook-helpers')
302302
logger.info(`[${requestId}] Creating Teams subscription before saving to database`)
303303
try {

apps/sim/app/api/webhooks/test/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ export async function GET(request: NextRequest) {
441441
})
442442
}
443443

444-
case 'microsoftteams': {
444+
case 'microsoft-teams': {
445445
const hmacSecret = providerConfig.hmacSecret
446446

447447
if (!hmacSecret) {

apps/sim/app/api/webhooks/trigger/[path]/route.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { type NextRequest, NextResponse } from 'next/server'
2-
import { v4 as uuidv4 } from 'uuid'
32
import { createLogger } from '@/lib/logs/console/logger'
4-
import { LoggingSession } from '@/lib/logs/execution/logging-session'
53
import { generateRequestId } from '@/lib/utils'
64
import {
75
checkRateLimits,
@@ -139,34 +137,10 @@ export async function POST(
139137
if (foundWebhook.blockId) {
140138
const blockExists = await blockExistsInDeployment(foundWorkflow.id, foundWebhook.blockId)
141139
if (!blockExists) {
142-
logger.warn(
140+
logger.info(
143141
`[${requestId}] Trigger block ${foundWebhook.blockId} not found in deployment for workflow ${foundWorkflow.id}`
144142
)
145-
146-
const executionId = uuidv4()
147-
const loggingSession = new LoggingSession(foundWorkflow.id, executionId, 'webhook', requestId)
148-
149-
const actorUserId = foundWorkflow.workspaceId
150-
? (await import('@/lib/workspaces/utils')).getWorkspaceBilledAccountUserId(
151-
foundWorkflow.workspaceId
152-
) || foundWorkflow.userId
153-
: foundWorkflow.userId
154-
155-
await loggingSession.safeStart({
156-
userId: actorUserId,
157-
workspaceId: foundWorkflow.workspaceId || '',
158-
variables: {},
159-
})
160-
161-
await loggingSession.safeCompleteWithError({
162-
error: {
163-
message: `Trigger block not deployed. The webhook trigger (block ${foundWebhook.blockId}) is not present in the deployed workflow. Please redeploy the workflow.`,
164-
stackTrace: undefined,
165-
},
166-
traceSpans: [],
167-
})
168-
169-
return new NextResponse('Trigger block not deployed', { status: 404 })
143+
return new NextResponse('Trigger block not found in deployment', { status: 404 })
170144
}
171145
}
172146

apps/sim/background/webhook-execution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
112112

113113
const idempotencyKey = IdempotencyService.createWebhookIdempotencyKey(
114114
payload.webhookId,
115-
payload.headers
115+
payload.headers,
116+
payload.body,
117+
payload.provider
116118
)
117119

118120
const runOperation = async () => {

apps/sim/executor/handlers/trigger/trigger-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class TriggerBlockHandler implements BlockHandler {
5555
}
5656
}
5757

58-
if (provider === 'microsoftteams') {
58+
if (provider === 'microsoft-teams') {
5959
const providerData = (starterOutput as any)[provider] || webhookData[provider] || {}
6060
const payloadSource = providerData?.message?.raw || webhookData.payload || {}
6161
return {

apps/sim/lib/idempotency/service.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { idempotencyKey } from '@sim/db/schema'
44
import { and, eq } from 'drizzle-orm'
55
import { createLogger } from '@/lib/logs/console/logger'
66
import { getRedisClient } from '@/lib/redis'
7+
import { extractProviderIdentifierFromBody } from '@/lib/webhooks/provider-utils'
78

89
const logger = createLogger('IdempotencyService')
910

@@ -451,13 +452,25 @@ export class IdempotencyService {
451452

452453
/**
453454
* Create an idempotency key from a webhook payload following RFC best practices
454-
* Standard webhook headers (webhook-id, x-webhook-id, etc.)
455+
* Checks both headers and body for unique identifiers to prevent duplicate executions
456+
*
457+
* @param webhookId - The webhook database ID
458+
* @param headers - HTTP headers from the webhook request
459+
* @param body - Parsed webhook body (optional, used for provider-specific identifiers)
460+
* @param provider - Provider name for body extraction (optional)
461+
* @returns A unique idempotency key for this webhook event
455462
*/
456-
static createWebhookIdempotencyKey(webhookId: string, headers?: Record<string, string>): string {
463+
static createWebhookIdempotencyKey(
464+
webhookId: string,
465+
headers?: Record<string, string>,
466+
body?: any,
467+
provider?: string
468+
): string {
457469
const normalizedHeaders = headers
458470
? Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]))
459471
: undefined
460472

473+
// Check standard webhook headers first
461474
const webhookIdHeader =
462475
normalizedHeaders?.['webhook-id'] ||
463476
normalizedHeaders?.['x-webhook-id'] ||
@@ -470,7 +483,22 @@ export class IdempotencyService {
470483
return `${webhookId}:${webhookIdHeader}`
471484
}
472485

486+
// Check body for provider-specific unique identifiers
487+
if (body && provider) {
488+
const bodyIdentifier = extractProviderIdentifierFromBody(provider, body)
489+
490+
if (bodyIdentifier) {
491+
return `${webhookId}:${bodyIdentifier}`
492+
}
493+
}
494+
495+
// No unique identifier found - generate random UUID
496+
// This means duplicate detection will not work for this webhook
473497
const uniqueId = randomUUID()
498+
logger.warn('No unique identifier found, duplicate executions may occur', {
499+
webhookId,
500+
provider,
501+
})
474502
return `${webhookId}:${uniqueId}`
475503
}
476504
}

apps/sim/lib/oauth/oauth.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,24 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig {
906906
featureType: 'sharepoint',
907907
}
908908
}
909+
if (provider === 'microsoft-teams' || provider === 'microsoftteams') {
910+
return {
911+
baseProvider: 'microsoft',
912+
featureType: 'microsoft-teams',
913+
}
914+
}
915+
if (provider === 'microsoft-excel') {
916+
return {
917+
baseProvider: 'microsoft',
918+
featureType: 'microsoft-excel',
919+
}
920+
}
921+
if (provider === 'microsoft-planner') {
922+
return {
923+
baseProvider: 'microsoft',
924+
featureType: 'microsoft-planner',
925+
}
926+
}
909927

910928
// Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' })
911929
const [base, feature] = provider.split('-')

apps/sim/lib/webhooks/processor.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export async function verifyProviderAuth(
250250
const rawProviderConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
251251
const providerConfig = resolveProviderConfigEnvVars(rawProviderConfig, decryptedEnvVars)
252252

253-
if (foundWebhook.provider === 'microsoftteams') {
253+
if (foundWebhook.provider === 'microsoft-teams') {
254254
if (providerConfig.hmacSecret) {
255255
const authHeader = request.headers.get('authorization')
256256

@@ -556,7 +556,7 @@ export async function checkRateLimits(
556556
traceSpans: [],
557557
})
558558

559-
if (foundWebhook.provider === 'microsoftteams') {
559+
if (foundWebhook.provider === 'microsoft-teams') {
560560
return NextResponse.json(
561561
{
562562
type: 'message',
@@ -634,7 +634,7 @@ export async function checkUsageLimits(
634634
traceSpans: [],
635635
})
636636

637-
if (foundWebhook.provider === 'microsoftteams') {
637+
if (foundWebhook.provider === 'microsoft-teams') {
638638
return NextResponse.json(
639639
{
640640
type: 'message',
@@ -783,7 +783,7 @@ export async function queueWebhookExecution(
783783

784784
// For Microsoft Teams Graph notifications, extract unique identifiers for idempotency
785785
if (
786-
foundWebhook.provider === 'microsoftteams' &&
786+
foundWebhook.provider === 'microsoft-teams' &&
787787
body?.value &&
788788
Array.isArray(body.value) &&
789789
body.value.length > 0
@@ -835,7 +835,7 @@ export async function queueWebhookExecution(
835835
)
836836
}
837837

838-
if (foundWebhook.provider === 'microsoftteams') {
838+
if (foundWebhook.provider === 'microsoft-teams') {
839839
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
840840
const triggerId = providerConfig.triggerId as string | undefined
841841

@@ -886,7 +886,7 @@ export async function queueWebhookExecution(
886886
} catch (error: any) {
887887
logger.error(`[${options.requestId}] Failed to queue webhook execution:`, error)
888888

889-
if (foundWebhook.provider === 'microsoftteams') {
889+
if (foundWebhook.provider === 'microsoft-teams') {
890890
return NextResponse.json(
891891
{
892892
type: 'message',
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Provider-specific unique identifier extractors for webhook idempotency
3+
*/
4+
5+
function extractSlackIdentifier(body: any): string | null {
6+
if (body.event_id) {
7+
return body.event_id
8+
}
9+
10+
if (body.event?.ts && body.team_id) {
11+
return `${body.team_id}:${body.event.ts}`
12+
}
13+
14+
return null
15+
}
16+
17+
function extractTwilioIdentifier(body: any): string | null {
18+
return body.MessageSid || body.CallSid || null
19+
}
20+
21+
function extractStripeIdentifier(body: any): string | null {
22+
if (body.id && body.object === 'event') {
23+
return body.id
24+
}
25+
return null
26+
}
27+
28+
function extractHubSpotIdentifier(body: any): string | null {
29+
if (Array.isArray(body) && body.length > 0 && body[0]?.eventId) {
30+
return String(body[0].eventId)
31+
}
32+
return null
33+
}
34+
35+
function extractLinearIdentifier(body: any): string | null {
36+
if (body.action && body.data?.id) {
37+
return `${body.action}:${body.data.id}`
38+
}
39+
return null
40+
}
41+
42+
function extractJiraIdentifier(body: any): string | null {
43+
if (body.webhookEvent && (body.issue?.id || body.project?.id)) {
44+
return `${body.webhookEvent}:${body.issue?.id || body.project?.id}`
45+
}
46+
return null
47+
}
48+
49+
function extractMicrosoftTeamsIdentifier(body: any): string | null {
50+
if (body.value && Array.isArray(body.value) && body.value.length > 0) {
51+
const notification = body.value[0]
52+
if (notification.subscriptionId && notification.resourceData?.id) {
53+
return `${notification.subscriptionId}:${notification.resourceData.id}`
54+
}
55+
}
56+
return null
57+
}
58+
59+
function extractAirtableIdentifier(body: any): string | null {
60+
if (body.cursor && typeof body.cursor === 'string') {
61+
return body.cursor
62+
}
63+
return null
64+
}
65+
66+
const PROVIDER_EXTRACTORS: Record<string, (body: any) => string | null> = {
67+
slack: extractSlackIdentifier,
68+
twilio: extractTwilioIdentifier,
69+
twilio_voice: extractTwilioIdentifier,
70+
stripe: extractStripeIdentifier,
71+
hubspot: extractHubSpotIdentifier,
72+
linear: extractLinearIdentifier,
73+
jira: extractJiraIdentifier,
74+
'microsoft-teams': extractMicrosoftTeamsIdentifier,
75+
airtable: extractAirtableIdentifier,
76+
}
77+
78+
export function extractProviderIdentifierFromBody(provider: string, body: any): string | null {
79+
if (!body || typeof body !== 'object') {
80+
return null
81+
}
82+
83+
const extractor = PROVIDER_EXTRACTORS[provider]
84+
return extractor ? extractor(body) : null
85+
}

0 commit comments

Comments
 (0)