Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integrations/gmail/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from './definitions'

export const INTEGRATION_NAME = 'gmail'
export const INTEGRATION_VERSION = '1.0.4'
export const INTEGRATION_VERSION = '1.0.5'

export default new sdk.IntegrationDefinition({
name: INTEGRATION_NAME,
Expand Down
37 changes: 1 addition & 36 deletions integrations/gmail/src/setup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { posthogHelper } from '@botpress/common'
import { posthogConfig } from 'src'
import { GoogleClient } from './google-api'
import * as bp from '.botpress'

export const register: bp.IntegrationProps['register'] = async ({ client, ctx, logger }) => {
const startTime = Date.now()
let googleClient: GoogleClient

const createFromRefreshToken = async () => {
Expand Down Expand Up @@ -49,38 +46,6 @@ export const register: bp.IntegrationProps['register'] = async ({ client, ctx, l
} catch (error) {
logger.forBot().error(`Failed to set up Gmail watch ${error}`)
}

const configurationTimeMs = Date.now() - startTime

await posthogHelper
.sendPosthogEvent(
{
distinctId: ctx.integrationId,
event: 'integration_registered',
properties: {
botId: ctx.botId,
configurationType: ctx.configurationType,
configurationTimeMs,
},
},
posthogConfig
)
.catch(() => {
// Silently fail if PostHog is unavailable
})
}

export const unregister: bp.IntegrationProps['unregister'] = async ({ ctx }) => {
await posthogHelper
.sendPosthogEvent(
{
distinctId: ctx.integrationId,
event: 'integration_unregistered',
properties: {
botId: ctx.botId,
},
},
posthogConfig
)
.catch(() => {})
}
export const unregister: bp.IntegrationProps['unregister'] = async () => {}
2 changes: 1 addition & 1 deletion integrations/gsheets/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from './definitions'

export const INTEGRATION_NAME = 'gsheets'
export const INTEGRATION_VERSION = '2.1.0'
export const INTEGRATION_VERSION = '2.1.2'

export default new sdk.IntegrationDefinition({
name: INTEGRATION_NAME,
Expand Down
33 changes: 32 additions & 1 deletion integrations/gsheets/src/google-api/error-handling.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { createAsyncFnWrapperWithErrorRedaction, createErrorHandlingDecorator } from '@botpress/common'
import { isApiError } from '@botpress/client'
import { createAsyncFnWrapperWithErrorRedaction, createErrorHandlingDecorator, posthogHelper } from '@botpress/common'
import * as sdk from '@botpress/sdk'
import { Common as GoogleApisCommon } from 'googleapis'
import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition'
import * as bp from '.botpress'

export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction((error: Error, customMessage: string) => {
if (error instanceof sdk.RuntimeError) {
Expand All @@ -10,6 +13,34 @@ export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction((e
const googleError = _extractGoogleApiError(error)
const redactedMessage = googleError ? `${customMessage}: ${googleError}` : customMessage

const errorMessage = error.message || String(error)
const distinctId = isApiError(error) ? error.id : undefined
const statusCode = _isGaxiosError(error) ? error.response?.status : undefined
const errorReason = _isGaxiosError(error) ? error.response?.statusText : undefined

posthogHelper
.sendPosthogEvent(
{
distinctId: distinctId ?? 'no id',
event: 'gsheets_api_error',
properties: {
from: 'google_sheets_client',
errorMessage: customMessage,
googleError: googleError?.substring(0, 200) || errorMessage.substring(0, 200),
statusCode: statusCode?.toString(),
errorReason: errorReason?.substring(0, 100),
},
},
{
integrationName: INTEGRATION_NAME,
integrationVersion: INTEGRATION_VERSION,
key: bp.secrets.POSTHOG_KEY,
}
)
.catch(() => {
// Silently fail if PostHog is unavailable
})

return new sdk.RuntimeError(redactedMessage)
})

Expand Down
40 changes: 1 addition & 39 deletions integrations/gsheets/src/setup.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,12 @@
import { posthogHelper } from '@botpress/common'
import { posthogConfig } from 'src'
import { GoogleClient } from './google-api/google-client'
import * as bp from '.botpress'

export const register: bp.IntegrationProps['register'] = async ({ logger, ctx, client }) => {
const startTime = Date.now()

logger.forBot().info('Registering Google Sheets integration')

const gsheetsClient = await GoogleClient.create({ ctx, client })
const summary = await gsheetsClient.getSpreadsheetSummary()
logger.forBot().info(`Successfully connected to Google Sheets: ${summary}`)

const configurationTimeMs = Date.now() - startTime

await posthogHelper
.sendPosthogEvent(
{
distinctId: ctx.integrationId,
event: 'integration_registered',
properties: {
botId: ctx.botId,
configurationType: ctx.configurationType,
configurationTimeMs,
},
},
posthogConfig
)
.catch(() => {
// Silently fail if PostHog is unavailable
})
}

export const unregister: bp.IntegrationProps['unregister'] = async ({ ctx }) => {
await posthogHelper
.sendPosthogEvent(
{
distinctId: ctx.integrationId,
event: 'integration_unregistered',
properties: {
botId: ctx.botId,
},
},
posthogConfig
)
.catch(() => {
// Silently fail if PostHog is unavailable
})
}
export const unregister: bp.IntegrationProps['unregister'] = async () => {}
2 changes: 1 addition & 1 deletion integrations/zendesk/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { actions, events, configuration, channels, states, user } from './src/de
export default new sdk.IntegrationDefinition({
name: 'zendesk',
title: 'Zendesk',
version: '3.0.5',
version: '3.0.6',
icon: 'icon.svg',
description:
'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.',
Expand Down
10 changes: 9 additions & 1 deletion integrations/zendesk/src/events/message-received.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { TriggerPayload } from '../triggers'
import { retrieveHitlConversation } from './hitl-ticket-filter'
import * as bp from '.botpress'

const COMMENT_ON_BEHALF_START = '----------------------------------------------\n\n!***'

export const executeMessageReceived = async ({
zendeskClient,
zendeskTrigger,
Expand Down Expand Up @@ -65,7 +67,13 @@ export const executeMessageReceived = async ({
})
}

const messageWithoutAuthor = zendeskTrigger.comment.split('\n').slice(3).join('\n')
let messageWithoutAuthor: string

if (zendeskTrigger.comment.startsWith(COMMENT_ON_BEHALF_START)) {
messageWithoutAuthor = zendeskTrigger.comment.split('\n').slice(5).join('\n')
} else {
messageWithoutAuthor = zendeskTrigger.comment.split('\n').slice(3).join('\n')
}

await client.createMessage({
tags: { zendeskCommentId: zendeskTrigger.commentId },
Expand Down
44 changes: 44 additions & 0 deletions packages/common/src/posthog/boolean-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, test, beforeEach } from 'vitest'
import { useBooleanGenerator } from './boolean-generator'

describe('Boolean Generator', () => {
test.each([0, -10, 101, 50.5, NaN])('Should throw error for invalid percentage: %p', (percentage) => {
expect(() => useBooleanGenerator(percentage)).toThrow('Percentage must be an integer between 1 and 100')
})

test.each([1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 99].map((p) => ({ percentage: p })))(
'$percentage%% probability, should be true approximately $percentage%% the time',
({ percentage }) => {
const CYCLES = 1000000
/** In percentage */
const TOLERANCE = 1

let shouldAllow = useBooleanGenerator(percentage)
let trueCount = 0
for (let i = 0; i < CYCLES; i++) {
if (shouldAllow()) {
trueCount++
}
}

const truthyPercentage = (trueCount / CYCLES) * 100
expect(truthyPercentage).toBeGreaterThan(percentage - TOLERANCE)
expect(truthyPercentage).toBeLessThan(percentage + TOLERANCE)
}
)

test('100% probability, should be true all the time', () => {
const CYCLES = 10000

let shouldAllow = useBooleanGenerator(100)
let trueCount = 0
for (let i = 0; i < CYCLES; i++) {
if (shouldAllow()) {
trueCount++
}
}

const truthyPercentage = (trueCount / CYCLES) * 100
expect(truthyPercentage).toBe(100)
})
})
12 changes: 12 additions & 0 deletions packages/common/src/posthog/boolean-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const useBooleanGenerator = (truthyPercentage: number): (() => boolean) => {
if (truthyPercentage <= 0 || truthyPercentage > 100 || !Number.isInteger(truthyPercentage)) {
throw new Error('Percentage must be an integer between 1 and 100')
}

if (truthyPercentage === 100) {
return () => true
}

const probability = truthyPercentage / 100
return () => Math.random() <= probability
}
32 changes: 29 additions & 3 deletions packages/common/src/posthog/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as client from '@botpress/client'
import * as sdk from '@botpress/sdk'

import { EventMessage, PostHog } from 'posthog-node'
import { useBooleanGenerator } from './boolean-generator'

export const COMMON_SECRET_NAMES = {
POSTHOG_KEY: {
Expand All @@ -13,6 +14,9 @@ export type PostHogConfig = {
key: string
integrationName: string
integrationVersion: string
/** An integer percentage between 1 and 100 which determines
* what percentage of events are allowed through. */
rateLimitPercentage?: number
}

type WrapFunctionProps = {
Expand All @@ -22,11 +26,19 @@ type WrapFunctionProps = {
functionArea: string
}

export const sendPosthogEvent = async (props: EventMessage, config: PostHogConfig): Promise<void> => {
const { key, integrationName, integrationVersion } = config
const client = new PostHog(key, {
const createPostHogClient = (key: string, rateLimitPercentage: number = 100): PostHog => {
const shouldAllow = useBooleanGenerator(rateLimitPercentage)
return new PostHog(key, {
host: 'https://us.i.posthog.com',
before_send: (event) => {
return shouldAllow() ? event : null
},
})
}

export const sendPosthogEvent = async (props: EventMessage, config: PostHogConfig): Promise<void> => {
const { key, integrationName, integrationVersion, rateLimitPercentage } = config
const client = createPostHogClient(key, rateLimitPercentage)
try {
const signedProps: EventMessage = {
...props,
Expand Down Expand Up @@ -103,6 +115,20 @@ function wrapFunction(props: WrapFunctionProps) {
const { config, fn, functionArea, functionName } = props
return async (...args: any[]) => {
try {
await sendPosthogEvent(
{
distinctId: `${config.integrationName}_${config.integrationVersion}`,
event: `${config.integrationName}_${functionName}`, // e.g. "gmail_registered"
properties: {
from: functionName,
area: functionArea,
integrationName: config.integrationName,
integrationVersion: config.integrationVersion,
},
},
config
)

return await fn(...args)
} catch (thrown) {
const errMsg = thrown instanceof Error ? thrown.message : String(thrown)
Expand Down
Loading