Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin-hubtype-analytics: event flow #2828

6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/botonic-core/src/models/legacy-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export interface Session {
_shadowing?: boolean
external?: any
zendesk_ticket_id?: string
flow_thread_id?: string
}

export type InputMatcher = (input: Input) => boolean
Expand Down
78 changes: 54 additions & 24 deletions packages/botonic-plugin-flow-builder/src/action/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { FlowBuilderApi } from '../api'
import { FlowContent, FlowHandoff } from '../content-fields'
import { HtNodeWithContent } from '../content-fields/hubtype-fields'
import { getFlowBuilderPlugin } from '../helpers'
import { EventName, trackEvent } from '../tracking'
import BotonicPluginFlowBuilder from '../index'
import { EventAction, getEventArgs, trackEvent } from '../tracking'
import { createNodeFromKnowledgeBase } from './knowledge-bases'

export type FlowBuilderActionProps = {
Expand All @@ -18,15 +19,7 @@ export class FlowBuilderAction extends React.Component<FlowBuilderActionProps> {
static async botonicInit(
request: ActionRequest
): Promise<FlowBuilderActionProps> {
const flowBuilderPlugin = getFlowBuilderPlugin(request.plugins)
const locale = flowBuilderPlugin.getLocale(request.session)

const targetNode = await getTargetNode(flowBuilderPlugin.cmsApi, request)

const contents = await flowBuilderPlugin.getContentsByNode(
targetNode,
locale
)
const contents = await getContents(request)

const handoffContent = contents.find(
content => content instanceof FlowHandoff
Expand Down Expand Up @@ -55,26 +48,64 @@ export class FlowBuilderMultichannelAction extends FlowBuilderAction {
}
}

async function getTargetNode(cmsApi: FlowBuilderApi, request: ActionRequest) {
async function getContents(request: ActionRequest): Promise<FlowContent[]> {
const flowBuilderPlugin = getFlowBuilderPlugin(request.plugins)
const cmsApi = flowBuilderPlugin.cmsApi
const locale = flowBuilderPlugin.getLocale(request.session)
const resolvedLocale = flowBuilderPlugin.cmsApi.getResolvedLocale(locale)
const context = {
cmsApi,
flowBuilderPlugin,
request,
resolvedLocale,
}

if (request.session.is_first_interaction) {
const startNode = cmsApi.getStartNode()
await trackEvent(request, EventName.botStart)
return startNode
return await flowBuilderPlugin.getStartContents(resolvedLocale)
}

if (request.input.payload) {
return await getContentsByPayload(context)
}
const contentId = request.input.payload

const targetNode = contentId
? cmsApi.getNodeById<HtNodeWithContent>(contentId)
return await getContentsByFallback(context)
}

interface FlowBuilderContext {
cmsApi: FlowBuilderApi
flowBuilderPlugin: BotonicPluginFlowBuilder
request: ActionRequest
resolvedLocale: string
}

async function getContentsByPayload({
cmsApi,
flowBuilderPlugin,
request,
resolvedLocale,
}: FlowBuilderContext): Promise<FlowContent[]> {
const targetNode = request.input.payload
? cmsApi.getNodeById<HtNodeWithContent>(request.input.payload)
: undefined

if (targetNode) {
const eventArgs = {
faq_name: targetNode.code,
}
await trackEvent(request, EventName.botFaq, eventArgs)
return targetNode
const eventArgs = getEventArgs(request, targetNode)
await trackEvent(request, EventAction.flowNode, eventArgs)
return await flowBuilderPlugin.getContentsByNode(targetNode, resolvedLocale)
}
return await getFallbackNode(cmsApi, request)
return []
}

async function getContentsByFallback({
cmsApi,
flowBuilderPlugin,
request,
resolvedLocale,
}: FlowBuilderContext): Promise<FlowContent[]> {
const fallbackNode = await getFallbackNode(cmsApi, request)
const eventArgs = getEventArgs(request, fallbackNode)
await trackEvent(request, EventAction.flowNode, eventArgs)
return await flowBuilderPlugin.getContentsByNode(fallbackNode, resolvedLocale)
}

async function getFallbackNode(cmsApi: FlowBuilderApi, request: ActionRequest) {
Expand All @@ -95,6 +126,5 @@ async function getFallbackNode(cmsApi: FlowBuilderApi, request: ActionRequest) {
const fallbackNode = cmsApi.getFallbackNode(isFirstFallbackOption)
request.session.user.extra_data.isFirstFallbackOption = !isFirstFallbackOption

await trackEvent(request, EventName.fallback)
return fallbackNode
}
5 changes: 5 additions & 0 deletions packages/botonic-plugin-flow-builder/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ export class FlowBuilderApi {
}
}

getFlowName(flowId: string): string {
const flow = this.flow.flows.find(flow => flow.id === flowId)
return flow ? flow.name : ''
}

getResolvedLocale(locale: string): string {
if (this.flow.locales.find(flowLocale => flowLocale === locale)) {
return locale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { ActionRequest, WebchatSettings } from '@botonic/react'
import React from 'react'

import { FlowBuilderApi } from '../api'
import { getQueueAvailability } from '../functions/conditional-queue-status'
import { EventName, trackEvent } from '../tracking'
import {
AvailabilityData,
getQueueAvailability,
} from '../functions/conditional-queue-status'
import { EventAction, trackEvent } from '../tracking'
import { ContentFieldsBase } from './content-fields-base'
import { HtHandoffNode, HtPayloadNode, HtQueueLocale } from './hubtype-fields'
import { HtHandoffNode, HtQueueLocale } from './hubtype-fields'

export class FlowHandoff extends ContentFieldsBase {
public code: string
Expand All @@ -22,33 +25,20 @@ export class FlowHandoff extends ContentFieldsBase {
const newHandoff = new FlowHandoff(cmsHandoff.id)
newHandoff.code = cmsHandoff.code
newHandoff.queue = this.getQueueByLocale(locale, cmsHandoff.content.queue)
newHandoff.onFinishPayload = this.getOnFinishPayload(
cmsHandoff,
locale,
cmsApi
)
newHandoff.onFinishPayload = this.getOnFinishPayload(cmsHandoff, cmsApi)
newHandoff.handoffAutoAssign = cmsHandoff.content.has_auto_assign

return newHandoff
}

private static getOnFinishPayload(
cmsHandoff: HtHandoffNode,
locale: string,
cmsApi: FlowBuilderApi
): string | undefined {
if (cmsHandoff.target?.id) {
return cmsApi.getPayload(cmsHandoff.target)
}

// OLD PAYLOAD
const payloadId = cmsHandoff.content.payload.find(
payload => payload.locale === locale
)?.id
if (payloadId) {
return cmsApi.getNodeById<HtPayloadNode>(payloadId).content.payload
}

return undefined
}

Expand All @@ -61,20 +51,33 @@ export class FlowHandoff extends ContentFieldsBase {
}
if (this.queue) {
const availabilityData = await getQueueAvailability(this.queue.id)
const eventArgs = {
queue_open: availabilityData.open,
queue_id: this.queue.id,
available_agents: availabilityData.available_agents > 0,
threshold_reached:
availabilityData.availability_threshold_waiting_cases > 0,
}
await trackEvent(request, EventName.handoffSuccess, eventArgs)
await this.trackHandoff(availabilityData, request, this.queue)

handOffBuilder.withQueue(this.queue.id)
await handOffBuilder.handOff()
}
}

private async trackHandoff(
availabilityData: AvailabilityData,
request: ActionRequest,
queue: HtQueueLocale
) {
const eventArgs = {
queueId: queue.id,
queueName: queue.name,
//caseId: 'handoffCaseIdTest', // de on surt?
isQueueOpen: availabilityData.open,
isAvailableAgent: availabilityData.available_agents > 0,
isThresholdReached:
availabilityData.availability_threshold_waiting_cases > 0,
}
const eventName = availabilityData.open
? EventAction.handoffSuccess
: EventAction.handoffFail
await trackEvent(request, eventName, eventArgs)
}

toBotonic(): JSX.Element {
return <WebchatSettings enableUserInput={true} />
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ActionRequest } from '@botonic/react'
import axios from 'axios'

import { EventAction, trackEvent } from '../tracking'

const HUBTYPE_API_URL = process.env.HUBTYPE_API_URL || 'https://api.hubtype.com'

type ConditionalQueueStatusArgs = {
Expand All @@ -17,10 +19,16 @@ enum QueueStatusResult {
}

export async function conditionalQueueStatus({
request,
queue_id,
queue_name,
check_available_agents,
}: ConditionalQueueStatusArgs): Promise<QueueStatusResult> {
const data = await getQueueAvailability(queue_id, check_available_agents)
await trackEvent(request, EventAction.handoffOption, {
queueId: queue_id,
queueName: queue_name,
})
if (check_available_agents && data.open && data.available_agents === 0) {
return QueueStatusResult.OPEN_WITHOUT_AGENTS
}
Expand Down
21 changes: 19 additions & 2 deletions packages/botonic-plugin-flow-builder/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Plugin, PluginPreRequest, Session } from '@botonic/core'
import { ActionRequest } from '@botonic/react'
import { v4 as uuid } from 'uuid'

import { FlowBuilderApi } from './api'
import {
Expand Down Expand Up @@ -29,6 +30,7 @@ import {
HtNodeWithContentType,
} from './content-fields/hubtype-fields'
import { DEFAULT_FUNCTIONS } from './functions'
import { EventAction, getEventArgs, trackEvent } from './tracking'
import {
BotonicPluginFlowBuilderOptions,
FlowBuilderJSONVersion,
Expand All @@ -48,7 +50,7 @@ export default class BotonicPluginFlowBuilder implements Plugin {
public getLocale: (session: Session) => string
public trackEvent?: (
request: ActionRequest,
eventName: string,
eventAction: string,
args?: Record<string, any>
) => Promise<void>
public getKnowledgeBaseResponse?: (
Expand Down Expand Up @@ -140,8 +142,19 @@ export default class BotonicPluginFlowBuilder implements Plugin {
}

async getStartContents(locale: string): Promise<FlowContent[]> {
const resolvedLocale = this.cmsApi.getResolvedLocale(locale)
const startNode = this.cmsApi.getStartNode()
return await this.getContentsByNode(startNode, locale)
this.currentRequest.session.flow_thread_id = uuid()
const eventArgs = getEventArgs(
this.currentRequest as unknown as ActionRequest,
startNode
)
await trackEvent(
this.currentRequest as unknown as ActionRequest,
EventAction.flowNode,
eventArgs
)
return await this.getContentsByNode(startNode, resolvedLocale)
}

async getContentsByNode(
Expand Down Expand Up @@ -257,6 +270,10 @@ export default class BotonicPluginFlowBuilder implements Plugin {
const payloadParams = JSON.parse(payload.split(SEPARATOR)[1] || '{}')
return payloadParams
}

getFlowName(flowId: string): string {
return this.cmsApi.getFlowName(flowId)
}
}

export * from './action'
Expand Down
45 changes: 31 additions & 14 deletions packages/botonic-plugin-flow-builder/src/tracking.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
import { ActionRequest } from '@botonic/react'

import { HtNodeWithContent } from './content-fields/hubtype-fields'
import { getFlowBuilderPlugin } from './helpers'

export async function trackEvent(
request: ActionRequest,
eventName: EventName,
eventAction: EventAction,
args?: Record<string, any>
): Promise<void> {
const flowBuilderPlugin = getFlowBuilderPlugin(request.plugins)
if (flowBuilderPlugin.trackEvent) {
await flowBuilderPlugin.trackEvent(request, eventName, args)
await flowBuilderPlugin.trackEvent(request, eventAction, args)
}
return
}

export enum EventName {
botAgentRating = 'bot_agent_rating',
botChannelRating = 'bot_channel_rating',
botFaqUseful = 'bot_faq_useful',
botRating = 'bot_rating',
botFaq = 'bot_faq',
botStart = 'bot_start',
botOpen = 'bot_open',
botAiModel = 'bot_ai_model',
botAiKnowledgeBase = 'bot_ai_knowledge_base',
botKeywordsModel = 'bot_keywords_model',
fallback = 'fallback',
export function getEventArgs(request: ActionRequest, node: HtNodeWithContent) {
const flowBuilderPlugin = getFlowBuilderPlugin(request.plugins)
return {
flowThreadId: request.session.flow_thread_id,
flowId: node.flow_id,
flowName: flowBuilderPlugin.getFlowName(node.flow_id),
flowNodeId: node.id,
flowNodeContentId: node.code,
flowNodeIsMeaningful: undefined, //node?.isMeaningful,
}
}

export enum EventAction {
flowNode = 'flow_node',
handoffOption = 'handoff_option',
handoffSuccess = 'handoff_success',
handoffFail = 'handoff_fail',
}

// export enum EventName {
// feedback = 'feedback',
// flow = 'botevent',

// botOpen = 'bot_open',
// botAiModel = 'bot_ai_model',
// botAiKnowledgeBase = 'bot_ai_knowledge_base',
// botKeywordsModel = 'bot_keywords_model',
// fallback = 'fallback',
// handoffOption = 'handoff_option',
// handoffSuccess = 'handoff_success',
// handoffFail = 'handoff_fail',
// }
Loading
Loading