@@ -1,6 +1,6 @@
|
||||
import { LiteBadge } from './LiteBadge'
|
||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { injectCustomHeadCode, isNotEmpty } from '@typebot.io/lib'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
|
||||
import { ConversationContainer } from './ConversationContainer'
|
||||
import { setIsMobile } from '@/utils/isMobileSignal'
|
||||
@@ -89,8 +89,6 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
groupId: data.input.groupId,
|
||||
})
|
||||
if (data.logs) props.onNewLogs?.(data.logs)
|
||||
const customHeadCode = data.typebot.settings.metadata.customHeadCode
|
||||
if (customHeadCode) injectCustomHeadCode(customHeadCode)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
@@ -5,7 +5,5 @@ export const executeGoogleAnalyticsBlock = async (
|
||||
options: GoogleAnalyticsOptions
|
||||
) => {
|
||||
if (!options?.trackingId) return
|
||||
const { default: initGoogleAnalytics } = await import('@/lib/gtag')
|
||||
await initGoogleAnalytics(options.trackingId)
|
||||
sendGaEvent(options)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { trackPixelEvent } from '@/lib/pixel'
|
||||
import { isEmpty } from '@typebot.io/lib/utils'
|
||||
import type { PixelBlock } from '@typebot.io/schemas'
|
||||
|
||||
export const executePixel = async (options: PixelBlock['options']) => {
|
||||
if (isEmpty(options?.pixelId)) return
|
||||
trackPixelEvent(options)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isEmpty } from '@typebot.io/lib/utils'
|
||||
import type { GoogleAnalyticsOptions } from '@typebot.io/schemas'
|
||||
|
||||
declare const gtag: (
|
||||
@@ -11,7 +12,7 @@ declare const gtag: (
|
||||
}
|
||||
) => void
|
||||
|
||||
const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
export const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
const existingScript = document.getElementById('gtag')
|
||||
if (!existingScript) {
|
||||
@@ -37,11 +38,9 @@ const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
export const sendGaEvent = (options: GoogleAnalyticsOptions) => {
|
||||
if (!options) return
|
||||
gtag('event', options.action, {
|
||||
event_category: options.label?.length ? options.category : undefined,
|
||||
event_label: options.label?.length ? options.label : undefined,
|
||||
event_category: isEmpty(options.category) ? undefined : options.category,
|
||||
event_label: isEmpty(options.label) ? undefined : options.label,
|
||||
value: options.value as number,
|
||||
send_to: options.sendTo?.length ? options.sendTo : undefined,
|
||||
send_to: isEmpty(options.sendTo) ? undefined : options.sendTo,
|
||||
})
|
||||
}
|
||||
|
||||
export default initGoogleAnalytics
|
||||
|
||||
23
packages/embeds/js/src/lib/gtm.ts
Normal file
23
packages/embeds/js/src/lib/gtm.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const gtmHeadSnippet = (
|
||||
googleTagManagerId: string
|
||||
) => `<!-- Google Tag Manager -->
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','${googleTagManagerId}');
|
||||
<!-- End Google Tag Manager -->`
|
||||
|
||||
export const gtmBodyElement = (googleTagManagerId: string) => {
|
||||
if (document.getElementById('gtm-noscript')) return ''
|
||||
const noScriptElement = document.createElement('noscript')
|
||||
noScriptElement.id = 'gtm-noscript'
|
||||
const iframeElement = document.createElement('iframe')
|
||||
iframeElement.src = `https://www.googletagmanager.com/ns.html?id=${googleTagManagerId}`
|
||||
iframeElement.height = '0'
|
||||
iframeElement.width = '0'
|
||||
iframeElement.style.display = 'none'
|
||||
iframeElement.style.visibility = 'hidden'
|
||||
noScriptElement.appendChild(iframeElement)
|
||||
return noScriptElement
|
||||
}
|
||||
41
packages/embeds/js/src/lib/pixel.ts
Normal file
41
packages/embeds/js/src/lib/pixel.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { PixelBlock } from '@typebot.io/schemas'
|
||||
|
||||
declare const fbq: (
|
||||
arg0: string,
|
||||
arg1: string,
|
||||
arg2: Record<string, string> | undefined
|
||||
) => void
|
||||
|
||||
export const initPixel = (pixelId: string) => {
|
||||
const script = document.createElement('script')
|
||||
script.innerHTML = `!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', '${pixelId}');
|
||||
fbq('track', 'PageView');`
|
||||
document.head.appendChild(script)
|
||||
|
||||
const noscript = document.createElement('noscript')
|
||||
noscript.innerHTML = `<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1"/>`
|
||||
document.head.appendChild(noscript)
|
||||
}
|
||||
|
||||
export const trackPixelEvent = (options: PixelBlock['options']) => {
|
||||
if (!options.eventType) return
|
||||
const params = options.params?.length
|
||||
? options.params.reduce<Record<string, string>>((obj, param) => {
|
||||
if (!param.key || !param.value) return obj
|
||||
return { ...obj, [param.key]: param.value }
|
||||
}, {})
|
||||
: undefined
|
||||
if (options.eventType === 'Custom') {
|
||||
if (!options.name) return
|
||||
fbq('trackCustom', options.name, params)
|
||||
}
|
||||
fbq('track', options.eventType, params)
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import { executeScript } from '@/features/blocks/logic/script/executeScript'
|
||||
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
|
||||
import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait'
|
||||
import { executeWebhook } from '@/features/blocks/integrations/webhook/executeWebhook'
|
||||
import { executePixel } from '@/features/blocks/integrations/pixel/executePixel'
|
||||
import { ClientSideActionContext } from '@/types'
|
||||
import type { ChatReply, ReplyLog } from '@typebot.io/schemas'
|
||||
import { injectStartProps } from './injectStartProps'
|
||||
|
||||
export const executeClientSideAction = async (
|
||||
clientSideAction: NonNullable<ChatReply['clientSideActions']>[0],
|
||||
@@ -58,4 +60,10 @@ export const executeClientSideAction = async (
|
||||
const response = await executeWebhook(clientSideAction.webhookToExecute)
|
||||
return { replyToSend: response }
|
||||
}
|
||||
if ('startPropsToInject' in clientSideAction) {
|
||||
return injectStartProps(clientSideAction.startPropsToInject)
|
||||
}
|
||||
if ('pixel' in clientSideAction) {
|
||||
return executePixel(clientSideAction.pixel)
|
||||
}
|
||||
}
|
||||
|
||||
20
packages/embeds/js/src/utils/injectStartProps.ts
Normal file
20
packages/embeds/js/src/utils/injectStartProps.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable solid/reactivity */
|
||||
import { initGoogleAnalytics } from '@/lib/gtag'
|
||||
import { gtmBodyElement } from '@/lib/gtm'
|
||||
import { initPixel } from '@/lib/pixel'
|
||||
import { injectCustomHeadCode, isNotEmpty } from '@typebot.io/lib/utils'
|
||||
import { StartPropsToInject } from '@typebot.io/schemas'
|
||||
|
||||
export const injectStartProps = async (
|
||||
startPropsToInject: StartPropsToInject
|
||||
) => {
|
||||
const customHeadCode = startPropsToInject.customHeadCode
|
||||
if (isNotEmpty(customHeadCode)) injectCustomHeadCode(customHeadCode)
|
||||
const gtmId = startPropsToInject.gtmId
|
||||
if (isNotEmpty(gtmId)) document.body.prepend(gtmBodyElement(gtmId))
|
||||
const googleAnalyticsId = startPropsToInject.googleAnalyticsId
|
||||
if (isNotEmpty(googleAnalyticsId))
|
||||
await initGoogleAnalytics(googleAnalyticsId)
|
||||
const pixelId = startPropsToInject.pixelId
|
||||
if (isNotEmpty(pixelId)) initPixel(pixelId)
|
||||
}
|
||||
@@ -8,4 +8,5 @@ export enum IntegrationBlockType {
|
||||
MAKE_COM = 'Make.com',
|
||||
PABBLY_CONNECT = 'Pabbly',
|
||||
CHATWOOT = 'Chatwoot',
|
||||
PIXEL = 'Pixel',
|
||||
}
|
||||
|
||||
@@ -7,3 +7,5 @@ export * from './pabblyConnect'
|
||||
export * from './sendEmail'
|
||||
export * from './webhook'
|
||||
export * from './zapier'
|
||||
export * from './pixel/schemas'
|
||||
export * from './pixel/constants'
|
||||
|
||||
134
packages/schemas/features/blocks/integrations/pixel/constants.ts
Normal file
134
packages/schemas/features/blocks/integrations/pixel/constants.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
// Reference: https://developers.facebook.com/docs/meta-pixel/reference#standard-events
|
||||
|
||||
export const pixelEventTypes = [
|
||||
'Lead',
|
||||
'Contact',
|
||||
'CompleteRegistration',
|
||||
'Schedule',
|
||||
'SubmitApplication',
|
||||
'ViewContent',
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CustomizeProduct',
|
||||
'Donate',
|
||||
'FindLocation',
|
||||
'InitiateCheckout',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'StartTrial',
|
||||
'Subscribe',
|
||||
] as const
|
||||
|
||||
export const allEventTypes = ['Custom', ...pixelEventTypes] as const
|
||||
|
||||
export const pixelObjectProperties: {
|
||||
key: string
|
||||
type: 'text' | 'code'
|
||||
associatedEvents: (typeof pixelEventTypes)[number][]
|
||||
}[] = [
|
||||
{
|
||||
key: 'content_category',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToWishlist',
|
||||
'InitiateCheckout',
|
||||
'Lead',
|
||||
'Search',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_ids',
|
||||
type: 'code',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'InitiateCheckout',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_name',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CompleteRegistration',
|
||||
'Lead',
|
||||
'Purchase',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contents',
|
||||
type: 'code',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'InitiateCheckout',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'currency',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CompleteRegistration',
|
||||
'InitiateCheckout',
|
||||
'Lead',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'StartTrial',
|
||||
'Subscribe',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'num_items',
|
||||
type: 'text',
|
||||
associatedEvents: ['InitiateCheckout', 'Purchase'],
|
||||
},
|
||||
{
|
||||
key: 'predicted_ltv',
|
||||
type: 'text',
|
||||
associatedEvents: ['StartTrial', 'Subscribe'],
|
||||
},
|
||||
{
|
||||
key: 'search_string',
|
||||
type: 'text',
|
||||
associatedEvents: ['Search'],
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
type: 'text',
|
||||
associatedEvents: ['CompleteRegistration'],
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CompleteRegistration',
|
||||
'InitiateCheckout',
|
||||
'Lead',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'StartTrial',
|
||||
'Subscribe',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
import { z } from 'zod'
|
||||
import { pixelEventTypes } from './constants'
|
||||
import { blockBaseSchema } from '../../baseSchemas'
|
||||
import { IntegrationBlockType } from '../enums'
|
||||
|
||||
const basePixelOptionSchema = z.object({
|
||||
pixelId: z.string().optional(),
|
||||
params: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
key: z.string().optional(),
|
||||
value: z.any().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const initialPixelOptionSchema = basePixelOptionSchema.merge(
|
||||
z.object({
|
||||
eventType: z.undefined(),
|
||||
})
|
||||
)
|
||||
|
||||
const standardPixelEventOptionSchema = basePixelOptionSchema.merge(
|
||||
z.object({
|
||||
eventType: z.enum(pixelEventTypes),
|
||||
})
|
||||
)
|
||||
|
||||
const customPixelOptionSchema = basePixelOptionSchema.merge(
|
||||
z.object({
|
||||
eventType: z.enum(['Custom']),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export const pixelOptionsSchema = z.discriminatedUnion('eventType', [
|
||||
initialPixelOptionSchema,
|
||||
standardPixelEventOptionSchema,
|
||||
customPixelOptionSchema,
|
||||
])
|
||||
|
||||
export const pixelBlockSchema = blockBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.enum([IntegrationBlockType.PIXEL]),
|
||||
options: pixelOptionsSchema,
|
||||
})
|
||||
)
|
||||
|
||||
export type PixelBlock = z.infer<typeof pixelBlockSchema>
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ZodDiscriminatedUnionOption, z } from 'zod'
|
||||
import { z } from 'zod'
|
||||
import { BubbleBlockType } from './bubbles/enums'
|
||||
import { choiceInputSchema } from './inputs/choice'
|
||||
import { InputBlockType } from './inputs/enums'
|
||||
import { IntegrationBlockType } from './integrations/enums'
|
||||
import { ConditionBlock, conditionBlockSchema } from './logic/condition'
|
||||
import { conditionBlockSchema } from './logic/condition'
|
||||
import { LogicBlockType } from './logic/enums'
|
||||
import { blockBaseSchema } from './baseSchemas'
|
||||
import { startBlockSchema } from './start/schemas'
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
googleSheetsBlockSchema,
|
||||
makeComBlockSchema,
|
||||
pabblyConnectBlockSchema,
|
||||
pixelBlockSchema,
|
||||
sendEmailBlockSchema,
|
||||
webhookBlockSchema,
|
||||
zapierBlockSchema,
|
||||
@@ -125,6 +126,7 @@ export const blockSchema = z.discriminatedUnion('type', [
|
||||
sendEmailBlockSchema,
|
||||
webhookBlockSchema,
|
||||
zapierBlockSchema,
|
||||
pixelBlockSchema,
|
||||
])
|
||||
|
||||
export type Block = z.infer<typeof blockSchema>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from 'zod'
|
||||
import {
|
||||
googleAnalyticsOptionsSchema,
|
||||
paymentInputRuntimeOptionsSchema,
|
||||
pixelOptionsSchema,
|
||||
redirectOptionsSchema,
|
||||
} from './blocks'
|
||||
import { publicTypebotSchema } from './publicTypebot'
|
||||
@@ -197,6 +198,13 @@ export const sendMessageInputSchema = z.object({
|
||||
|
||||
const runtimeOptionsSchema = paymentInputRuntimeOptionsSchema.optional()
|
||||
|
||||
const startPropsToInjectSchema = z.object({
|
||||
googleAnalyticsId: z.string().optional(),
|
||||
pixelId: z.string().optional(),
|
||||
gtmId: z.string().optional(),
|
||||
customHeadCode: z.string().optional(),
|
||||
})
|
||||
|
||||
const clientSideActionSchema = z
|
||||
.object({
|
||||
lastBubbleBlockId: z.string().optional(),
|
||||
@@ -247,6 +255,16 @@ const clientSideActionSchema = z
|
||||
webhookToExecute: executableWebhookSchema,
|
||||
})
|
||||
)
|
||||
.or(
|
||||
z.object({
|
||||
startPropsToInject: startPropsToInjectSchema,
|
||||
})
|
||||
)
|
||||
.or(
|
||||
z.object({
|
||||
pixel: pixelOptionsSchema,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export const chatReplySchema = z.object({
|
||||
@@ -282,3 +300,4 @@ export type StartParams = z.infer<typeof startParamsSchema>
|
||||
export type RuntimeOptions = z.infer<typeof runtimeOptionsSchema>
|
||||
export type StartTypebot = z.infer<typeof startTypebotSchema>
|
||||
export type ReplyLog = z.infer<typeof replyLogSchema>
|
||||
export type StartPropsToInject = z.infer<typeof startPropsToInjectSchema>
|
||||
|
||||
Reference in New Issue
Block a user