2
0

Add Segment block (#1672)

Added support for Twilio Segment

New Segment Block added to Forge. Includes the following:
- Credentials
- Identify User, including Traits
- Alias User
- Track Event, including Properties
- Track Bot View ( Page ), including Properties
- Generate UUID for User IDs

Tested integration with Segment, verified working. Uses Segment's NodeJS
analytics library to support non-browser based platforms, such as
WhatsApp

---------

Co-authored-by: John Walsh <john@famkit.com>
Co-authored-by: Baptiste Arnaud <baptiste.arnaud95@gmail.com>
This commit is contained in:
John Walsh
2024-08-16 11:23:23 -04:00
committed by GitHub
parent d197cf9b4d
commit 29ab812512
18 changed files with 4382 additions and 3023 deletions

View File

@ -0,0 +1,25 @@
---
title: Segment
---
With the Segment block, you can send events to Twilio's Segment and trigger your Segment workflows. It uses the Segment Analytics Node.js library to send server side events to Segment, so that it works on non web browser devices.
## How to find my `Write Key`?
To find your `Write Key`, you need to go to your Segment dashboard and click on the `Sources` tab. Then click on the `API Keys` button of the source you want to use.
## Identify User
This action allows you to identify a user in Segment. It requires the `User ID` and `Email` and can optionally take a set of `Traits`.
## Alias
This action allows you to alias a user in Segment. It requires the `Previous ID` and the `User ID`.
## Event
This action allows you to track an event in Segment. It requires the `Event Name` and `User ID`, and can optionally take a set of `Properties`.
## Page
This action allows you to send a page view event to Segment. It requires the Chatbot `Name` and can optionally take a `Category` name and also a set of `Properties`.

View File

@ -126,7 +126,8 @@
"editor/blocks/integrations/elevenlabs", "editor/blocks/integrations/elevenlabs",
"editor/blocks/integrations/anthropic", "editor/blocks/integrations/anthropic",
"editor/blocks/integrations/dify-ai", "editor/blocks/integrations/dify-ai",
"editor/blocks/integrations/nocodb" "editor/blocks/integrations/nocodb",
"editor/blocks/integrations/segment"
] ]
} }
] ]

View File

@ -0,0 +1,38 @@
import { createAction, option } from '@typebot.io/forge'
import { Analytics } from '@segment/analytics-node'
import { auth } from '../auth'
export const alias = createAction({
auth,
name: 'Alias',
options: option.object({
userId: option.string.layout({
label: 'User ID',
isRequired: true,
moreInfoTooltip: 'New ID of the user.',
}),
previousId: option.string.layout({
label: 'Previous ID',
moreInfoTooltip: 'Previous ID of the user to alias.',
}),
}),
run: {
server: async ({
credentials: { apiKey },
options: { userId, previousId },
}) => {
if (!userId || userId.length === 0
|| !previousId || previousId.length === 0
|| apiKey === undefined) return
const analytics = new Analytics({ writeKey: apiKey })
analytics.alias({
userId: userId,
previousId: previousId
})
await analytics.closeAndFlush()
}
},
})

View File

@ -0,0 +1,71 @@
import { createAction, option } from '@typebot.io/forge'
import { Analytics } from '@segment/analytics-node'
import { auth } from '../auth'
export const identify = createAction({
auth,
name: 'Identify User',
options: option.object({
userId: option.string.layout({
label: 'User ID',
isRequired: true,
}),
email: option.string.layout({
label: 'Email',
isRequired: false,
}),
traits: option.array(option.object({
key: option.string.layout({
label: 'Key',
isRequired: true,
}),
value: option.string.layout({
label: 'Value',
isRequired: true,
}),
})).layout({
itemLabel: 'trait',
}),
}),
run: {
server: async ({
credentials: { apiKey },
options: { userId, email, traits },
}) => {
if (!email || email.length === 0
|| !userId || userId.length === 0
|| apiKey === undefined) return
const analytics = new Analytics({ writeKey: apiKey })
if (traits === undefined || traits.length === 0) {
analytics.identify({
userId: userId,
traits: {
email: email
}
})
} else {
analytics.identify({
userId: userId,
traits: createTraits(traits, email)
})
}
await analytics.closeAndFlush()
}
},
})
const createTraits = (traits: { key?: string; value?: string }[], email: string) => {
const _traits: Record<string, any> = {}
// add email as a default trait
traits.push({ key: 'email', value: email })
traits.forEach(({ key, value }) => {
if (!key || !value) return
_traits[key] = value
})
return _traits
}

View File

@ -0,0 +1,70 @@
import { createAction, option } from '@typebot.io/forge'
import { Analytics } from '@segment/analytics-node'
import { auth } from '../auth'
export const trackEvent = createAction({
auth,
name: 'Track',
options: option.object({
eventName: option.string.layout({
label: 'Name',
isRequired: true,
}),
userId: option.string.layout({
label: 'User ID',
isRequired: true,
}),
properties: option.array(option.object({
key: option.string.layout({
label: 'Key',
isRequired: true,
}),
value: option.string.layout({
label: 'Value',
isRequired: true,
}),
})).layout({
itemLabel: 'property',
}),
}),
run: {
server: async ({
credentials: { apiKey },
options: { eventName, userId, properties },
}) => {
if (!eventName || eventName.length === 0
|| !userId || userId.length === 0
|| apiKey === undefined) return
const analytics = new Analytics({ writeKey: apiKey })
if (properties === undefined || properties.length === 0) {
analytics.track({
userId: userId,
event: eventName
})
} else {
analytics.track({
userId: userId,
event: eventName,
properties: createProperties(properties)
})
}
await analytics.closeAndFlush()
}
},
})
const createProperties = (properties: { key?: string; value?: string }[]) => {
const props: Record<string, any> = {}
properties.forEach(({ key, value }) => {
if (!key || !value) return
props[key] = value
})
return props
}

View File

@ -0,0 +1,76 @@
import { createAction, option } from '@typebot.io/forge'
import { Analytics } from '@segment/analytics-node'
import { auth } from '../auth'
export const trackPage = createAction({
auth,
name: 'Page',
options: option.object({
userId: option.string.layout({
label: 'User ID',
isRequired: true,
}),
name: option.string.layout({
label: 'Name',
isRequired: true,
}),
category: option.string.layout({
label: 'Category',
isRequired: false,
}),
properties: option.array(option.object({
key: option.string.layout({
label: 'Key',
isRequired: true,
}),
value: option.string.layout({
label: 'Value',
isRequired: true,
}),
})).layout({
itemLabel: 'property',
}),
}),
run: {
server: async ({
credentials: { apiKey },
options: { userId, name, category, properties },
}) => {
if (!name || name.length === 0
|| !userId || userId.length === 0
|| apiKey === undefined) return
const analytics = new Analytics({ writeKey: apiKey })
if (properties === undefined || properties.length === 0) {
analytics.page({
userId: userId,
name: name,
category: category != undefined ? category : ''
});
} else {
analytics.page({
userId: userId,
name: name,
category: category != undefined ? category : '',
properties: createProperties(properties)
});
}
await analytics.closeAndFlush()
}
},
})
const createProperties = (properties: { key?: string; value?: string }[]) => {
const props: Record<string, any> = {}
properties.forEach(({ key, value }) => {
if (!key || !value) return
props[key] = value
})
return props
}

View File

@ -0,0 +1,16 @@
import { option, AuthDefinition } from '@typebot.io/forge'
export const auth = {
type: 'encryptedCredentials',
name: 'Segment account',
schema: option.object({
apiKey: option.string.layout({
label: 'Write Key',
isRequired: true,
inputType: 'password',
helperText: 'You can find your Write Key in your Segment source settings.',
withVariableButton: false,
isDebounceDisabled: true,
})
}),
} satisfies AuthDefinition

View File

@ -0,0 +1,16 @@
import { createBlock } from '@typebot.io/forge'
import { SegmentLogo } from './logo'
import { auth } from './auth'
import { identify } from './actions/identify'
import { trackEvent } from './actions/trackEvent'
import { alias } from './actions/alias'
import { trackPage } from './actions/trackPage'
export const segmentBlock = createBlock({
id: 'segment',
name: 'Segment',
tags: ['events', 'analytics'],
LightLogo: SegmentLogo,
auth,
actions: [alias, identify, trackPage, trackEvent]
})

View File

@ -0,0 +1,30 @@
/** @jsxImportSource react */
export const SegmentLogo = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 32 32" {...props}>
<path
d="M30.096 10.093H13.1186C12.7596 10.093 12.4686 10.384 12.4686 10.743V12.6145C12.4686 12.9735 12.7596 13.2645 13.1186 13.2645H30.096C30.455 13.2645 30.746 12.9735 30.746 12.6145V10.743C30.746 10.384 30.455 10.093 30.096 10.093Z"
fill="#52BD95"
/>
<path
d="M18.7872 18.0176H1.80979C1.4508 18.0176 1.15979 18.3087 1.15979 18.6676V20.5391C1.15979 20.8981 1.4508 21.1891 1.80979 21.1891H18.7872C19.1462 21.1891 19.4372 20.8981 19.4372 20.5391V18.6676C19.4372 18.3087 19.1462 18.0176 18.7872 18.0176Z"
fill="#52BD95"
/>
<path
d="M3.35259 12.2631C3.66453 12.3437 3.9837 12.1602 4.07113 11.8502C6.03309 5.7089 12.3512 2.07413 18.6464 3.46509C18.9516 3.52799 19.2527 3.34116 19.3319 3.03975L19.8481 1.11331C19.8889 0.957036 19.8641 0.790884 19.7795 0.653294C19.695 0.515704 19.5579 0.418597 19.4 0.384442C11.4398 -1.40735 3.43933 3.22032 1.02352 11.0139C0.97921 11.1662 0.998011 11.3299 1.07568 11.4681C1.15336 11.6064 1.28338 11.7076 1.43647 11.749L3.35259 12.2631Z"
fill="#52BD95"
/>
<path
d="M28.5573 19.0108C28.2428 18.9262 27.9188 19.1103 27.8305 19.4238C25.8727 25.5691 19.5534 29.2091 13.2552 27.8191C12.9498 27.7551 12.648 27.9424 12.5697 28.2445L12.0639 30.1606C12.0224 30.3172 12.0469 30.4839 12.1315 30.6219C12.2161 30.76 12.3536 30.8574 12.5119 30.8915C20.4721 32.6789 28.4701 28.0517 30.8884 20.26C30.9333 20.108 30.9147 19.9443 30.8369 19.8062C30.7591 19.6681 30.6287 19.5674 30.4755 19.527L28.5573 19.0108Z"
fill="#52BD95"
/>
<path
d="M24.9894 6.61181C25.9495 6.61181 26.7279 5.83344 26.7279 4.87327C26.7279 3.9131 25.9495 3.13473 24.9894 3.13473C24.0292 3.13473 23.2508 3.9131 23.2508 4.87327C23.2508 5.83344 24.0292 6.61181 24.9894 6.61181Z"
fill="#52BD95"
/>
<path
d="M6.92052 28.1454C7.88069 28.1454 8.65906 27.367 8.65906 26.4068C8.65906 25.4467 7.88069 24.6683 6.92052 24.6683C5.96035 24.6683 5.18198 25.4467 5.18198 26.4068C5.18198 27.367 5.96035 28.1454 6.92052 28.1454Z"
fill="#52BD95"
/>
</svg>
)

View File

@ -0,0 +1,18 @@
{
"name": "@typebot.io/segment-block",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"typescript": "5.4.5"
},
"dependencies": {
"@segment/analytics-node": "^2.1.2"
}
}

View File

@ -0,0 +1,10 @@
// Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { segmentBlock } from '.'
import { auth } from './auth'
export const segmentBlockSchema = parseBlockSchema(segmentBlock)
export const segmentCredentialsSchema = parseBlockCredentials(
segmentBlock.id,
auth.schema
)

View File

@ -0,0 +1,11 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "react"
}
}

View File

@ -13,4 +13,5 @@ export const forgedBlockIds = [
'together-ai', 'together-ai',
'open-router', 'open-router',
'nocodb', 'nocodb',
'segment',
] as const satisfies ForgedBlock['type'][] ] as const satisfies ForgedBlock['type'][]

View File

@ -16,6 +16,8 @@ import { togetherAiBlock } from '@typebot.io/together-ai-block'
import { togetherAiCredentialsSchema } from '@typebot.io/together-ai-block/schemas' import { togetherAiCredentialsSchema } from '@typebot.io/together-ai-block/schemas'
import { nocodbBlock } from '@typebot.io/nocodb-block' import { nocodbBlock } from '@typebot.io/nocodb-block'
import { nocodbCredentialsSchema } from '@typebot.io/nocodb-block/schemas' import { nocodbCredentialsSchema } from '@typebot.io/nocodb-block/schemas'
import { segmentBlock } from '@typebot.io/segment-block'
import { segmentCredentialsSchema } from '@typebot.io/segment-block/schemas'
export const forgedCredentialsSchemas = { export const forgedCredentialsSchemas = {
[openAIBlock.id]: openAICredentialsSchema, [openAIBlock.id]: openAICredentialsSchema,
@ -27,4 +29,5 @@ export const forgedCredentialsSchemas = {
[togetherAiBlock.id]: togetherAiCredentialsSchema, [togetherAiBlock.id]: togetherAiCredentialsSchema,
[openRouterBlock.id]: openRouterCredentialsSchema, [openRouterBlock.id]: openRouterCredentialsSchema,
[nocodbBlock.id]: nocodbCredentialsSchema, [nocodbBlock.id]: nocodbCredentialsSchema,
[segmentBlock.id]: segmentCredentialsSchema,
} }

View File

@ -10,6 +10,7 @@ import { chatNodeBlock } from '@typebot.io/chat-node-block'
import { calComBlock } from '@typebot.io/cal-com-block' import { calComBlock } from '@typebot.io/cal-com-block'
import { openAIBlock } from '@typebot.io/openai-block' import { openAIBlock } from '@typebot.io/openai-block'
import { nocodbBlock } from '@typebot.io/nocodb-block' import { nocodbBlock } from '@typebot.io/nocodb-block'
import { segmentBlock } from '@typebot.io/segment-block'
export const forgedBlocks = { export const forgedBlocks = {
[openAIBlock.id]: openAIBlock, [openAIBlock.id]: openAIBlock,
@ -23,4 +24,5 @@ export const forgedBlocks = {
[togetherAiBlock.id]: togetherAiBlock, [togetherAiBlock.id]: togetherAiBlock,
[openRouterBlock.id]: openRouterBlock, [openRouterBlock.id]: openRouterBlock,
[nocodbBlock.id]: nocodbBlock, [nocodbBlock.id]: nocodbBlock,
[segmentBlock.id]: segmentBlock,
} }

View File

@ -17,6 +17,7 @@
"@typebot.io/anthropic-block": "workspace:*", "@typebot.io/anthropic-block": "workspace:*",
"@typebot.io/together-ai-block": "workspace:*", "@typebot.io/together-ai-block": "workspace:*",
"@typebot.io/open-router-block": "workspace:*", "@typebot.io/open-router-block": "workspace:*",
"@typebot.io/nocodb-block": "workspace:*" "@typebot.io/nocodb-block": "workspace:*",
"@typebot.io/segment-block": "workspace:*"
} }
} }

View File

@ -21,6 +21,8 @@ import { togetherAiBlock } from '@typebot.io/together-ai-block'
import { togetherAiBlockSchema } from '@typebot.io/together-ai-block/schemas' import { togetherAiBlockSchema } from '@typebot.io/together-ai-block/schemas'
import { nocodbBlock } from '@typebot.io/nocodb-block' import { nocodbBlock } from '@typebot.io/nocodb-block'
import { nocodbBlockSchema } from '@typebot.io/nocodb-block/schemas' import { nocodbBlockSchema } from '@typebot.io/nocodb-block/schemas'
import { segmentBlock } from '@typebot.io/segment-block'
import { segmentBlockSchema } from '@typebot.io/segment-block/schemas'
export const forgedBlockSchemas = { export const forgedBlockSchemas = {
[openAIBlock.id]: openAIBlockSchema, [openAIBlock.id]: openAIBlockSchema,
@ -34,4 +36,5 @@ export const forgedBlockSchemas = {
[togetherAiBlock.id]: togetherAiBlockSchema, [togetherAiBlock.id]: togetherAiBlockSchema,
[openRouterBlock.id]: openRouterBlockSchema, [openRouterBlock.id]: openRouterBlockSchema,
[nocodbBlock.id]: nocodbBlockSchema, [nocodbBlock.id]: nocodbBlockSchema,
[segmentBlock.id]: segmentBlockSchema,
} }

7009
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff