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 217f808641.

* fix

* fix

* fix provider name

* fix oauth for msteams
This commit is contained in:
Vikhyath Mondreti
2025-11-17 17:48:22 -08:00
committed by GitHub
parent 00d9b45a22
commit 98908dbfb9
12 changed files with 165 additions and 50 deletions

View File

@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { webhook as webhookTable, workflow as workflowTable } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { and, eq, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { createLogger } from '@/lib/logs/console/logger'
@@ -35,7 +35,15 @@ export async function GET(request: NextRequest) {
})
.from(webhookTable)
.innerJoin(workflowTable, eq(webhookTable.workflowId, workflowTable.id))
.where(and(eq(webhookTable.isActive, true), eq(webhookTable.provider, 'microsoftteams')))
.where(
and(
eq(webhookTable.isActive, true),
or(
eq(webhookTable.provider, 'microsoft-teams'),
eq(webhookTable.provider, 'microsoftteams')
)
)
)
logger.info(
`Found ${webhooksWithWorkflows.length} active Teams webhooks, checking for expiring subscriptions`

View File

@@ -137,7 +137,7 @@ export async function POST(request: NextRequest) {
const isCredentialBased = credentialBasedProviders.includes(provider)
// Treat Microsoft Teams chat subscription as credential-based for path generation purposes
const isMicrosoftTeamsChatSubscription =
provider === 'microsoftteams' &&
provider === 'microsoft-teams' &&
typeof providerConfig === 'object' &&
providerConfig?.triggerId === 'microsoftteams_chat_subscription'
@@ -297,7 +297,7 @@ export async function POST(request: NextRequest) {
}
}
if (provider === 'microsoftteams') {
if (provider === 'microsoft-teams') {
const { createTeamsSubscription } = await import('@/lib/webhooks/webhook-helpers')
logger.info(`[${requestId}] Creating Teams subscription before saving to database`)
try {

View File

@@ -441,7 +441,7 @@ export async function GET(request: NextRequest) {
})
}
case 'microsoftteams': {
case 'microsoft-teams': {
const hmacSecret = providerConfig.hmacSecret
if (!hmacSecret) {

View File

@@ -1,7 +1,5 @@
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { generateRequestId } from '@/lib/utils'
import {
checkRateLimits,
@@ -139,34 +137,10 @@ export async function POST(
if (foundWebhook.blockId) {
const blockExists = await blockExistsInDeployment(foundWorkflow.id, foundWebhook.blockId)
if (!blockExists) {
logger.warn(
logger.info(
`[${requestId}] Trigger block ${foundWebhook.blockId} not found in deployment for workflow ${foundWorkflow.id}`
)
const executionId = uuidv4()
const loggingSession = new LoggingSession(foundWorkflow.id, executionId, 'webhook', requestId)
const actorUserId = foundWorkflow.workspaceId
? (await import('@/lib/workspaces/utils')).getWorkspaceBilledAccountUserId(
foundWorkflow.workspaceId
) || foundWorkflow.userId
: foundWorkflow.userId
await loggingSession.safeStart({
userId: actorUserId,
workspaceId: foundWorkflow.workspaceId || '',
variables: {},
})
await loggingSession.safeCompleteWithError({
error: {
message: `Trigger block not deployed. The webhook trigger (block ${foundWebhook.blockId}) is not present in the deployed workflow. Please redeploy the workflow.`,
stackTrace: undefined,
},
traceSpans: [],
})
return new NextResponse('Trigger block not deployed', { status: 404 })
return new NextResponse('Trigger block not found in deployment', { status: 404 })
}
}

View File

@@ -112,7 +112,9 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
const idempotencyKey = IdempotencyService.createWebhookIdempotencyKey(
payload.webhookId,
payload.headers
payload.headers,
payload.body,
payload.provider
)
const runOperation = async () => {

View File

@@ -55,7 +55,7 @@ export class TriggerBlockHandler implements BlockHandler {
}
}
if (provider === 'microsoftteams') {
if (provider === 'microsoft-teams') {
const providerData = (starterOutput as any)[provider] || webhookData[provider] || {}
const payloadSource = providerData?.message?.raw || webhookData.payload || {}
return {

View File

@@ -4,6 +4,7 @@ import { idempotencyKey } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { createLogger } from '@/lib/logs/console/logger'
import { getRedisClient } from '@/lib/redis'
import { extractProviderIdentifierFromBody } from '@/lib/webhooks/provider-utils'
const logger = createLogger('IdempotencyService')
@@ -451,13 +452,25 @@ export class IdempotencyService {
/**
* Create an idempotency key from a webhook payload following RFC best practices
* Standard webhook headers (webhook-id, x-webhook-id, etc.)
* Checks both headers and body for unique identifiers to prevent duplicate executions
*
* @param webhookId - The webhook database ID
* @param headers - HTTP headers from the webhook request
* @param body - Parsed webhook body (optional, used for provider-specific identifiers)
* @param provider - Provider name for body extraction (optional)
* @returns A unique idempotency key for this webhook event
*/
static createWebhookIdempotencyKey(webhookId: string, headers?: Record<string, string>): string {
static createWebhookIdempotencyKey(
webhookId: string,
headers?: Record<string, string>,
body?: any,
provider?: string
): string {
const normalizedHeaders = headers
? Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]))
: undefined
// Check standard webhook headers first
const webhookIdHeader =
normalizedHeaders?.['webhook-id'] ||
normalizedHeaders?.['x-webhook-id'] ||
@@ -470,7 +483,22 @@ export class IdempotencyService {
return `${webhookId}:${webhookIdHeader}`
}
// Check body for provider-specific unique identifiers
if (body && provider) {
const bodyIdentifier = extractProviderIdentifierFromBody(provider, body)
if (bodyIdentifier) {
return `${webhookId}:${bodyIdentifier}`
}
}
// No unique identifier found - generate random UUID
// This means duplicate detection will not work for this webhook
const uniqueId = randomUUID()
logger.warn('No unique identifier found, duplicate executions may occur', {
webhookId,
provider,
})
return `${webhookId}:${uniqueId}`
}
}

View File

@@ -906,6 +906,24 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig {
featureType: 'sharepoint',
}
}
if (provider === 'microsoft-teams' || provider === 'microsoftteams') {
return {
baseProvider: 'microsoft',
featureType: 'microsoft-teams',
}
}
if (provider === 'microsoft-excel') {
return {
baseProvider: 'microsoft',
featureType: 'microsoft-excel',
}
}
if (provider === 'microsoft-planner') {
return {
baseProvider: 'microsoft',
featureType: 'microsoft-planner',
}
}
// Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' })
const [base, feature] = provider.split('-')

View File

@@ -250,7 +250,7 @@ export async function verifyProviderAuth(
const rawProviderConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const providerConfig = resolveProviderConfigEnvVars(rawProviderConfig, decryptedEnvVars)
if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
if (providerConfig.hmacSecret) {
const authHeader = request.headers.get('authorization')
@@ -556,7 +556,7 @@ export async function checkRateLimits(
traceSpans: [],
})
if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
return NextResponse.json(
{
type: 'message',
@@ -634,7 +634,7 @@ export async function checkUsageLimits(
traceSpans: [],
})
if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
return NextResponse.json(
{
type: 'message',
@@ -783,7 +783,7 @@ export async function queueWebhookExecution(
// For Microsoft Teams Graph notifications, extract unique identifiers for idempotency
if (
foundWebhook.provider === 'microsoftteams' &&
foundWebhook.provider === 'microsoft-teams' &&
body?.value &&
Array.isArray(body.value) &&
body.value.length > 0
@@ -835,7 +835,7 @@ export async function queueWebhookExecution(
)
}
if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined
@@ -886,7 +886,7 @@ export async function queueWebhookExecution(
} catch (error: any) {
logger.error(`[${options.requestId}] Failed to queue webhook execution:`, error)
if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
return NextResponse.json(
{
type: 'message',

View File

@@ -0,0 +1,85 @@
/**
* Provider-specific unique identifier extractors for webhook idempotency
*/
function extractSlackIdentifier(body: any): string | null {
if (body.event_id) {
return body.event_id
}
if (body.event?.ts && body.team_id) {
return `${body.team_id}:${body.event.ts}`
}
return null
}
function extractTwilioIdentifier(body: any): string | null {
return body.MessageSid || body.CallSid || null
}
function extractStripeIdentifier(body: any): string | null {
if (body.id && body.object === 'event') {
return body.id
}
return null
}
function extractHubSpotIdentifier(body: any): string | null {
if (Array.isArray(body) && body.length > 0 && body[0]?.eventId) {
return String(body[0].eventId)
}
return null
}
function extractLinearIdentifier(body: any): string | null {
if (body.action && body.data?.id) {
return `${body.action}:${body.data.id}`
}
return null
}
function extractJiraIdentifier(body: any): string | null {
if (body.webhookEvent && (body.issue?.id || body.project?.id)) {
return `${body.webhookEvent}:${body.issue?.id || body.project?.id}`
}
return null
}
function extractMicrosoftTeamsIdentifier(body: any): string | null {
if (body.value && Array.isArray(body.value) && body.value.length > 0) {
const notification = body.value[0]
if (notification.subscriptionId && notification.resourceData?.id) {
return `${notification.subscriptionId}:${notification.resourceData.id}`
}
}
return null
}
function extractAirtableIdentifier(body: any): string | null {
if (body.cursor && typeof body.cursor === 'string') {
return body.cursor
}
return null
}
const PROVIDER_EXTRACTORS: Record<string, (body: any) => string | null> = {
slack: extractSlackIdentifier,
twilio: extractTwilioIdentifier,
twilio_voice: extractTwilioIdentifier,
stripe: extractStripeIdentifier,
hubspot: extractHubSpotIdentifier,
linear: extractLinearIdentifier,
jira: extractJiraIdentifier,
'microsoft-teams': extractMicrosoftTeamsIdentifier,
airtable: extractAirtableIdentifier,
}
export function extractProviderIdentifierFromBody(provider: string, body: any): string | null {
if (!body || typeof body !== 'object') {
return null
}
const extractor = PROVIDER_EXTRACTORS[provider]
return extractor ? extractor(body) : null
}

View File

@@ -133,7 +133,7 @@ async function formatTeamsGraphNotification(
input: 'Teams notification received',
webhook: {
data: {
provider: 'microsoftteams',
provider: 'microsoft-teams',
path: foundWebhook?.path || '',
providerConfig: foundWebhook?.providerConfig || {},
payload: body,
@@ -397,7 +397,7 @@ async function formatTeamsGraphNotification(
},
webhook: {
data: {
provider: 'microsoftteams',
provider: 'microsoft-teams',
path: foundWebhook?.path || '',
providerConfig: foundWebhook?.providerConfig || {},
payload: body,
@@ -446,7 +446,7 @@ async function formatTeamsGraphNotification(
},
webhook: {
data: {
provider: 'microsoftteams',
provider: 'microsoft-teams',
path: foundWebhook?.path || '',
providerConfig: foundWebhook?.providerConfig || {},
payload: body,
@@ -818,7 +818,7 @@ export async function formatWebhookInput(
}
}
if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
// Check if this is a Microsoft Graph change notification
if (body?.value && Array.isArray(body.value) && body.value.length > 0) {
return await formatTeamsGraphNotification(body, foundWebhook, foundWorkflow, request)
@@ -875,7 +875,7 @@ export async function formatWebhookInput(
webhook: {
data: {
provider: 'microsoftteams',
provider: 'microsoft-teams',
path: foundWebhook.path,
providerConfig: foundWebhook.providerConfig,
payload: body,
@@ -1653,7 +1653,7 @@ export function verifyProviderWebhook(
break
}
case 'microsoftteams':
case 'microsoft-teams':
break
case 'generic':
if (providerConfig.requireAuth) {

View File

@@ -623,7 +623,7 @@ export async function cleanupExternalWebhook(
): Promise<void> {
if (webhook.provider === 'airtable') {
await deleteAirtableWebhook(webhook, workflow, requestId)
} else if (webhook.provider === 'microsoftteams') {
} else if (webhook.provider === 'microsoft-teams') {
await deleteTeamsSubscription(webhook, workflow, requestId)
} else if (webhook.provider === 'telegram') {
await deleteTelegramWebhook(webhook, requestId)