diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx index cb31c90f47..117d46cb71 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx @@ -81,7 +81,7 @@ const Page = async (props) => { return ( - + {t("environments.integrations.slack.connected_with_team", { - team: slackIntegration.config.key.team.name, + team: slackIntegration.config.key.team?.name, })} diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx index cbbaab6649..1e6ac7f4db 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx @@ -46,7 +46,8 @@ export const SlackWrapper = ({ if ( getSlackChannelsResponse?.serverError && - getSlackChannelsResponse.serverError.includes("missing_scope") + (getSlackChannelsResponse.serverError.includes("missing_scope") || + getSlackChannelsResponse.serverError.includes("invalid_auth")) ) { setShowReconnectButton(true); } diff --git a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts index e99090e96c..f95ce3fb91 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts @@ -4,8 +4,10 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { writeData as writeNotionData } from "@formbricks/lib/notion/service"; import { processResponseData } from "@formbricks/lib/responses"; import { writeDataToSlack } from "@formbricks/lib/slack/service"; +import { parseRecallInfo } from "@formbricks/lib/utils/recall"; +import { TAttributes } from "@formbricks/types/attributes"; import { Result } from "@formbricks/types/error-handlers"; -import { TIntegration } from "@formbricks/types/integration"; +import { TIntegration, TIntegrationType } from "@formbricks/types/integration"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion"; @@ -29,18 +31,20 @@ const convertMetaObjectToString = (metadata: TResponseMeta): string => { }; const processDataForIntegration = async ( + integrationType: TIntegrationType, data: TPipelineInput, survey: TSurvey, includeVariables: boolean, includeMetadata: boolean, includeHiddenFields: boolean, - questionIds: string[] + questionIds: string[], + attributes?: TAttributes ): Promise => { const ids = includeHiddenFields && survey.hiddenFields.fieldIds ? [...questionIds, ...survey.hiddenFields.fieldIds] : questionIds; - const values = await extractResponses(data, ids, survey); + const values = await extractResponses(integrationType, data, ids, survey, attributes); if (includeMetadata) { values[0].push(convertMetaObjectToString(data.response.meta)); values[1].push("Metadata"); @@ -61,7 +65,8 @@ const processDataForIntegration = async ( export const handleIntegrations = async ( integrations: TIntegration[], data: TPipelineInput, - survey: TSurvey + survey: TSurvey, + attributes: TAttributes ) => { for (const integration of integrations) { switch (integration.type) { @@ -76,7 +81,12 @@ export const handleIntegrations = async ( } break; case "slack": - const slackResult = await handleSlackIntegration(integration as TIntegrationSlack, data, survey); + const slackResult = await handleSlackIntegration( + integration as TIntegrationSlack, + data, + survey, + attributes + ); if (!slackResult.ok) { console.error("Error in slack integration: ", slackResult.error); } @@ -111,6 +121,7 @@ const handleAirtableIntegration = async ( for (const element of integration.config.data) { if (element.surveyId === data.surveyId) { const values = await processDataForIntegration( + "airtable", data, survey, !!element.includeVariables, @@ -145,6 +156,7 @@ const handleGoogleSheetsIntegration = async ( for (const element of integration.config.data) { if (element.surveyId === data.surveyId) { const values = await processDataForIntegration( + "googleSheets", data, survey, !!element.includeVariables, @@ -177,19 +189,22 @@ const handleGoogleSheetsIntegration = async ( const handleSlackIntegration = async ( integration: TIntegrationSlack, data: TPipelineInput, - survey: TSurvey + survey: TSurvey, + attributes: TAttributes ): Promise> => { try { if (integration.config.data.length > 0) { for (const element of integration.config.data) { if (element.surveyId === data.surveyId) { const values = await processDataForIntegration( + "slack", data, survey, !!element.includeVariables, !!element.includeMetadata, !!element.includeHiddenFields, - element.questionIds + element.questionIds, + attributes ); await writeDataToSlack(integration.config.key, element.channelId, values, survey?.name); } @@ -209,9 +224,11 @@ const handleSlackIntegration = async ( }; const extractResponses = async ( + integrationType: TIntegrationType, pipelineData: TPipelineInput, questionIds: string[], - survey: TSurvey + survey: TSurvey, + attributes?: TAttributes ): Promise => { const responses: string[] = []; const questions: string[] = []; @@ -246,7 +263,22 @@ const extractResponses = async ( } else { responses.push(""); } - questions.push(getLocalizedValue(question?.headline, "default") || ""); + // Create emptyResponseObject with same keys but empty string values + const emptyResponseObject = Object.keys(pipelineData.response.data).reduce( + (acc, key) => { + acc[key] = ""; + return acc; + }, + {} as Record + ); + questions.push( + parseRecallInfo( + getLocalizedValue(question?.headline, "default"), + integrationType === "slack" ? attributes : {}, + integrationType === "slack" ? pipelineData.response.data : emptyResponseObject, + integrationType === "slack" ? pipelineData.response.variables : {} + ) || "" + ); } return [responses, questions]; diff --git a/apps/web/app/api/(internal)/pipeline/route.ts b/apps/web/app/api/(internal)/pipeline/route.ts index 93a6128354..af74afd973 100644 --- a/apps/web/app/api/(internal)/pipeline/route.ts +++ b/apps/web/app/api/(internal)/pipeline/route.ts @@ -42,6 +42,7 @@ export const POST = async (request: Request) => { } const { environmentId, surveyId, event, response } = inputValidation.data; + const attributes = response.person?.id ? await getAttributes(response.person?.id) : {}; // Fetch webhooks const getWebhooksForPipeline = cache( @@ -99,7 +100,7 @@ export const POST = async (request: Request) => { } if (integrations.length > 0) { - await handleIntegrations(integrations, inputValidation.data, survey); + await handleIntegrations(integrations, inputValidation.data, survey, attributes); } // Fetch users with notifications in a single query @@ -197,8 +198,6 @@ export const POST = async (request: Request) => { const isAIEnabled = await getIsAIEnabled(organization); if (isAIEnabled) { - const attributes = response.person?.id ? await getAttributes(response.person?.id) : {}; - for (const question of survey.questions) { if (question.type === "openText" && question.insightsEnabled) { const isQuestionAnswered =