diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 6830cce0a76..27451a99429 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -27,4 +27,6 @@ runs: - name: Build shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} + env: + BP_VERBOSE: 'true' run: pnpm build ${{ inputs.extra_filters }} diff --git a/integrations/instagram/integration.definition.ts b/integrations/instagram/integration.definition.ts index 6c9e5e7811d..507381c6d84 100644 --- a/integrations/instagram/integration.definition.ts +++ b/integrations/instagram/integration.definition.ts @@ -1,5 +1,5 @@ +import { posthogHelper } from '@botpress/common' import { z, IntegrationDefinition, messages } from '@botpress/sdk' -import { sentry as sentryHelpers } from '@botpress/sdk-addons' import proactiveConversation from 'bp_modules/proactive-conversation' import proactiveUser from 'bp_modules/proactive-user' import { dmChannelMessages } from './definitions/channel' @@ -16,7 +16,7 @@ const commonConfigSchema = z.object({ export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '4.1.1', + version: '4.1.2', title: 'Instagram', description: 'Automate interactions, manage comments, and send/receive messages all in real-time.', icon: 'icon.svg', @@ -165,7 +165,7 @@ export default new IntegrationDefinition({ actions: {}, events: {}, secrets: { - ...sentryHelpers.COMMON_SECRET_NAMES, + ...posthogHelper.COMMON_SECRET_NAMES, CLIENT_ID: { description: 'The client ID of the OAuth Meta app.', }, @@ -187,9 +187,6 @@ export default new IntegrationDefinition({ SANDBOX_INSTAGRAM_ID: { description: 'Instagram ID for the Sandbox Instagram profile', }, - POSTHOG_KEY: { - description: 'The PostHog key for the Instagram integration', - }, }, user: { tags: { diff --git a/integrations/instagram/package.json b/integrations/instagram/package.json index 4b68ff932c5..445e567f896 100644 --- a/integrations/instagram/package.json +++ b/integrations/instagram/package.json @@ -14,8 +14,7 @@ "@botpress/client": "workspace:*", "@botpress/sdk": "workspace:*", "@botpress/sdk-addons": "workspace:*", - "axios": "^1.6.2", - "posthog-node": "^5.10.4" + "axios": "^1.6.2" }, "devDependencies": { "@botpress/cli": "workspace:*", diff --git a/integrations/instagram/src/index.ts b/integrations/instagram/src/index.ts index 268ca2275cd..ba292af2e35 100644 --- a/integrations/instagram/src/index.ts +++ b/integrations/instagram/src/index.ts @@ -1,20 +1,25 @@ -import { sentry as sentryHelpers } from '@botpress/sdk-addons' +import { posthogHelper } from '@botpress/common' +import { INTEGRATION_NAME } from 'integration.definition' import actions from './actions' import channels from './channels' import { register, unregister } from './setup' import { handler } from './webhook' import * as bp from '.botpress' -const integration = new bp.Integration({ - register, - unregister, - actions, - channels, - handler, +@posthogHelper.wrapIntegration({ + integrationName: INTEGRATION_NAME, + key: bp.secrets.POSTHOG_KEY, }) +class InstagramIntegration extends bp.Integration { + public constructor() { + super({ + register, + unregister, + actions, + channels, + handler, + }) + } +} -export default sentryHelpers.wrapIntegration(integration, { - dsn: bp.secrets.SENTRY_DSN, - environment: bp.secrets.SENTRY_ENVIRONMENT, - release: bp.secrets.SENTRY_RELEASE, -}) +export default new InstagramIntegration() diff --git a/integrations/instagram/src/misc/posthog-client.ts b/integrations/instagram/src/misc/posthog-client.ts deleted file mode 100644 index a9c8fb09641..00000000000 --- a/integrations/instagram/src/misc/posthog-client.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventMessage, PostHog } from 'posthog-node' -import * as bp from '.botpress' - -type BotpressEventMessage = Omit & { - event: BotpressEvent -} - -type PostHogErrorOptions = { - from: string - integrationName: string - errorType?: BotpressEvent -} - -export const botpressEvents = { - UNHANDLED_ERROR: 'unhandled_error', - UNHANDLED_MESSAGE: 'unhandled_message', - INVALID_MESSAGE_FORMAT: 'invalid_message_format', -} as const -type BotpressEvent = (typeof botpressEvents)[keyof typeof botpressEvents] - -const sendPosthogEvent = async (props: BotpressEventMessage): Promise => { - const client = new PostHog(bp.secrets.POSTHOG_KEY, { - host: 'https://us.i.posthog.com', - }) - try { - await client.captureImmediate(props) - await client.shutdown() - console.info('PostHog event sent') - } catch (thrown: any) { - const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - console.error(`The server for posthog could not be reached - Error: ${errMsg}`) - } -} - -export const sendPosthogError = async ( - distinctId: string, - errorMessage: string, - { from, integrationName, errorType = botpressEvents.UNHANDLED_ERROR }: PostHogErrorOptions -): Promise => { - await sendPosthogEvent({ - distinctId, - event: errorType, - properties: { - from, - integrationName, - message: errorMessage, - }, - }) -} diff --git a/integrations/instagram/src/webhook/handler.ts b/integrations/instagram/src/webhook/handler.ts index 8a9bbf65e89..02614747094 100644 --- a/integrations/instagram/src/webhook/handler.ts +++ b/integrations/instagram/src/webhook/handler.ts @@ -1,9 +1,7 @@ import { isSandboxCommand } from '@botpress/common' import { Request } from '@botpress/sdk' import * as crypto from 'crypto' -import { INTEGRATION_NAME } from 'integration.definition' import { getClientSecret } from 'src/misc/client' -import { sendPosthogError } from 'src/misc/posthog-client' import { instagramPayloadSchema, InstagramLegacyCommentEntry, @@ -119,19 +117,11 @@ const _handlerWrapper: typeof _handler = async (props: bp.HandlerProps) => { if (response && response.status !== 200) { const errorMessage = `Instagram handler failed with status ${response.status}: ${response.body}` props.logger.error(errorMessage) - await sendPosthogError(props.ctx.integrationId, errorMessage, { - from: `${INTEGRATION_NAME}:handler`, - integrationName: INTEGRATION_NAME, - }) } return response } catch (thrown: unknown) { const errorMsg = thrown instanceof Error ? thrown.message : String(thrown) const errorMessage = `Instagram handler failed with error: ${errorMsg}` - await sendPosthogError(props.ctx.integrationId, errorMessage, { - from: `${INTEGRATION_NAME}:handler`, - integrationName: INTEGRATION_NAME, - }) props.logger.error(errorMessage) return { status: 500, body: errorMessage } } diff --git a/integrations/messenger/integration.definition.ts b/integrations/messenger/integration.definition.ts index 607c9f5f23d..3a2c6f0f31c 100644 --- a/integrations/messenger/integration.definition.ts +++ b/integrations/messenger/integration.definition.ts @@ -1,6 +1,6 @@ +import { posthogHelper } from '@botpress/common' import { z, IntegrationDefinition } from '@botpress/sdk' import * as sdk from '@botpress/sdk' -import { sentry as sentryHelpers } from '@botpress/sdk-addons' import proactiveConversation from 'bp_modules/proactive-conversation' import proactiveUser from 'bp_modules/proactive-user' import typingIndicator from 'bp_modules/typing-indicator' @@ -36,7 +36,7 @@ const replyToCommentsSchema = z.object({ export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '5.0.0', + version: '5.0.1', title: 'Messenger and Facebook', description: 'Give your bot access to one of the world’s largest messaging platforms and manage your Facebook page content in one place.', @@ -164,7 +164,7 @@ export default new IntegrationDefinition({ }, }, secrets: { - ...sentryHelpers.COMMON_SECRET_NAMES, + ...posthogHelper.COMMON_SECRET_NAMES, CLIENT_ID: { description: 'The client ID of your Meta app', }, @@ -201,9 +201,6 @@ export default new IntegrationDefinition({ SANDBOX_SHOULD_GET_USER_PROFILE: { description: "Whether to get the user profile infos from Messenger when creating a new user ('true' or 'false')", }, - POSTHOG_KEY: { - description: 'The PostHog API key', - }, }, user: { tags: { id: { title: 'User ID', description: 'The Messenger ID of the user' } }, diff --git a/integrations/messenger/package.json b/integrations/messenger/package.json index dbc5095efc0..a324cdd6aa8 100644 --- a/integrations/messenger/package.json +++ b/integrations/messenger/package.json @@ -14,8 +14,7 @@ "@botpress/sdk": "workspace:*", "@botpress/sdk-addons": "workspace:*", "axios": "^1.6.2", - "messaging-api-messenger": "^1.1.0", - "posthog-node": "^5.10.4" + "messaging-api-messenger": "^1.1.0" }, "devDependencies": { "@botpress/cli": "workspace:*", diff --git a/integrations/messenger/src/index.ts b/integrations/messenger/src/index.ts index 268ca2275cd..667f872bfc4 100644 --- a/integrations/messenger/src/index.ts +++ b/integrations/messenger/src/index.ts @@ -1,20 +1,25 @@ -import { sentry as sentryHelpers } from '@botpress/sdk-addons' +import { posthogHelper } from '@botpress/common' +import { INTEGRATION_NAME } from 'integration.definition' import actions from './actions' import channels from './channels' import { register, unregister } from './setup' import { handler } from './webhook' import * as bp from '.botpress' -const integration = new bp.Integration({ - register, - unregister, - actions, - channels, - handler, +@posthogHelper.wrapIntegration({ + integrationName: INTEGRATION_NAME, + key: bp.secrets.POSTHOG_KEY, }) +class MessengerIntegration extends bp.Integration { + public constructor() { + super({ + register, + unregister, + actions, + channels, + handler, + }) + } +} -export default sentryHelpers.wrapIntegration(integration, { - dsn: bp.secrets.SENTRY_DSN, - environment: bp.secrets.SENTRY_ENVIRONMENT, - release: bp.secrets.SENTRY_RELEASE, -}) +export default new MessengerIntegration() diff --git a/integrations/messenger/src/misc/posthog-client.ts b/integrations/messenger/src/misc/posthog-client.ts deleted file mode 100644 index bc3bba5dfc1..00000000000 --- a/integrations/messenger/src/misc/posthog-client.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventMessage, PostHog } from 'posthog-node' -import * as bp from '.botpress' - -type BotpressEventMessage = Omit & { - event: BotpressEvent -} - -type PostHogErrorOptions = { - from: string - integrationName: string - errorType?: BotpressEvent -} - -export const botpressEvents = { - UNHANDLED_ERROR: 'unhandled_error', - UNHANDLED_MESSAGE: 'unhandled_message', - INVALID_MESSAGE_FORMAT: 'invalid_message_format', -} as const -type BotpressEvent = (typeof botpressEvents)[keyof typeof botpressEvents] - -const sendPosthogEvent = async (props: BotpressEventMessage): Promise => { - const client = new PostHog(bp.secrets.POSTHOG_KEY, { - host: 'https://us.i.posthog.com', - }) - try { - await client.captureImmediate(props) - await client.shutdown() - console.info('PostHog event sent') - } catch (thrown: unknown) { - const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - console.error(`The server for posthog could not be reached - Error: ${errMsg}`) - } -} - -export const sendPosthogError = async ( - distinctId: string, - errorMessage: string, - { from, integrationName, errorType }: PostHogErrorOptions -): Promise => { - await sendPosthogEvent({ - distinctId, - event: errorType ?? botpressEvents.UNHANDLED_ERROR, - properties: { - from, - integrationName, - message: errorMessage, - }, - }) -} diff --git a/integrations/messenger/src/webhook/handler.ts b/integrations/messenger/src/webhook/handler.ts index 2a0c922a46b..0da69ec4c75 100644 --- a/integrations/messenger/src/webhook/handler.ts +++ b/integrations/messenger/src/webhook/handler.ts @@ -1,6 +1,4 @@ import { isSandboxCommand, meta } from '@botpress/common' -import { INTEGRATION_NAME } from 'integration.definition' -import { sendPosthogError } from 'src/misc/posthog-client' import { getClientSecret, getVerifyToken } from '../misc/auth' import { eventPayloadSchema } from '../misc/types' import { safeJsonParse } from '../misc/utils' @@ -64,20 +62,12 @@ const _handlerWrapper: typeof _handler = async (props: bp.HandlerProps) => { if (response?.status && response.status >= 400) { const errorMessage = `Messenger handler failed with status ${response.status}: ${response.body}` props.logger.error(errorMessage) - await sendPosthogError(props.ctx.integrationId, errorMessage, { - from: `${INTEGRATION_NAME}:handler`, - integrationName: INTEGRATION_NAME, - }) } return response } catch (thrown: unknown) { const errorMsg = thrown instanceof Error ? thrown.message : String(thrown) const errorMessage = `Messenger handler failed with error: ${errorMsg}` props.logger.error(errorMessage) - await sendPosthogError(props.ctx.integrationId, errorMessage, { - from: `${INTEGRATION_NAME}:handler`, - integrationName: INTEGRATION_NAME, - }) return { status: 500, body: errorMessage } } } diff --git a/integrations/twilio/integration.definition.ts b/integrations/twilio/integration.definition.ts index 153f5b9d319..827006245f0 100644 --- a/integrations/twilio/integration.definition.ts +++ b/integrations/twilio/integration.definition.ts @@ -6,7 +6,7 @@ import proactiveUser from 'bp_modules/proactive-user' export const INTEGRATION_NAME = 'twilio' export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '1.0.1', + version: '1.0.2', title: 'Twilio', description: 'Send and receive messages, voice calls, emails, SMS, and more.', icon: 'icon.svg', diff --git a/integrations/twilio/package.json b/integrations/twilio/package.json index 27c7a0a99ac..f384e19dc94 100644 --- a/integrations/twilio/package.json +++ b/integrations/twilio/package.json @@ -14,7 +14,6 @@ "@botpress/sdk": "workspace:*", "@botpress/sdk-addons": "workspace:*", "axios": "^1.6.0", - "posthog-node": "^5.10.4", "query-string": "^6.14.1", "twilio": "^3.84.0" }, diff --git a/integrations/twilio/src/index.ts b/integrations/twilio/src/index.ts index bce20acc7e7..48805e982c8 100644 --- a/integrations/twilio/src/index.ts +++ b/integrations/twilio/src/index.ts @@ -1,12 +1,13 @@ -import { RuntimeError } from '@botpress/client' +import { RuntimeError, isApiError } from '@botpress/client' +import { posthogHelper } from '@botpress/common' import * as sdk from '@botpress/sdk' import { sentry as sentryHelpers } from '@botpress/sdk-addons' import axios from 'axios' import * as crypto from 'crypto' +import { INTEGRATION_NAME } from 'integration.definition' import queryString from 'query-string' import { Twilio } from 'twilio' import { transformMarkdownForTwilio } from './markdown-to-twilio' -import { botpressEvents, sendPosthogEvent } from './posthogClient' import * as types from './types' import * as bp from '.botpress' @@ -404,10 +405,15 @@ async function sendMessage({ ctx, conversation, ack, mediaUrl, text, logger }: S } catch (thrown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) logger.forBot().debug('Failed to transform markdown - Error:', errMsg) - await sendPosthogEvent({ - distinctId: errMsg, - event: botpressEvents.UNHANDLED_MARKDOWN, - }) + const distinctId = isApiError(thrown) ? thrown.id : undefined + await posthogHelper.sendPosthogEvent( + { + distinctId: distinctId ?? 'no id', + event: 'unhandled_markdown', + properties: { errMsg }, + }, + { integrationName: INTEGRATION_NAME, key: bp.secrets.POSTHOG_KEY } + ) } } const { sid } = await twilioClient.messages.create({ to, from, mediaUrl, body }) diff --git a/integrations/twilio/src/posthogClient.ts b/integrations/twilio/src/posthogClient.ts deleted file mode 100644 index 1ed9e5168a3..00000000000 --- a/integrations/twilio/src/posthogClient.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { INTEGRATION_NAME } from 'integration.definition' -import { EventMessage, PostHog } from 'posthog-node' -import * as bp from '.botpress' - -type BotpressEventMessage = Omit & { - event: BotpressEvent -} - -type PostHogErrorOptions = { - from: string - integrationName: string -} - -export const botpressEvents = { - UNHANDLED_MARKDOWN: 'unhandled_markdown', - UNHANDLED_ERROR: 'unhandled_error', -} as const -type BotpressEvent = (typeof botpressEvents)[keyof typeof botpressEvents] - -export const sendPosthogEvent = async (props: BotpressEventMessage): Promise => { - const client = new PostHog(bp.secrets.POSTHOG_KEY, { - host: 'https://us.i.posthog.com', - }) - try { - const signedProps: BotpressEventMessage = { - ...props, - properties: { - ...props.properties, - integrationName: INTEGRATION_NAME, - }, - } - await client.captureImmediate(signedProps) - await client.shutdown() - console.info('PostHog event sent') - } catch (thrown: any) { - const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - console.error(`The server for posthog could not be reached - Error: ${errMsg}`) - } -} - -export const sendPosthogError = async (thrown: unknown, { from }: Partial): Promise => { - const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - await sendPosthogEvent({ - distinctId: errMsg, - event: botpressEvents.UNHANDLED_ERROR, - properties: { - from, - integrationName: INTEGRATION_NAME, - }, - }) -} diff --git a/integrations/whatsapp/integration.definition.ts b/integrations/whatsapp/integration.definition.ts index 07e57d2c745..8716cdc9bcf 100644 --- a/integrations/whatsapp/integration.definition.ts +++ b/integrations/whatsapp/integration.definition.ts @@ -1,5 +1,5 @@ +import { posthogHelper } from '@botpress/common' import { z, IntegrationDefinition, messages } from '@botpress/sdk' -import { sentry as sentryHelpers } from '@botpress/sdk-addons' import proactiveConversation from 'bp_modules/proactive-conversation' import typingIndicator from 'bp_modules/typing-indicator' import { @@ -95,7 +95,7 @@ const defaultBotPhoneNumberId = { export const INTEGRATION_NAME = 'whatsapp' export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '4.5.15', + version: '4.5.16', title: 'WhatsApp', description: 'Send and receive messages through WhatsApp.', icon: 'icon.svg', @@ -374,7 +374,7 @@ export default new IntegrationDefinition({ }, }, secrets: { - ...sentryHelpers.COMMON_SECRET_NAMES, + ...posthogHelper.COMMON_SECRET_NAMES, CLIENT_ID: { description: 'The client ID of the OAuth Meta app', }, @@ -405,9 +405,6 @@ export default new IntegrationDefinition({ SANDBOX_PHONE_NUMBER_ID: { description: 'Phone number ID of the Sandbox WhatsApp Business profile', }, - POSTHOG_KEY: { - description: 'Posthog key for error dashboards', - }, }, entities: { proactiveConversation: { diff --git a/integrations/whatsapp/package.json b/integrations/whatsapp/package.json index 726c3a3297c..dcb161a6225 100644 --- a/integrations/whatsapp/package.json +++ b/integrations/whatsapp/package.json @@ -16,7 +16,6 @@ "awesome-phonenumber": "^7.5.0", "axios": "^1.6.2", "marked": "^15.0.1", - "posthog-node": "^5.10.4", "preact": "^10.26.6", "preact-render-to-string": "^6.5.13", "whatsapp-api-js": "^5.3.0" diff --git a/integrations/whatsapp/src/actions/start-conversation.ts b/integrations/whatsapp/src/actions/start-conversation.ts index 68c97a8d8ec..86ae585e047 100644 --- a/integrations/whatsapp/src/actions/start-conversation.ts +++ b/integrations/whatsapp/src/actions/start-conversation.ts @@ -1,7 +1,9 @@ +import { isApiError } from '@botpress/client' +import { posthogHelper } from '@botpress/common' +import { INTEGRATION_NAME } from 'integration.definition' import { BodyComponent, BodyParameter, Language, Template } from 'whatsapp-api-js/messages' import { getDefaultBotPhoneNumberId, getAuthenticatedWhatsappClient } from '../auth' import { formatPhoneNumber } from '../misc/phone-number-to-whatsapp' -import { sendPosthogEvent, botpressEvents, sendPosthogError } from '../misc/posthogClient' import { getTemplateText, parseTemplateVariablesJSON } from '../misc/template-utils' import { TemplateVariables } from '../misc/types' import { hasAtleastOne, logForBotAndThrow } from '../misc/util' @@ -20,97 +22,97 @@ export const startConversation: bp.IntegrationProps['actions']['startConversatio client, logger, }) => { - try { - // Prevent the use of billable resources through the sandbox account - if (ctx.configurationType === 'sandbox') { - logForBotAndThrow('Sending template is not supported in sandbox mode', logger) - } + // Prevent the use of billable resources through the sandbox account + if (ctx.configurationType === 'sandbox') { + logForBotAndThrow('Sending template is not supported in sandbox mode', logger) + } - const { userPhone, templateName, templateVariablesJson } = input.conversation - const botPhoneNumberId = input.conversation.botPhoneNumberId - ? input.conversation.botPhoneNumberId - : await getDefaultBotPhoneNumberId(client, ctx).catch(() => { - logForBotAndThrow('No default bot phone number ID available', logger) - }) + const { userPhone, templateName, templateVariablesJson } = input.conversation + const botPhoneNumberId = input.conversation.botPhoneNumberId + ? input.conversation.botPhoneNumberId + : await getDefaultBotPhoneNumberId(client, ctx).catch(() => { + logForBotAndThrow('No default bot phone number ID available', logger) + }) - const templateLanguage = input.conversation.templateLanguage || 'en' - let templateVariables: TemplateVariables = [] - if (templateVariablesJson) { - templateVariables = parseTemplateVariablesJSON(templateVariablesJson, logger) - } + const templateLanguage = input.conversation.templateLanguage || 'en' + let templateVariables: TemplateVariables = [] + if (templateVariablesJson) { + templateVariables = parseTemplateVariablesJSON(templateVariablesJson, logger) + } - let formattedUserPhone = userPhone - try { - formattedUserPhone = formatPhoneNumber(userPhone) - } catch (thrown) { - await sendPosthogEvent({ - distinctId: userPhone, - event: botpressEvents.INVALID_PHONE_NUMBER, + let formattedUserPhone = userPhone + try { + formattedUserPhone = formatPhoneNumber(userPhone) + } catch (thrown) { + const distinctId = isApiError(thrown) ? thrown.id : undefined + await posthogHelper.sendPosthogEvent( + { + distinctId: distinctId ?? 'no id', + event: 'invalid_phone_number', properties: { from: 'action', + phoneNumber: userPhone, }, - }) - const errorMessage = (thrown instanceof Error ? thrown : new Error(String(thrown))).message - logForBotAndThrow(`Failed to parse phone number "${userPhone}": ${errorMessage}`, logger) - } - - const { conversation } = await client.getOrCreateConversation({ - channel: 'channel', - tags: { - botPhoneNumberId, - userPhone: formattedUserPhone, }, - }) - - const whatsapp = await getAuthenticatedWhatsappClient(client, ctx) - const language = new Language(templateLanguage) - const bodyParams: BodyParameter[] = templateVariables.map((variable) => ({ - type: 'text', - text: variable.toString(), - })) - const components = hasAtleastOne(bodyParams) ? [new BodyComponent(...bodyParams)] : [] - const template = new Template(templateName, language, ...components) + { integrationName: INTEGRATION_NAME, key: bp.secrets.POSTHOG_KEY } + ) + const errorMessage = (thrown instanceof Error ? thrown : new Error(String(thrown))).message + logForBotAndThrow(`Failed to parse phone number "${userPhone}": ${errorMessage}`, logger) + } - const response = await whatsapp.sendMessage(botPhoneNumberId, userPhone, template) + const { conversation } = await client.getOrCreateConversation({ + channel: 'channel', + tags: { + botPhoneNumberId, + userPhone: formattedUserPhone, + }, + }) - if ('error' in response) { - const errorJSON = JSON.stringify(response.error) - logForBotAndThrow( - `Failed to send WhatsApp template "${templateName}" with language "${templateLanguage}" - Error: ${errorJSON}`, - logger - ) - } + const whatsapp = await getAuthenticatedWhatsappClient(client, ctx) + const language = new Language(templateLanguage) + const bodyParams: BodyParameter[] = templateVariables.map((variable) => ({ + type: 'text', + text: variable.toString(), + })) + const components = hasAtleastOne(bodyParams) ? [new BodyComponent(...bodyParams)] : [] + const template = new Template(templateName, language, ...components) - await client - .createMessage({ - origin: 'synthetic', - conversationId: conversation.id, - userId: ctx.botId, - tags: {}, - type: 'text', - payload: { - text: await getTemplateText(ctx, client, logger, templateName, templateLanguage, templateVariables), - }, - }) - .catch((err: any) => { - logger.forBot().error(`Failed to Create synthetic message from template message - Error: ${err?.message ?? ''}`) - }) + const response = await whatsapp.sendMessage(botPhoneNumberId, userPhone, template) - logger - .forBot() - .info( - `Successfully sent WhatsApp template "${templateName}" with language "${templateLanguage}"${ - templateVariables && templateVariables.length - ? ` using template variables: ${JSON.stringify(templateVariables)}` - : ' without template variables' - }` - ) + if ('error' in response) { + const errorJSON = JSON.stringify(response.error) + logForBotAndThrow( + `Failed to send WhatsApp template "${templateName}" with language "${templateLanguage}" - Error: ${errorJSON}`, + logger + ) + } - return { + await client + .createMessage({ + origin: 'synthetic', conversationId: conversation.id, - } - } catch (thrown) { - await sendPosthogError(thrown, { from: 'action-start-conversation' }) - throw thrown + userId: ctx.botId, + tags: {}, + type: 'text', + payload: { + text: await getTemplateText(ctx, client, logger, templateName, templateLanguage, templateVariables), + }, + }) + .catch((err: any) => { + logger.forBot().error(`Failed to Create synthetic message from template message - Error: ${err?.message ?? ''}`) + }) + + logger + .forBot() + .info( + `Successfully sent WhatsApp template "${templateName}" with language "${templateLanguage}"${ + templateVariables && templateVariables.length + ? ` using template variables: ${JSON.stringify(templateVariables)}` + : ' without template variables' + }` + ) + + return { + conversationId: conversation.id, } } diff --git a/integrations/whatsapp/src/channels/channel.ts b/integrations/whatsapp/src/channels/channel.ts index a21b4271afe..9cfb8cfbcd9 100644 --- a/integrations/whatsapp/src/channels/channel.ts +++ b/integrations/whatsapp/src/channels/channel.ts @@ -14,7 +14,6 @@ import { import { getAuthenticatedWhatsappClient } from '../auth' import { WHATSAPP } from '../misc/constants' import { convertMarkdownToWhatsApp } from '../misc/markdown-to-whatsapp-rtf' -import { sendPosthogError } from '../misc/posthogClient' import { sleep } from '../misc/util' import { repeat } from '../repeat' import * as card from './message-types/card' @@ -27,182 +26,136 @@ import * as bp from '.botpress' export const channel: bp.IntegrationProps['channels']['channel'] = { messages: { text: async ({ payload, ...props }) => { - try { - if (payload.text.trim().length === 0) { - props.logger - .forBot() - .warn( - `Message ${props.message.id} skipped: payload text must contain at least one non-invisible character.` - ) - return - } - const text = convertMarkdownToWhatsApp(payload.text) - await _send({ ...props, message: new Text(text) }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-text' }) + if (payload.text.trim().length === 0) { + props.logger + .forBot() + .warn(`Message ${props.message.id} skipped: payload text must contain at least one non-invisible character.`) + return } + const text = convertMarkdownToWhatsApp(payload.text) + await _send({ ...props, message: new Text(text) }) }, image: async ({ payload, logger, ...props }) => { - try { - await _send({ - ...props, + await _send({ + ...props, + logger, + message: await image.generateOutgoingMessage({ + payload, logger, - message: await image.generateOutgoingMessage({ - payload, - logger, - }), - }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-image' }) - } + }), + }) }, audio: async ({ payload, ...props }) => { - try { - await _send({ - ...props, - message: new Audio(payload.audioUrl.trim(), false), - }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-audio' }) - } + await _send({ + ...props, + message: new Audio(payload.audioUrl.trim(), false), + }) }, video: async ({ payload, ...props }) => { - try { - await _send({ - ...props, - message: new Video(payload.videoUrl.trim(), false), - }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-video' }) - } + await _send({ + ...props, + message: new Video(payload.videoUrl.trim(), false), + }) }, file: async ({ payload, ...props }) => { - try { - const title = payload.title?.trim() - const url = payload.fileUrl.trim() - const inputFilename = payload.filename?.trim() - let filename = inputFilename || title || 'file' - const fileExtension = _extractFileExtension(filename) - if (!fileExtension) { - filename += _extractFileExtension(url) ?? '' - } - await _send({ - ...props, - message: new Document(url, false, title, filename), - }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-file' }) + const title = payload.title?.trim() + const url = payload.fileUrl.trim() + const inputFilename = payload.filename?.trim() + let filename = inputFilename || title || 'file' + const fileExtension = _extractFileExtension(filename) + if (!fileExtension) { + filename += _extractFileExtension(url) ?? '' } + await _send({ + ...props, + message: new Document(url, false, title, filename), + }) }, location: async ({ payload, ...props }) => { - try { - await _send({ - ...props, - message: new Location(payload.longitude, payload.latitude), - }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-location' }) - } + await _send({ + ...props, + message: new Location(payload.longitude, payload.latitude), + }) }, carousel: async ({ payload, logger, ...props }) => { - try { - await _sendMany({ ...props, logger, generator: carousel.generateOutgoingMessages(payload, logger) }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-carousel' }) - } + await _sendMany({ ...props, logger, generator: carousel.generateOutgoingMessages(payload, logger) }) }, card: async ({ payload, logger, ...props }) => { - try { - await _sendMany({ ...props, logger, generator: card.generateOutgoingMessages(payload, logger) }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-card' }) - } + await _sendMany({ ...props, logger, generator: card.generateOutgoingMessages(payload, logger) }) }, dropdown: async ({ payload, logger, ...props }) => { - try { + await _sendMany({ + ...props, + logger, + generator: dropdown.generateOutgoingMessages({ payload, logger }), + }) + }, + choice: async ({ payload, logger, ...props }) => { + if (payload.options.length <= WHATSAPP.INTERACTIVE_MAX_BUTTONS_COUNT) { + await _sendMany({ + ...props, + logger, + generator: choice.generateOutgoingMessages({ payload, logger }), + }) + } else { + // If choice options exceeds the maximum number of buttons allowed by WhatsApp we use a dropdown instead to avoid buttons being split into multiple groups with a repeated message. await _sendMany({ ...props, logger, generator: dropdown.generateOutgoingMessages({ payload, logger }), }) - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-dropdown' }) - } - }, - choice: async ({ payload, logger, ...props }) => { - try { - if (payload.options.length <= WHATSAPP.INTERACTIVE_MAX_BUTTONS_COUNT) { - await _sendMany({ - ...props, - logger, - generator: choice.generateOutgoingMessages({ payload, logger }), - }) - } else { - // If choice options exceeds the maximum number of buttons allowed by WhatsApp we use a dropdown instead to avoid buttons being split into multiple groups with a repeated message. - await _sendMany({ - ...props, - logger, - generator: dropdown.generateOutgoingMessages({ payload, logger }), - }) - } - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-choice' }) } }, bloc: async ({ payload, ...props }) => { - try { - if (!payload.items) { - return - } - for (const item of payload.items) { - switch (item.type) { - case 'text': - if (item.payload.text.trim().length === 0) { - props.logger - .forBot() - .warn( - `Message ${props.message.id} skipped: payload text must contain at least one non-invisible character.` - ) - break - } - await _send({ ...props, message: new Text(convertMarkdownToWhatsApp(item.payload.text)) }) - break - case 'image': - await _send({ - ...props, - message: await image.generateOutgoingMessage({ payload: item.payload, logger: props.logger }), - }) - break - case 'audio': - await _send({ ...props, message: new Audio(item.payload.audioUrl.trim(), false) }) - break - case 'video': - await _send({ - ...props, - message: new Video(item.payload.videoUrl.trim(), false), - }) - break - case 'file': - const title = item.payload.title?.trim() - const url = item.payload.fileUrl.trim() - const inputFilename = item.payload.filename?.trim() - let filename = inputFilename || title || 'file' - const fileExtension = _extractFileExtension(filename) - if (!fileExtension) { - filename += _extractFileExtension(url) ?? '' - } - await _send({ ...props, message: new Document(url, false, title, filename) }) - break - case 'location': - await _send({ ...props, message: new Location(item.payload.longitude, item.payload.latitude) }) + if (!payload.items) { + return + } + for (const item of payload.items) { + switch (item.type) { + case 'text': + if (item.payload.text.trim().length === 0) { + props.logger + .forBot() + .warn( + `Message ${props.message.id} skipped: payload text must contain at least one non-invisible character.` + ) break - default: - props.logger.forBot().warn('The type passed in bloc is not supported') - continue - } + } + await _send({ ...props, message: new Text(convertMarkdownToWhatsApp(item.payload.text)) }) + break + case 'image': + await _send({ + ...props, + message: await image.generateOutgoingMessage({ payload: item.payload, logger: props.logger }), + }) + break + case 'audio': + await _send({ ...props, message: new Audio(item.payload.audioUrl.trim(), false) }) + break + case 'video': + await _send({ + ...props, + message: new Video(item.payload.videoUrl.trim(), false), + }) + break + case 'file': + const title = item.payload.title?.trim() + const url = item.payload.fileUrl.trim() + const inputFilename = item.payload.filename?.trim() + let filename = inputFilename || title || 'file' + const fileExtension = _extractFileExtension(filename) + if (!fileExtension) { + filename += _extractFileExtension(url) ?? '' + } + await _send({ ...props, message: new Document(url, false, title, filename) }) + break + case 'location': + await _send({ ...props, message: new Location(item.payload.longitude, item.payload.latitude) }) + break + default: + props.logger.forBot().warn('The type passed in bloc is not supported') + continue } - } catch (thrown) { - await sendPosthogError(thrown, { from: 'channel-bloc' }) } }, }, diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index 268ca2275cd..b64e986199a 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -1,20 +1,25 @@ -import { sentry as sentryHelpers } from '@botpress/sdk-addons' +import { posthogHelper } from '@botpress/common' +import { INTEGRATION_NAME } from 'integration.definition' import actions from './actions' import channels from './channels' import { register, unregister } from './setup' import { handler } from './webhook' import * as bp from '.botpress' -const integration = new bp.Integration({ - register, - unregister, - actions, - channels, - handler, +@posthogHelper.wrapIntegration({ + integrationName: INTEGRATION_NAME, + key: bp.secrets.POSTHOG_KEY, }) +class WhatsappIntegration extends bp.Integration { + public constructor() { + super({ + register, + unregister, + actions, + channels, + handler, + }) + } +} -export default sentryHelpers.wrapIntegration(integration, { - dsn: bp.secrets.SENTRY_DSN, - environment: bp.secrets.SENTRY_ENVIRONMENT, - release: bp.secrets.SENTRY_RELEASE, -}) +export default new WhatsappIntegration() diff --git a/integrations/whatsapp/src/misc/posthogClient.ts b/integrations/whatsapp/src/misc/posthogClient.ts deleted file mode 100644 index c1f76bf1d6a..00000000000 --- a/integrations/whatsapp/src/misc/posthogClient.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { INTEGRATION_NAME } from 'integration.definition' -import { EventMessage, PostHog } from 'posthog-node' -import * as bp from '.botpress' - -type BotpressEventMessage = Omit & { - event: BotpressEvent -} - -type PostHogErrorOptions = { - from: string - integrationName: string -} - -export const botpressEvents = { - INVALID_PHONE_NUMBER: 'invalid_phone_number', - UNHANDLED_MESSAGE_TYPE: 'unhandled_message_type', - UNHANDLED_ERROR: 'unhandled_error', -} as const -type BotpressEvent = (typeof botpressEvents)[keyof typeof botpressEvents] - -export const sendPosthogEvent = async (props: BotpressEventMessage): Promise => { - const client = new PostHog(bp.secrets.POSTHOG_KEY, { - host: 'https://us.i.posthog.com', - }) - try { - const signedProps: BotpressEventMessage = { - ...props, - properties: { - ...props.properties, - integrationName: INTEGRATION_NAME, - }, - } - await client.captureImmediate(signedProps) - await client.shutdown() - console.info('PostHog event sent') - } catch (thrown: any) { - const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - console.error(`The server for posthog could not be reached - Error: ${errMsg}`) - } -} - -export const sendPosthogError = async (thrown: unknown, { from }: Partial): Promise => { - const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - await sendPosthogEvent({ - distinctId: errMsg, - event: botpressEvents.UNHANDLED_ERROR, - properties: { - from, - integrationName: INTEGRATION_NAME, - }, - }) -} diff --git a/integrations/whatsapp/src/setup.ts b/integrations/whatsapp/src/setup.ts index 6b10c3b322c..905b06c7d5f 100644 --- a/integrations/whatsapp/src/setup.ts +++ b/integrations/whatsapp/src/setup.ts @@ -1,39 +1,34 @@ import { RuntimeError } from '@botpress/sdk' -import { sendPosthogError } from './misc/posthogClient' import * as bp from '.botpress' export const register: bp.IntegrationProps['register'] = async (props) => { - try { - const configTypeName = props.ctx.configurationType ? props.ctx.configurationType : 'OAuth' - props.logger.forBot().debug(`Whatsapp Registration with configurationType ${configTypeName}`) + const configTypeName = props.ctx.configurationType ? props.ctx.configurationType : 'OAuth' + props.logger.forBot().debug(`Whatsapp Registration with configurationType ${configTypeName}`) - // Always make sure a bot is dissociated from WhatsApp conversations once the configuration type changes - const configureIntegrationProps: Parameters[0] = { - sandboxIdentifiers: null, - } - // Ensure that requests sent to a profile associated with a bot via OAuth are not received by the bot - if (props.ctx.configurationType !== null) { - configureIntegrationProps.identifier = null - } - await props.client.configureIntegration(configureIntegrationProps) - if (props.ctx.configurationType !== 'manual') { - return // nothing more to do if we're not using manual configuration - } + // Always make sure a bot is dissociated from WhatsApp conversations once the configuration type changes + const configureIntegrationProps: Parameters[0] = { + sandboxIdentifiers: null, + } + // Ensure that requests sent to a profile associated with a bot via OAuth are not received by the bot + if (props.ctx.configurationType !== null) { + configureIntegrationProps.identifier = null + } + await props.client.configureIntegration(configureIntegrationProps) + if (props.ctx.configurationType !== 'manual') { + return // nothing more to do if we're not using manual configuration + } + + const { accessToken, defaultBotPhoneNumberId, verifyToken } = props.ctx.configuration - const { accessToken, defaultBotPhoneNumberId, verifyToken } = props.ctx.configuration - - // clientSecret is optional and not required for validation - if (accessToken && defaultBotPhoneNumberId && verifyToken) { - // let's check the credentials - const isValidConfiguration = await _checkManualConfiguration(accessToken) - if (!isValidConfiguration) { - throw new RuntimeError('Error! Please check your credentials and webhook.') - } - } else { - throw new RuntimeError('Error! Please add the missing fields and save.') + // clientSecret is optional and not required for validation + if (accessToken && defaultBotPhoneNumberId && verifyToken) { + // let's check the credentials + const isValidConfiguration = await _checkManualConfiguration(accessToken) + if (!isValidConfiguration) { + throw new RuntimeError('Error! Please check your credentials and webhook.') } - } catch (thrown) { - await sendPosthogError(thrown, { from: 'register' }) + } else { + throw new RuntimeError('Error! Please add the missing fields and save.') } } diff --git a/integrations/whatsapp/src/webhook/handler.ts b/integrations/whatsapp/src/webhook/handler.ts index df3baa21807..a48062d6a05 100644 --- a/integrations/whatsapp/src/webhook/handler.ts +++ b/integrations/whatsapp/src/webhook/handler.ts @@ -1,7 +1,6 @@ import { Request } from '@botpress/sdk' import * as crypto from 'crypto' import { getClientSecret } from '../auth' -import { sendPosthogError } from '../misc/posthogClient' import { WhatsAppPayload, WhatsAppPayloadSchema } from '../misc/types' import { messagesHandler } from './handlers/messages' import { oauthCallbackHandler } from './handlers/oauth' @@ -164,7 +163,6 @@ const _handlerWrapper: typeof _handler = async (props: bp.HandlerProps) => { } return response } catch (thrown: unknown) { - await sendPosthogError(thrown, { from: 'handler' }) const errMsg = thrown instanceof Error ? thrown.message : 'Unknown error thrown' const errorMessage = `Webhook handler failed with error: ${errMsg}` props.logger.error(errorMessage) diff --git a/integrations/whatsapp/src/webhook/handlers/messages.ts b/integrations/whatsapp/src/webhook/handlers/messages.ts index a7c87477ece..4fcfa63b308 100644 --- a/integrations/whatsapp/src/webhook/handlers/messages.ts +++ b/integrations/whatsapp/src/webhook/handlers/messages.ts @@ -1,9 +1,10 @@ -import { RuntimeError } from '@botpress/client' +import { RuntimeError, isApiError } from '@botpress/client' +import { posthogHelper } from '@botpress/common' import { ValueOf } from '@botpress/sdk/dist/utils/type-utils' import axios from 'axios' +import { INTEGRATION_NAME } from 'integration.definition' import { getAccessToken, getAuthenticatedWhatsappClient } from '../../auth' import { formatPhoneNumber } from '../../misc/phone-number-to-whatsapp' -import { sendPosthogEvent, botpressEvents } from '../../misc/posthogClient' import { WhatsAppMessage, WhatsAppMessageValue } from '../../misc/types' import { getMessageFromWhatsappMessageId } from '../../misc/util' import { getMediaInfos } from '../../misc/whatsapp-utils' @@ -42,13 +43,18 @@ async function _handleIncomingMessage( try { userPhone = formatPhoneNumber(message.from) } catch (thrown) { - await sendPosthogEvent({ - distinctId: userPhone, - event: botpressEvents.INVALID_PHONE_NUMBER, - properties: { - from: 'handler', + const distinctId = isApiError(thrown) ? thrown.id : undefined + await posthogHelper.sendPosthogEvent( + { + distinctId: distinctId ?? 'no id', + event: 'invalid_phone_number', + properties: { + from: 'handler', + phoneNumber: message.from, + }, }, - }) + { integrationName: INTEGRATION_NAME, key: bp.secrets.POSTHOG_KEY } + ) const errorMessage = thrown instanceof Error ? thrown.message : String(thrown) logger.error(`Failed to parse phone number "${message.from}": ${errorMessage}`) } @@ -167,13 +173,6 @@ async function _handleIncomingMessage( const errors = message.errors?.map((err) => `${err.message} (${err.error_data.details})`).join('\n') logger.forBot().warn(`Received message type ${message.type} by WhatsApp, errors: ${errors ?? 'none'}`) } else { - await sendPosthogEvent({ - distinctId: 'WhatsApp', - event: botpressEvents.UNHANDLED_MESSAGE_TYPE, - properties: { - type, - }, - }) logger.forBot().warn(`Unhandled message type ${type}: ${JSON.stringify(message)}`) } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 0639b0371ba..4513d99def1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "4.25.2", + "version": "4.26.0", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", diff --git a/packages/cli/src/command-implementations/base-command.ts b/packages/cli/src/command-implementations/base-command.ts index d1df6ef4c93..6c0c3b7f98c 100644 --- a/packages/cli/src/command-implementations/base-command.ts +++ b/packages/cli/src/command-implementations/base-command.ts @@ -12,6 +12,10 @@ export abstract class BaseCommand { protected bootstrap?(): Promise protected teardown?(): Promise + private get _cmdName(): string { + return this.constructor.name + } + public async handler(): Promise<{ exitCode: number }> { let exitCode = 0 try { @@ -22,13 +26,10 @@ export abstract class BaseCommand { } catch (thrown) { const error = errors.BotpressCLIError.map(thrown) - if (error.debug) { - const msg = error.message + ' (Run with verbose flag (-v) to see more details)' - this.logger.error(msg) - this.logger.debug(error.debug) - } else { - this.logger.error(error.message) - } + this.logger.error(error.message) + + const stack = error.stack ?? 'No stack trace available' + this.logger.debug(`[${this._cmdName}] ${stack}`) exitCode = 1 } finally { diff --git a/packages/cli/src/command-implementations/bundle-command.ts b/packages/cli/src/command-implementations/bundle-command.ts index d4338d63737..bf538dd96d1 100644 --- a/packages/cli/src/command-implementations/bundle-command.ts +++ b/packages/cli/src/command-implementations/bundle-command.ts @@ -24,11 +24,11 @@ export class BundleCommand extends ProjectCommand { line.started('Bundling bot...') await this._bundle(abs.outFileCJS, buildContext) } else if (projectType === 'plugin') { - line.started('Bundling plugin with platform node...') - await this._bundle(abs.outFileCJS, buildContext) - - line.started('Bundling plugin with platform browser...') - await this._bundle(abs.outFileESM, buildContext, { platform: 'browser', format: 'esm' }) + line.started('Bundling plugin for node and browser platforms...') + await Promise.all([ + this._bundle(abs.outFileCJS, buildContext), + this._bundle(abs.outFileESM, buildContext, { platform: 'browser', format: 'esm' }), + ]) } else { throw new errors.UnsupportedProjectType() } diff --git a/packages/cli/src/errors.ts b/packages/cli/src/errors.ts index ac79febb144..fa5d433612f 100644 --- a/packages/cli/src/errors.ts +++ b/packages/cli/src/errors.ts @@ -4,7 +4,8 @@ import { VError } from 'verror' import * as consts from './consts' type KnownApiError = Exclude -const isKnownApiError = (e: unknown): e is KnownApiError => client.isApiError(e) && !(e instanceof client.UnknownError) +const isUnknownApiError = (e: unknown): e is client.UnknownError => client.isApiError(e) && e.type === 'Unknown' +const isKnownApiError = (e: unknown): e is KnownApiError => client.isApiError(e) && e.type !== 'Unknown' export class BotpressCLIError extends VError { public static wrap(thrown: unknown, message: string): BotpressCLIError { @@ -16,16 +17,20 @@ export class BotpressCLIError extends VError { if (thrown instanceof BotpressCLIError) { return thrown } - if (thrown instanceof client.UnknownError) { - let inst: HTTPError + if (isUnknownApiError(thrown)) { const cause = thrown.error?.cause - if (cause && typeof cause === 'object' && 'code' in cause && (cause as any).code === 'ECONNREFUSED') { - inst = new HTTPError(500, 'The connection was refused by the server') - } else { - inst = new HTTPError(500, 'An unknown error has occurred.') + if (cause && typeof cause === 'object' && 'code' in cause && cause.code === 'ECONNREFUSED') { + return new HTTPError(500, 'The connection was refused by the server') } - inst.debug = thrown.message - return inst + + const unknownMessage = 'An unknown API error occurred' + const actualTrimmedMessage = thrown.message.trim() + if (!actualTrimmedMessage) { + return new HTTPError(500, unknownMessage) + } + + const inner = new HTTPError(500, actualTrimmedMessage) + return new BotpressCLIError(inner, unknownMessage) } if (isKnownApiError(thrown)) { return HTTPError.fromApi(thrown) @@ -37,33 +42,17 @@ export class BotpressCLIError extends VError { const { message } = thrown return new BotpressCLIError(message) } - return new BotpressCLIError(`${thrown}`) + return new BotpressCLIError(String(thrown)) } - private readonly _debug: string[] - public constructor(error: BotpressCLIError, message: string) public constructor(message: string) public constructor(first: BotpressCLIError | string, second?: string) { if (typeof first === 'string') { super(first) - this._debug = [] return } super(first, second!) - this._debug = [...first._debug] - } - - public set debug(msg: string) { - this._debug.push(msg) - } - - public get debug(): string { - const dbgMsgs = this._debug.filter((s) => s.length) - if (!dbgMsgs.length) { - return '' - } - return 'Error: \n' + dbgMsgs.map((s) => ` ${s}`).join('\n') } } diff --git a/packages/common/package.json b/packages/common/package.json index fb8b405e77c..ee7ab297acd 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -11,6 +11,7 @@ "dedent": "^1.6.0", "marked": "^15.0.1", "openai": "^6.9.0", + "posthog-node": "^5.11.2", "preact": "^10.26.6", "preact-render-to-string": "^6.5.13", "remark": "^15.0.1", diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 87213d3f36b..ae7e3841c7d 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -11,3 +11,4 @@ export * from './user-resolver' export * from './sandbox' export * as meta from './meta' export * from './markdown-transformer' +export * from './posthog' diff --git a/packages/common/src/posthog/helper.ts b/packages/common/src/posthog/helper.ts new file mode 100644 index 00000000000..885db96ddaa --- /dev/null +++ b/packages/common/src/posthog/helper.ts @@ -0,0 +1,135 @@ +import * as client from '@botpress/client' +import * as sdk from '@botpress/sdk' +import { EventMessage, PostHog } from 'posthog-node' + +export const COMMON_SECRET_NAMES = { + POSTHOG_KEY: { + description: 'Posthog key for error dashboards', + }, +} satisfies sdk.IntegrationDefinitionProps['secrets'] + +type PostHogConfig = { + key: string + integrationName: string +} + +export const sendPosthogEvent = async (props: EventMessage, config: PostHogConfig): Promise => { + const { key, integrationName } = config + const client = new PostHog(key, { + host: 'https://us.i.posthog.com', + }) + try { + const signedProps: EventMessage = { + ...props, + properties: { + ...props.properties, + integrationName, + }, + } + await client.captureImmediate(signedProps) + await client.shutdown() + console.info('PostHog event sent') + } catch (thrown: unknown) { + const errMsg = thrown instanceof Error ? thrown.message : String(thrown) + console.error(`The server for posthog could not be reached - Error: ${errMsg}`) + } +} + +export function wrapIntegration(config: PostHogConfig) { + return function }>(constructor: T): T { + return class extends constructor { + public constructor(...args: any[]) { + super(...args) + this.props.register = wrapFunction(this.props.register, config) + this.props.unregister = wrapFunction(this.props.unregister, config) + this.props.handler = wrapFunction(wrapHandler(this.props.handler, config), config) + + if (this.props.actions) { + for (const actionType of Object.keys(this.props.actions)) { + const actionFn = this.props.actions[actionType] + if (typeof actionFn === 'function') { + this.props.actions[actionType] = wrapFunction(actionFn, config) + } + } + } + + if (this.props.channels) { + for (const channelName of Object.keys(this.props.channels)) { + const channel = this.props.channels[channelName] + if (!channel || !channel.messages) continue + Object.keys(channel.messages).forEach((messageType) => { + const messageFn = channel.messages[messageType] + if (typeof messageFn === 'function') { + channel.messages[messageType] = wrapFunction(messageFn, config) + } + }) + } + } + } + } + } +} + +function wrapFunction(fn: Function, config: PostHogConfig) { + return async (...args: any[]) => { + try { + return await fn(...args) + } catch (thrown) { + const errMsg = thrown instanceof Error ? thrown.message : String(thrown) + + const distinctId = client.isApiError(thrown) ? thrown.id : undefined + await sendPosthogEvent( + { + distinctId: distinctId ?? 'no id', + event: 'unhandled_error', + properties: { + from: fn.name, + integrationName: config.integrationName, + errMsg, + }, + }, + config + ) + throw thrown + } + } +} + +const isServerErrorStatus = (status: number): boolean => status >= 500 && status < 600 + +function wrapHandler(fn: Function, config: PostHogConfig) { + return async (...args: any[]) => { + const resp: void | Response = await fn(...args) + if (resp instanceof Response && isServerErrorStatus(resp.status)) { + if (!resp.body) { + await sendPosthogEvent( + { + distinctId: 'no id', + event: 'unhandled_error_empty_body', + properties: { + from: fn.name, + integrationName: config.integrationName, + errMsg: 'Empty Body', + }, + }, + config + ) + return resp + } + await sendPosthogEvent( + { + distinctId: 'no id', + event: 'unhandled_error', + properties: { + from: fn.name, + integrationName: config.integrationName, + errMsg: JSON.stringify(resp.body), + }, + }, + config + ) + return resp + } + return resp + } +} diff --git a/packages/common/src/posthog/index.ts b/packages/common/src/posthog/index.ts new file mode 100644 index 00000000000..d28ca819bbb --- /dev/null +++ b/packages/common/src/posthog/index.ts @@ -0,0 +1 @@ +export * as posthogHelper from './helper' diff --git a/plugins/conversation-insights/package.json b/plugins/conversation-insights/package.json index d79ca15da87..629789940be 100644 --- a/plugins/conversation-insights/package.json +++ b/plugins/conversation-insights/package.json @@ -6,7 +6,7 @@ }, "private": true, "dependencies": { - "@botpress/cognitive": "workspace:*", + "@botpress/cognitive": "0.3.0", "@botpress/sdk": "workspace:*", "browser-or-node": "^2.1.1", "jsonrepair": "^3.10.0" diff --git a/plugins/conversation-insights/plugin.definition.ts b/plugins/conversation-insights/plugin.definition.ts index 04418a73c8c..2efdac45f0e 100644 --- a/plugins/conversation-insights/plugin.definition.ts +++ b/plugins/conversation-insights/plugin.definition.ts @@ -2,7 +2,7 @@ import { PluginDefinition, z } from '@botpress/sdk' export default new PluginDefinition({ name: 'conversation-insights', - version: '0.4.4', + version: '0.4.5', configuration: { schema: z.object({ aiEnabled: z.boolean().default(true).describe('Set to true to enable title, summary and sentiment ai generation'), diff --git a/plugins/conversation-insights/src/prompt/prompt.ts b/plugins/conversation-insights/src/prompt/prompt.ts index bc4d2bece07..f293ac03801 100644 --- a/plugins/conversation-insights/src/prompt/prompt.ts +++ b/plugins/conversation-insights/src/prompt/prompt.ts @@ -42,4 +42,5 @@ export const createPrompt = (args: PromptArgs): LLMInput => ({ temperature: 0, systemPrompt: args.systemPrompt.trim(), messages: formatMessages(args.messages, args.context, args.botId), + model: 'fast', }) diff --git a/plugins/conversation-insights/src/updateAllConversations.test.ts b/plugins/conversation-insights/src/updateAllConversations.test.ts index 95716ed5f3a..62b92c0cb50 100644 --- a/plugins/conversation-insights/src/updateAllConversations.test.ts +++ b/plugins/conversation-insights/src/updateAllConversations.test.ts @@ -63,7 +63,6 @@ describe('updateAllConversations', () => { expect(props.client.listMessages).toHaveBeenCalledTimes(2) expect(updateTitleAndSummarySpy).toHaveBeenCalledTimes(2) expect(props.workflow.setCompleted).toHaveBeenCalled() - expect(props.logger.info).toHaveBeenCalledWith('updateAllConversations workflow completed') }) it('should handle no dirty conversations gracefully', async () => { diff --git a/plugins/conversation-insights/src/updateAllConversations.ts b/plugins/conversation-insights/src/updateAllConversations.ts index 1ac6a037af5..01cd0c43f36 100644 --- a/plugins/conversation-insights/src/updateAllConversations.ts +++ b/plugins/conversation-insights/src/updateAllConversations.ts @@ -2,14 +2,10 @@ import * as summaryUpdater from './tagsUpdater' import * as types from './types' import * as bp from '.botpress' -export type WorkflowProps = types.CommonProps & - bp.WorkflowHandlerProps['updateAllConversations'] & { nextToken?: string } +export type WorkflowProps = types.CommonProps & bp.WorkflowHandlerProps['updateAllConversations'] export const updateAllConversations = async (props: WorkflowProps) => { await props.workflow.acknowledgeStartOfProcessing() - const dirtyConversations = await props.client.listConversations({ - tags: { isDirty: 'true' }, - nextToken: props.nextToken, - }) + const dirtyConversations = await props.client.listConversations({ tags: { isDirty: 'true' } }) const promises: Promise[] = [] for (const conversation of dirtyConversations.conversations) { @@ -21,10 +17,7 @@ export const updateAllConversations = async (props: WorkflowProps) => { } await Promise.all(promises) - if (!dirtyConversations.meta.nextToken) { await props.workflow.setCompleted() - props.logger.info('updateAllConversations workflow completed') - return } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bf6c018bfa..37f6cd3c3c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1179,9 +1179,6 @@ importers: axios: specifier: ^1.6.2 version: 1.7.4 - posthog-node: - specifier: ^5.10.4 - version: 5.11.1 devDependencies: '@botpress/common': specifier: workspace:* @@ -1375,9 +1372,6 @@ importers: messaging-api-messenger: specifier: ^1.1.0 version: 1.1.0 - posthog-node: - specifier: ^5.10.4 - version: 5.11.1 devDependencies: '@botpress/common': specifier: workspace:* @@ -1804,9 +1798,6 @@ importers: axios: specifier: ^1.6.0 version: 1.8.4 - posthog-node: - specifier: ^5.10.4 - version: 5.11.1 query-string: specifier: ^6.14.1 version: 6.14.1 @@ -1937,9 +1928,6 @@ importers: marked: specifier: ^15.0.1 version: 15.0.1 - posthog-node: - specifier: ^5.10.4 - version: 5.11.1 preact: specifier: ^10.26.6 version: 10.26.6 @@ -2598,6 +2586,9 @@ importers: openai: specifier: ^6.9.0 version: 6.9.0(ws@8.18.2) + posthog-node: + specifier: ^5.11.2 + version: 5.11.2 preact: specifier: ^10.26.6 version: 10.26.6 @@ -2911,7 +2902,7 @@ importers: plugins/conversation-insights: dependencies: '@botpress/cognitive': - specifier: workspace:* + specifier: 0.3.0 version: link:../../packages/cognitive '@botpress/sdk': specifier: workspace:* @@ -4947,8 +4938,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@posthog/core@1.5.1': - resolution: {integrity: sha512-8fdEzfvdStr45iIncTD+gnqp45UBTUpRK/bwB4shP5usCKytnPIeilU8rIpNBOVjJPwfW+2N8yWhQ0l14x191Q==} + '@posthog/core@1.5.2': + resolution: {integrity: sha512-iedUP3EnOPPxTA2VaIrsrd29lSZnUV+ZrMnvY56timRVeZAXoYCkmjfIs3KBAsF8OUT5h1GXLSkoQdrV0r31OQ==} '@react-email/body@0.0.10': resolution: {integrity: sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==} @@ -9790,8 +9781,8 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - posthog-node@5.11.1: - resolution: {integrity: sha512-P6rtzdCVvS718r011x0W0cwmJo7gfP5YWXiWh0/S3OL+pnHtcqbWHDjrtRxN/IrMkjZWzMU4xDze5vRK/cZ23w==} + posthog-node@5.11.2: + resolution: {integrity: sha512-z+XekcBUmGePMsjPlGaEF2bJFiDHKHYPQjS4OEw4YPDQz8s7Owuim/L7xNX+6UJkyIRniBza9iC7bW8yrGTv1g==} engines: {node: '>=20'} preact-render-to-string@6.5.13: @@ -13950,7 +13941,7 @@ snapshots: '@pkgr/core@0.2.9': {} - '@posthog/core@1.5.1': + '@posthog/core@1.5.2': dependencies: cross-spawn: 7.0.6 @@ -20109,9 +20100,9 @@ snapshots: dependencies: xtend: 4.0.2 - posthog-node@5.11.1: + posthog-node@5.11.2: dependencies: - '@posthog/core': 1.5.1 + '@posthog/core': 1.5.2 preact-render-to-string@6.5.13(preact@10.26.6): dependencies: diff --git a/readme.md b/readme.md index d1d93df45dd..3943f062d46 100644 --- a/readme.md +++ b/readme.md @@ -101,11 +101,11 @@ Coming soon. ## Devtools -| **Package** | **Description** | **Docs** | **Code** | -| -------------------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------- | ---------------------- | -| [`@botpress/cli`](https://www.npmjs.com/package/@botpress/cli) | Build, Deploy and Manage Bots, Integrations and Plugins | [Docs](https://botpress.com/docs/integration/cli/) | [Code](./packages/cli) | -| [`@botpress/client`](https://www.npmjs.com/package/@botpress/client) | Type-safe clients to consume the Botpress APIs | [Docs]() | [Code]() | -| [`@botpress/sdk`](https://www.npmjs.com/package/@botpress/sdk) | SDK used by to build integrations | [Docs]() | [Code]() | +| **Package** | **Description** | **Docs** | **Code** | +| -------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------- | +| [`@botpress/cli`](https://www.npmjs.com/package/@botpress/cli) | Build, Deploy and Manage Bots, Integrations and Plugins | [Docs](https://www.botpress.com/docs/for-developers/sdk/cli-reference) | [Code](./packages/cli) | +| [`@botpress/client`](https://www.npmjs.com/package/@botpress/client) | Type-safe clients to consume the Botpress APIs | [Docs]() | [Code]() | +| [`@botpress/sdk`](https://www.npmjs.com/package/@botpress/sdk) | SDK used by to build integrations | [Docs]() | [Code]() | ## Local Development