diff --git a/packages/compass-assistant/src/components/assistant-chat.tsx b/packages/compass-assistant/src/components/assistant-chat.tsx index b3fa99f467e..a5f8aad9146 100644 --- a/packages/compass-assistant/src/components/assistant-chat.tsx +++ b/packages/compass-assistant/src/components/assistant-chat.tsx @@ -326,7 +326,7 @@ export const AssistantChat: React.FunctionComponent = ({ void ensureOptInAndSend?.(undefined, {}, () => {}); } }, - [ensureOptInAndSend, setMessages] + [ensureOptInAndSend, setMessages, track] ); return ( diff --git a/packages/compass-components/src/components/compass-components-provider.tsx b/packages/compass-components/src/components/compass-components-provider.tsx index b46c32cc606..8da0abfaf86 100644 --- a/packages/compass-components/src/components/compass-components-provider.tsx +++ b/packages/compass-components/src/components/compass-components-provider.tsx @@ -61,6 +61,9 @@ type CompassComponentsProviderProps = { itemGroup: ContextMenuItemGroup, item: ContextMenuItem ) => void; +} & { + onDrawerSectionOpen?: (drawerSectionId: string) => void; + onDrawerSectionHide?: (drawerSectionId: string) => void; } & React.ComponentProps; const darkModeMediaQuery = (() => { @@ -119,6 +122,8 @@ export const CompassComponentsProvider = ({ onNextGuideCueGroup, onContextMenuOpen, onContextMenuItemClick, + onDrawerSectionOpen, + onDrawerSectionHide, utmSource, utmMedium, stackedElementsZIndex, @@ -149,7 +154,10 @@ export const CompassComponentsProvider = ({ darkMode={darkMode} popoverPortalContainer={popoverPortalContainer} > - + + {[1, 2, 3].map((n, idx) => { return ( @@ -85,33 +92,62 @@ describe('DrawerSection', function () { expect(screen.getByText('This is section 1')).to.be.visible; }); + expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-1'); + onDrawerSectionOpenSpy.resetHistory(); + userEvent.click(screen.getByRole('button', { name: 'Section 2' })); await waitFor(() => { expect(screen.getByText('This is section 2')).to.be.visible; }); + expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-1'); + expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-2'); + onDrawerSectionOpenSpy.resetHistory(); + onDrawerSectionHideSpy.resetHistory(); + userEvent.click(screen.getByRole('button', { name: 'Section 3' })); await waitFor(() => { expect(screen.getByText('This is section 3')).to.be.visible; }); + expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-2'); + expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-3'); + onDrawerSectionOpenSpy.resetHistory(); + onDrawerSectionHideSpy.resetHistory(); + userEvent.click(screen.getByRole('button', { name: 'Section 1' })); await waitFor(() => { expect(screen.getByText('This is section 1')).to.be.visible; }); + expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-3'); + expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-1'); + onDrawerSectionOpenSpy.resetHistory(); + onDrawerSectionHideSpy.resetHistory(); + userEvent.click(screen.getByRole('button', { name: 'Close drawer' })); await waitFor(() => { expect(screen.queryByText('This is section 1')).not.to.exist; expect(screen.queryByText('This is section 2')).not.to.exist; expect(screen.queryByText('This is section 3')).not.to.exist; }); + + expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-1'); + expect(onDrawerSectionOpenSpy).to.not.have.been.called; + onDrawerSectionOpenSpy.resetHistory(); + onDrawerSectionHideSpy.resetHistory(); }); it('closes drawer when opened section is removed from toolbar data', async function () { + const onDrawerSectionOpenSpy = sinon.spy(); + const onDrawerSectionHideSpy = sinon.spy(); + // Render two sections, auto-open first one const { rerender } = render( - + + { const { isDrawerOpen } = useDrawerState(); const { openDrawer, closeDrawer } = useDrawerActions(); @@ -188,7 +242,10 @@ describe('DrawerSection', function () { ); }; render( - + { @@ -221,12 +283,24 @@ describe('DrawerSection', function () { expect(screen.getByText('This is the controlled section')).to.be.visible; }); + expect(onDrawerSectionHideSpy).to.not.have.been.called; + expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith( + 'controlled-section' + ); + onDrawerSectionOpenSpy.resetHistory(); + onDrawerSectionHideSpy.resetHistory(); + // Close the drawer userEvent.click(screen.getByRole('button', { name: 'Hook Close drawer' })); await waitFor(() => { expect(screen.getByTestId('drawer-state')).to.have.text('closed'); expect(screen.queryByText('This is the controlled section')).not.to.exist; }); + + expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith( + 'controlled-section' + ); + expect(onDrawerSectionOpenSpy).to.not.have.been.called; }); it('renders guide cue when passed in props', async function () { diff --git a/packages/compass-components/src/components/drawer-portal.tsx b/packages/compass-components/src/components/drawer-portal.tsx index 57c7a048acd..b6738f0b240 100644 --- a/packages/compass-components/src/components/drawer-portal.tsx +++ b/packages/compass-components/src/components/drawer-portal.tsx @@ -62,6 +62,16 @@ const DrawerOpenStateContext = const DrawerSetOpenStateContext = React.createContext(() => {}); +type DrawerCurrentTabStateContextValue = string | null; + +type DrawerSetCurrentTabContextValue = (currentTab: string | null) => void; + +const DrawerCurrentTabStateContext = + React.createContext(null); + +const DrawerSetCurrentTabContext = + React.createContext(() => {}); + const DrawerActionsContext = React.createContext({ current: { openDrawer: () => undefined, @@ -104,12 +114,16 @@ const DrawerActionsContext = React.createContext({ * ) * } */ -export const DrawerContentProvider: React.FunctionComponent = ({ - children, -}) => { +export const DrawerContentProvider: React.FunctionComponent<{ + onDrawerSectionOpen?: (drawerSectionId: string) => void; + onDrawerSectionHide?: (drawerSectionId: string) => void; + children?: React.ReactNode; +}> = ({ onDrawerSectionOpen, onDrawerSectionHide, children }) => { const [drawerState, setDrawerState] = useState([]); const [drawerOpenState, setDrawerOpenState] = useState(false); + const [drawerCurrentTab, setDrawerCurrentTab] = + useState(null); const drawerActions = useRef({ openDrawer: () => undefined, closeDrawer: () => undefined, @@ -135,13 +149,36 @@ export const DrawerContentProvider: React.FunctionComponent = ({ }, }); + const prevDrawerCurrentTabRef = React.useRef(null); + + useEffect(() => { + if (drawerCurrentTab === prevDrawerCurrentTabRef.current) { + // ignore unless it changed + return; + } + + if (drawerCurrentTab) { + onDrawerSectionOpen?.(drawerCurrentTab); + } + + if (prevDrawerCurrentTabRef.current) { + onDrawerSectionHide?.(prevDrawerCurrentTabRef.current); + } + + prevDrawerCurrentTabRef.current = drawerCurrentTab; + }, [drawerCurrentTab, onDrawerSectionHide, onDrawerSectionOpen]); + return ( - - {children} - + + + + {children} + + + @@ -152,11 +189,21 @@ const DrawerContextGrabber: React.FunctionComponent = ({ children }) => { const drawerToolbarContext = useDrawerToolbarContext(); const actions = useContext(DrawerActionsContext); const openStateSetter = useContext(DrawerSetOpenStateContext); + const currentTabSetter = useContext(DrawerSetCurrentTabContext); actions.current.openDrawer = drawerToolbarContext.openDrawer; actions.current.closeDrawer = drawerToolbarContext.closeDrawer; + useEffect(() => { openStateSetter(drawerToolbarContext.isDrawerOpen); }, [drawerToolbarContext.isDrawerOpen, openStateSetter]); + + useEffect(() => { + const currentTab = + drawerToolbarContext.getActiveDrawerContent()?.id ?? null; + + currentTabSetter(currentTab); + }, [drawerToolbarContext, currentTabSetter]); + return <>{children}; }; diff --git a/packages/compass-telemetry/src/telemetry-events.ts b/packages/compass-telemetry/src/telemetry-events.ts index dcfe8cf256e..16fe32e09fa 100644 --- a/packages/compass-telemetry/src/telemetry-events.ts +++ b/packages/compass-telemetry/src/telemetry-events.ts @@ -1458,6 +1458,34 @@ type IndexDroppedEvent = ConnectionScopedEvent<{ }; }>; +/** + * This event is fired when user opens a drawer section. Either by switching + * to it via the drawer toolbar or by opening the drawer and the first tab is + * this drawer section. + * + * @category Gen AI + */ +type DrawerSectionOpenedEvent = CommonEvent<{ + name: 'Drawer Section Opened'; + payload: { + sectionId: string; + }; +}>; + +/** + * This event is fired when user closes a drawer section. Either by switching + * to another tab via the drawer toolbar or by closing the drawer when the + * active tab is this drawer section. + * + * @category Gen AI + */ +type DrawerSectionClosedEvent = CommonEvent<{ + name: 'Drawer Section Closed'; + payload: { + sectionId: string; + }; +}>; + /** * This event is fired when user enters a prompt in the assistant chat * and hits "enter". @@ -3174,6 +3202,8 @@ export type TelemetryEvent = | DocumentDeletedEvent | DocumentInsertedEvent | DocumentUpdatedEvent + | DrawerSectionOpenedEvent + | DrawerSectionClosedEvent | EditorTypeChangedEvent | ErrorFetchingAttributesEvent | ExplainPlanExecutedEvent diff --git a/packages/compass-web/src/entrypoint.tsx b/packages/compass-web/src/entrypoint.tsx index 68cd0522b57..e43725cc09b 100644 --- a/packages/compass-web/src/entrypoint.tsx +++ b/packages/compass-web/src/entrypoint.tsx @@ -440,6 +440,16 @@ const CompassWeb = ({ item_label: item.label, }); }} + onDrawerSectionOpen={(drawerSectionId) => { + onTrackRef.current?.('Drawer Section Opened', { + sectionId: drawerSectionId, + }); + }} + onDrawerSectionHide={(drawerSectionId) => { + onTrackRef.current?.('Drawer Section Closed', { + sectionId: drawerSectionId, + }); + }} onSignalMount={(id) => { onTrackRef.current?.('Signal Shown', { id }); }} diff --git a/packages/compass/src/app/components/home.tsx b/packages/compass/src/app/components/home.tsx index 40133d34a95..44666cc7810 100644 --- a/packages/compass/src/app/components/home.tsx +++ b/packages/compass/src/app/components/home.tsx @@ -210,6 +210,12 @@ export default function ThemedHome( item_label: item.label, }); }} + onDrawerSectionOpen={(drawerSectionId) => { + track('Drawer Section Opened', { sectionId: drawerSectionId }); + }} + onDrawerSectionHide={(drawerSectionId) => { + track('Drawer Section Closed', { sectionId: drawerSectionId }); + }} utmSource="compass" utmMedium="product" onSignalMount={(id) => {