diff --git a/.changeset/ask-view-trigger.md b/.changeset/ask-view-trigger.md new file mode 100644 index 000000000..f4f9cd804 --- /dev/null +++ b/.changeset/ask-view-trigger.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Track which Ask AI entry point opened the chat in ask view analytics. diff --git a/packages/gitbook/src/components/AI/useAI.tsx b/packages/gitbook/src/components/AI/useAI.tsx index a3146c8ae..4cb8568aa 100644 --- a/packages/gitbook/src/components/AI/useAI.tsx +++ b/packages/gitbook/src/components/AI/useAI.tsx @@ -7,7 +7,7 @@ import type { CustomizationAIMode } from '@gitbook/api'; import type { GitBookAssistant } from '@gitbook/browser-types'; import { Icon, type IconName } from '@gitbook/icons'; -import { useAIChatController, useAIChatState } from '.'; +import { type AskAITrigger, useAIChatController, useAIChatState } from '.'; import { AIChatIcon, AISearchIcon, getAIChatName } from '../AIChat'; import { useIntegrationAssistants } from '../Integrations'; import { useSetSearchState } from '../Search/useSearch'; @@ -26,7 +26,8 @@ export type AIConfig = { }; }; -export type Assistant = Omit & { +export type Assistant = Omit & { + open: (query?: string, trigger?: AskAITrigger) => void; /** * Unique identifier for the assistant. Generated automatically using Crypto.randomUUID(). * @example '123e4567-e89b-12d3-a456-426614174000' @@ -102,8 +103,8 @@ export function useAI(): AIContext { className="size-text-lg" /> ), - open: (query?: string) => { - chatController.open(); + open: (query?: string, trigger?: AskAITrigger) => { + chatController.open(trigger); if (query) { chatController.postMessage({ message: query }); } diff --git a/packages/gitbook/src/components/AI/useAIChat.tsx b/packages/gitbook/src/components/AI/useAIChat.tsx index b3ce0c340..63be4351e 100644 --- a/packages/gitbook/src/components/AI/useAIChat.tsx +++ b/packages/gitbook/src/components/AI/useAIChat.tsx @@ -13,7 +13,7 @@ import { SiteInsightsDisplayContext, } from '@gitbook/api'; -import { getInsightsSession, useTrackEvent } from '../Insights'; +import { getInsightsSession, trackAskView, useTrackEvent } from '../Insights'; import { type UpdateSearchState, useSetSearchState } from '../Search'; import { addRecentSearchQuery } from '../Search/recent-queries'; import type { AnyAIControl } from './controls'; @@ -60,6 +60,8 @@ export type AIChatStatus = | 'error' | 'confirm'; +export type AskAITrigger = 'hover' | 'selection' | 'site' | 'page' | 'embed'; + export type AIChatState = { /** * If true, the chat is open. @@ -160,7 +162,7 @@ type AIChatEventListener = (input?: Omit) => void; export type AIChatController = { /** Open the dialog */ - open: () => void; + open: (trigger?: AskAITrigger) => void; /** Close the dialog */ close: () => void; /** Post a message to the session */ @@ -302,22 +304,28 @@ export function AIChatProvider(props: { const eventsRef = React.useRef>(new Map()); // Open AI chat and sync with search state - const onOpen = React.useCallback(() => { - setIntercomLauncherHidden(true); + const onOpen = React.useCallback( + (trigger?: AskAITrigger) => { + setIntercomLauncherHidden(true); + if (!renderMessageOptions?.asEmbeddable) { + trackAskView(trackEvent, trigger); + } - const { initialQuery } = globalState.getState(); - globalState.setState((state) => ({ ...state, opened: true })); + const { initialQuery } = globalState.getState(); + globalState.setState((state) => ({ ...state, opened: true })); - // Update search state to show ask mode with first message or current ask value - setSearchState((prev) => ({ - ask: prev?.ask ?? initialQuery ?? '', - query: prev?.query ?? null, - scope: prev?.scope ?? 'default', - open: false, // Close search popover when opening chat - })); + // Update search state to show ask mode with first message or current ask value + setSearchState((prev) => ({ + ask: prev?.ask ?? initialQuery ?? '', + query: prev?.query ?? null, + scope: prev?.scope ?? 'default', + open: false, // Close search popover when opening chat + })); - notify(eventsRef.current.get('open'), {}); - }, [setSearchState]); + notify(eventsRef.current.get('open'), {}); + }, + [setSearchState, trackEvent, renderMessageOptions?.asEmbeddable] + ); // Close AI chat and clear ask parameter const onClose = React.useCallback(() => { diff --git a/packages/gitbook/src/components/AIChat/AIChat.tsx b/packages/gitbook/src/components/AIChat/AIChat.tsx index 1c0e8163a..a3fe78819 100644 --- a/packages/gitbook/src/components/AIChat/AIChat.tsx +++ b/packages/gitbook/src/components/AIChat/AIChat.tsx @@ -24,7 +24,6 @@ import { EmbeddableFrameTitle, } from '../Embeddable/EmbeddableFrame'; import { useIsMounted, useNow } from '../hooks'; -import { useTrackEvent } from '../Insights'; import { Button } from '../primitives'; import { ScrollContainer } from '../primitives/ScrollContainer'; import { SideSheet } from '../primitives/SideSheet'; @@ -68,16 +67,6 @@ export function AIChat() { } ); - // Track the view of the AI chat - const trackEvent = useTrackEvent(); - React.useEffect(() => { - if (chat.opened) { - trackEvent({ - type: 'ask_view', - }); - } - }, [chat.opened, trackEvent]); - return ( } aria-label={tString(language, 'ai_chat_ask', assistant.label)} - onClick={() => assistant.open()} + onClick={() => assistant.open(undefined, 'site')} > {showLabel ? t(language, 'ask') : null} diff --git a/packages/gitbook/src/components/AIChat/AskAIParagraphButton/AskAIParagraphButton.tsx b/packages/gitbook/src/components/AIChat/AskAIParagraphButton/AskAIParagraphButton.tsx index 7f4034c2c..7e5a915a9 100644 --- a/packages/gitbook/src/components/AIChat/AskAIParagraphButton/AskAIParagraphButton.tsx +++ b/packages/gitbook/src/components/AIChat/AskAIParagraphButton/AskAIParagraphButton.tsx @@ -33,7 +33,7 @@ export function AskAIParagraphButton(props: { content: string; className?: Class id: `text-${fnv1a(text, { size: 32 })}`, content: text, }); - chatController.open(); + chatController.open('hover'); chatController.setDraft(tString(language, 'ai_chat_paragraph_draft')); chatController.focus(); }; diff --git a/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx b/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx index ba6db9854..ce0965fe9 100644 --- a/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx +++ b/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx @@ -84,7 +84,7 @@ export function AskAITextSelection() { id: `text-${fnv1a(content, { size: 32 })}`, content, }); - chatController.open(); + chatController.open('selection'); chatController.focus(); clear(); }; diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableAIChat.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableAIChat.tsx index c05f45dd1..cbfd68d31 100644 --- a/packages/gitbook/src/components/Embeddable/EmbeddableAIChat.tsx +++ b/packages/gitbook/src/components/Embeddable/EmbeddableAIChat.tsx @@ -4,7 +4,7 @@ import React from 'react'; import * as api from '@gitbook/api'; -import { useTrackEvent } from '../Insights'; +import { trackAskView, useTrackEvent } from '../Insights'; import { LinkContext } from '../primitives'; import { useIsVisible } from '../VisibilityContext'; import { @@ -60,15 +60,10 @@ export function EmbeddableAIChat(props: EmbeddableAIChatProps) { return; } - trackEvent( - { - type: 'ask_view', - }, - { - pageId: null, - displayContext: api.SiteInsightsDisplayContext.Embed, - } - ); + trackAskView(trackEvent, 'embed', { + pageId: null, + displayContext: api.SiteInsightsDisplayContext.Embed, + }); }, [trackEvent, isVisible]); const tabsRef = React.useRef(null); diff --git a/packages/gitbook/src/components/Insights/InsightsProvider.tsx b/packages/gitbook/src/components/Insights/InsightsProvider.tsx index 40789941d..7cdf3a0e9 100644 --- a/packages/gitbook/src/components/Insights/InsightsProvider.tsx +++ b/packages/gitbook/src/components/Insights/InsightsProvider.tsx @@ -6,6 +6,7 @@ import { useDebounceCallback, useEventCallback } from 'usehooks-ts'; import type * as api from '@gitbook/api'; import { OpenAPIOperationContextProvider } from '@gitbook/react-openapi'; +import type { AskAITrigger } from '../AI'; import { type CurrentContentContext, useCurrentContent } from '../hooks'; import { getSession } from './sessions'; import { type VisitorResponse, useVisitor } from './visitorId'; @@ -42,7 +43,7 @@ export type TrackEventInput( +export type TrackEventCallback = ( event: TrackEventInput, ctx?: InsightsEventPageContext, options?: InsightsEventOptions @@ -214,6 +215,16 @@ export function useTrackEvent(): TrackEventCallback { return React.useContext(InsightsContext); } +/** Track an ask view while the frontend uses an API type that predates its trigger field. */ +export function trackAskView( + trackEvent: TrackEventCallback, + trigger: AskAITrigger | undefined, + ctx?: InsightsEventPageContext +) { + // TODO: remove this cast once @gitbook/api includes SiteInsightsEventAskView.trigger. + trackEvent({ type: 'ask_view', trigger } as unknown as TrackEventInput<'ask_view'>, ctx); +} + /** * Post the events to the server. */ diff --git a/packages/gitbook/src/components/PageActions/PageActions.tsx b/packages/gitbook/src/components/PageActions/PageActions.tsx index df50737f0..691b7abba 100644 --- a/packages/gitbook/src/components/PageActions/PageActions.tsx +++ b/packages/gitbook/src/components/PageActions/PageActions.tsx @@ -67,7 +67,7 @@ export function ActionOpenAssistant(props: { href: page.href, }); } - assistant.open(); + assistant.open(undefined, 'page'); }} /> );