Skip to content

Commit 65c558d

Browse files
authored
feat(compass-assistant): track opening and closing the drawer sections COMPASS-9884 (#7372)
* track opening and closing the assistant * rather make the drawer section tracking part of the drawer for all drawer sections * DrawerPortal unit tests * redundant checks
1 parent 20d740f commit 65c558d

File tree

7 files changed

+187
-12
lines changed

7 files changed

+187
-12
lines changed

packages/compass-assistant/src/components/assistant-chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export const AssistantChat: React.FunctionComponent<AssistantChatProps> = ({
359359
void ensureOptInAndSend?.(undefined, {}, () => {});
360360
}
361361
},
362-
[ensureOptInAndSend, setMessages]
362+
[ensureOptInAndSend, setMessages, track]
363363
);
364364

365365
return (

packages/compass-components/src/components/compass-components-provider.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ type CompassComponentsProviderProps = {
6161
itemGroup: ContextMenuItemGroup,
6262
item: ContextMenuItem
6363
) => void;
64+
} & {
65+
onDrawerSectionOpen?: (drawerSectionId: string) => void;
66+
onDrawerSectionHide?: (drawerSectionId: string) => void;
6467
} & React.ComponentProps<typeof SignalHooksProvider>;
6568

6669
const darkModeMediaQuery = (() => {
@@ -119,6 +122,8 @@ export const CompassComponentsProvider = ({
119122
onNextGuideCueGroup,
120123
onContextMenuOpen,
121124
onContextMenuItemClick,
125+
onDrawerSectionOpen,
126+
onDrawerSectionHide,
122127
utmSource,
123128
utmMedium,
124129
stackedElementsZIndex,
@@ -149,7 +154,10 @@ export const CompassComponentsProvider = ({
149154
darkMode={darkMode}
150155
popoverPortalContainer={popoverPortalContainer}
151156
>
152-
<DrawerContentProvider>
157+
<DrawerContentProvider
158+
onDrawerSectionOpen={onDrawerSectionOpen}
159+
onDrawerSectionHide={onDrawerSectionHide}
160+
>
153161
<StackedComponentProvider zIndex={stackedElementsZIndex}>
154162
<RequiredURLSearchParamsProvider
155163
utmSource={utmSource}

packages/compass-components/src/components/drawer-portal.spec.tsx

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
useDrawerActions,
1414
} from './drawer-portal';
1515
import { expect } from 'chai';
16+
import sinon from 'sinon';
1617

1718
describe('DrawerSection', function () {
1819
it('renders DrawerSection in the portal and updates the content when it updates', async function () {
@@ -60,8 +61,14 @@ describe('DrawerSection', function () {
6061
const icons = ['ArrowDown', 'CaretDown', 'ChevronDown'] as const;
6162

6263
it('switches drawer content when selecting a different section in the toolbar', async function () {
64+
const onDrawerSectionOpenSpy = sinon.spy();
65+
const onDrawerSectionHideSpy = sinon.spy();
66+
6367
render(
64-
<DrawerContentProvider>
68+
<DrawerContentProvider
69+
onDrawerSectionOpen={onDrawerSectionOpenSpy}
70+
onDrawerSectionHide={onDrawerSectionHideSpy}
71+
>
6572
<DrawerAnchor>
6673
{[1, 2, 3].map((n, idx) => {
6774
return (
@@ -85,33 +92,62 @@ describe('DrawerSection', function () {
8592
expect(screen.getByText('This is section 1')).to.be.visible;
8693
});
8794

95+
expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-1');
96+
onDrawerSectionOpenSpy.resetHistory();
97+
8898
userEvent.click(screen.getByRole('button', { name: 'Section 2' }));
8999
await waitFor(() => {
90100
expect(screen.getByText('This is section 2')).to.be.visible;
91101
});
92102

103+
expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-1');
104+
expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-2');
105+
onDrawerSectionOpenSpy.resetHistory();
106+
onDrawerSectionHideSpy.resetHistory();
107+
93108
userEvent.click(screen.getByRole('button', { name: 'Section 3' }));
94109
await waitFor(() => {
95110
expect(screen.getByText('This is section 3')).to.be.visible;
96111
});
97112

113+
expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-2');
114+
expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-3');
115+
onDrawerSectionOpenSpy.resetHistory();
116+
onDrawerSectionHideSpy.resetHistory();
117+
98118
userEvent.click(screen.getByRole('button', { name: 'Section 1' }));
99119
await waitFor(() => {
100120
expect(screen.getByText('This is section 1')).to.be.visible;
101121
});
102122

123+
expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-3');
124+
expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith('section-1');
125+
onDrawerSectionOpenSpy.resetHistory();
126+
onDrawerSectionHideSpy.resetHistory();
127+
103128
userEvent.click(screen.getByRole('button', { name: 'Close drawer' }));
104129
await waitFor(() => {
105130
expect(screen.queryByText('This is section 1')).not.to.exist;
106131
expect(screen.queryByText('This is section 2')).not.to.exist;
107132
expect(screen.queryByText('This is section 3')).not.to.exist;
108133
});
134+
135+
expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith('section-1');
136+
expect(onDrawerSectionOpenSpy).to.not.have.been.called;
137+
onDrawerSectionOpenSpy.resetHistory();
138+
onDrawerSectionHideSpy.resetHistory();
109139
});
110140

111141
it('closes drawer when opened section is removed from toolbar data', async function () {
142+
const onDrawerSectionOpenSpy = sinon.spy();
143+
const onDrawerSectionHideSpy = sinon.spy();
144+
112145
// Render two sections, auto-open first one
113146
const { rerender } = render(
114-
<DrawerContentProvider>
147+
<DrawerContentProvider
148+
onDrawerSectionOpen={onDrawerSectionOpenSpy}
149+
onDrawerSectionHide={onDrawerSectionHideSpy}
150+
>
115151
<DrawerAnchor>
116152
<DrawerSection
117153
id="test-section-1"
@@ -138,9 +174,19 @@ describe('DrawerSection', function () {
138174
expect(screen.getByText('This is a test section')).to.be.visible;
139175
});
140176

177+
expect(onDrawerSectionHideSpy).to.not.have.been.called;
178+
expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith(
179+
'test-section-1'
180+
);
181+
onDrawerSectionOpenSpy.resetHistory();
182+
onDrawerSectionHideSpy.resetHistory();
183+
141184
// Now render without opened section
142185
rerender(
143-
<DrawerContentProvider>
186+
<DrawerContentProvider
187+
onDrawerSectionOpen={onDrawerSectionOpenSpy}
188+
onDrawerSectionHide={onDrawerSectionHideSpy}
189+
>
144190
<DrawerAnchor>
145191
<DrawerSection
146192
id="test-section-2"
@@ -163,9 +209,17 @@ describe('DrawerSection', function () {
163209
// drawer with toolbar
164210
screen.getByTestId('lg-drawer')
165211
).to.have.attribute('aria-hidden', 'true');
212+
213+
expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith(
214+
'test-section-1'
215+
);
216+
expect(onDrawerSectionOpenSpy).to.not.have.been.called;
166217
});
167218

168219
it('can control drawer state via the hooks', async function () {
220+
const onDrawerSectionOpenSpy = sinon.spy();
221+
const onDrawerSectionHideSpy = sinon.spy();
222+
169223
const ControlElement = () => {
170224
const { isDrawerOpen } = useDrawerState();
171225
const { openDrawer, closeDrawer } = useDrawerActions();
@@ -188,7 +242,10 @@ describe('DrawerSection', function () {
188242
);
189243
};
190244
render(
191-
<DrawerContentProvider>
245+
<DrawerContentProvider
246+
onDrawerSectionOpen={onDrawerSectionOpenSpy}
247+
onDrawerSectionHide={onDrawerSectionHideSpy}
248+
>
192249
<ControlElement />
193250
<DrawerAnchor>
194251
<DrawerSection
@@ -214,19 +271,36 @@ describe('DrawerSection', function () {
214271
// Drawer is closed by default
215272
expect(screen.getByTestId('drawer-state')).to.have.text('closed');
216273

274+
expect(onDrawerSectionHideSpy).to.not.have.been.called;
275+
expect(onDrawerSectionOpenSpy).to.not.have.been.called;
276+
onDrawerSectionOpenSpy.resetHistory();
277+
onDrawerSectionHideSpy.resetHistory();
278+
217279
// Open the drawer
218280
userEvent.click(screen.getByRole('button', { name: 'Hook Open drawer' }));
219281
await waitFor(() => {
220282
expect(screen.getByTestId('drawer-state')).to.have.text('open');
221283
expect(screen.getByText('This is the controlled section')).to.be.visible;
222284
});
223285

286+
expect(onDrawerSectionHideSpy).to.not.have.been.called;
287+
expect(onDrawerSectionOpenSpy).to.have.been.calledOnceWith(
288+
'controlled-section'
289+
);
290+
onDrawerSectionOpenSpy.resetHistory();
291+
onDrawerSectionHideSpy.resetHistory();
292+
224293
// Close the drawer
225294
userEvent.click(screen.getByRole('button', { name: 'Hook Close drawer' }));
226295
await waitFor(() => {
227296
expect(screen.getByTestId('drawer-state')).to.have.text('closed');
228297
expect(screen.queryByText('This is the controlled section')).not.to.exist;
229298
});
299+
300+
expect(onDrawerSectionHideSpy).to.have.been.calledOnceWith(
301+
'controlled-section'
302+
);
303+
expect(onDrawerSectionOpenSpy).to.not.have.been.called;
230304
});
231305

232306
it('renders guide cue when passed in props', async function () {

packages/compass-components/src/components/drawer-portal.tsx

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ const DrawerOpenStateContext =
6262
const DrawerSetOpenStateContext =
6363
React.createContext<DrawerSetOpenStateContextValue>(() => {});
6464

65+
type DrawerCurrentTabStateContextValue = string | null;
66+
67+
type DrawerSetCurrentTabContextValue = (currentTab: string | null) => void;
68+
69+
const DrawerCurrentTabStateContext =
70+
React.createContext<DrawerCurrentTabStateContextValue>(null);
71+
72+
const DrawerSetCurrentTabContext =
73+
React.createContext<DrawerSetCurrentTabContextValue>(() => {});
74+
6575
const DrawerActionsContext = React.createContext<DrawerActionsContextValue>({
6676
current: {
6777
openDrawer: () => undefined,
@@ -104,12 +114,16 @@ const DrawerActionsContext = React.createContext<DrawerActionsContextValue>({
104114
* )
105115
* }
106116
*/
107-
export const DrawerContentProvider: React.FunctionComponent = ({
108-
children,
109-
}) => {
117+
export const DrawerContentProvider: React.FunctionComponent<{
118+
onDrawerSectionOpen?: (drawerSectionId: string) => void;
119+
onDrawerSectionHide?: (drawerSectionId: string) => void;
120+
children?: React.ReactNode;
121+
}> = ({ onDrawerSectionOpen, onDrawerSectionHide, children }) => {
110122
const [drawerState, setDrawerState] = useState<DrawerSectionProps[]>([]);
111123
const [drawerOpenState, setDrawerOpenState] =
112124
useState<DrawerOpenStateContextValue>(false);
125+
const [drawerCurrentTab, setDrawerCurrentTab] =
126+
useState<DrawerCurrentTabStateContextValue>(null);
113127
const drawerActions = useRef({
114128
openDrawer: () => undefined,
115129
closeDrawer: () => undefined,
@@ -135,13 +149,36 @@ export const DrawerContentProvider: React.FunctionComponent = ({
135149
},
136150
});
137151

152+
const prevDrawerCurrentTabRef = React.useRef<string | null>(null);
153+
154+
useEffect(() => {
155+
if (drawerCurrentTab === prevDrawerCurrentTabRef.current) {
156+
// ignore unless it changed
157+
return;
158+
}
159+
160+
if (drawerCurrentTab) {
161+
onDrawerSectionOpen?.(drawerCurrentTab);
162+
}
163+
164+
if (prevDrawerCurrentTabRef.current) {
165+
onDrawerSectionHide?.(prevDrawerCurrentTabRef.current);
166+
}
167+
168+
prevDrawerCurrentTabRef.current = drawerCurrentTab;
169+
}, [drawerCurrentTab, onDrawerSectionHide, onDrawerSectionOpen]);
170+
138171
return (
139172
<DrawerStateContext.Provider value={drawerState}>
140173
<DrawerOpenStateContext.Provider value={drawerOpenState}>
141174
<DrawerSetOpenStateContext.Provider value={setDrawerOpenState}>
142-
<DrawerActionsContext.Provider value={drawerActions}>
143-
{children}
144-
</DrawerActionsContext.Provider>
175+
<DrawerCurrentTabStateContext.Provider value={drawerCurrentTab}>
176+
<DrawerSetCurrentTabContext.Provider value={setDrawerCurrentTab}>
177+
<DrawerActionsContext.Provider value={drawerActions}>
178+
{children}
179+
</DrawerActionsContext.Provider>
180+
</DrawerSetCurrentTabContext.Provider>
181+
</DrawerCurrentTabStateContext.Provider>
145182
</DrawerSetOpenStateContext.Provider>
146183
</DrawerOpenStateContext.Provider>
147184
</DrawerStateContext.Provider>
@@ -152,11 +189,21 @@ const DrawerContextGrabber: React.FunctionComponent = ({ children }) => {
152189
const drawerToolbarContext = useDrawerToolbarContext();
153190
const actions = useContext(DrawerActionsContext);
154191
const openStateSetter = useContext(DrawerSetOpenStateContext);
192+
const currentTabSetter = useContext(DrawerSetCurrentTabContext);
155193
actions.current.openDrawer = drawerToolbarContext.openDrawer;
156194
actions.current.closeDrawer = drawerToolbarContext.closeDrawer;
195+
157196
useEffect(() => {
158197
openStateSetter(drawerToolbarContext.isDrawerOpen);
159198
}, [drawerToolbarContext.isDrawerOpen, openStateSetter]);
199+
200+
useEffect(() => {
201+
const currentTab =
202+
drawerToolbarContext.getActiveDrawerContent()?.id ?? null;
203+
204+
currentTabSetter(currentTab);
205+
}, [drawerToolbarContext, currentTabSetter]);
206+
160207
return <>{children}</>;
161208
};
162209

packages/compass-telemetry/src/telemetry-events.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,34 @@ type IndexDroppedEvent = ConnectionScopedEvent<{
14581458
};
14591459
}>;
14601460

1461+
/**
1462+
* This event is fired when user opens a drawer section. Either by switching
1463+
* to it via the drawer toolbar or by opening the drawer and the first tab is
1464+
* this drawer section.
1465+
*
1466+
* @category Gen AI
1467+
*/
1468+
type DrawerSectionOpenedEvent = CommonEvent<{
1469+
name: 'Drawer Section Opened';
1470+
payload: {
1471+
sectionId: string;
1472+
};
1473+
}>;
1474+
1475+
/**
1476+
* This event is fired when user closes a drawer section. Either by switching
1477+
* to another tab via the drawer toolbar or by closing the drawer when the
1478+
* active tab is this drawer section.
1479+
*
1480+
* @category Gen AI
1481+
*/
1482+
type DrawerSectionClosedEvent = CommonEvent<{
1483+
name: 'Drawer Section Closed';
1484+
payload: {
1485+
sectionId: string;
1486+
};
1487+
}>;
1488+
14611489
/**
14621490
* This event is fired when user enters a prompt in the assistant chat
14631491
* and hits "enter".
@@ -3174,6 +3202,8 @@ export type TelemetryEvent =
31743202
| DocumentDeletedEvent
31753203
| DocumentInsertedEvent
31763204
| DocumentUpdatedEvent
3205+
| DrawerSectionOpenedEvent
3206+
| DrawerSectionClosedEvent
31773207
| EditorTypeChangedEvent
31783208
| ErrorFetchingAttributesEvent
31793209
| ExplainPlanExecutedEvent

packages/compass-web/src/entrypoint.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,16 @@ const CompassWeb = ({
440440
item_label: item.label,
441441
});
442442
}}
443+
onDrawerSectionOpen={(drawerSectionId) => {
444+
onTrackRef.current?.('Drawer Section Opened', {
445+
sectionId: drawerSectionId,
446+
});
447+
}}
448+
onDrawerSectionHide={(drawerSectionId) => {
449+
onTrackRef.current?.('Drawer Section Closed', {
450+
sectionId: drawerSectionId,
451+
});
452+
}}
443453
onSignalMount={(id) => {
444454
onTrackRef.current?.('Signal Shown', { id });
445455
}}

packages/compass/src/app/components/home.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ export default function ThemedHome(
210210
item_label: item.label,
211211
});
212212
}}
213+
onDrawerSectionOpen={(drawerSectionId) => {
214+
track('Drawer Section Opened', { sectionId: drawerSectionId });
215+
}}
216+
onDrawerSectionHide={(drawerSectionId) => {
217+
track('Drawer Section Closed', { sectionId: drawerSectionId });
218+
}}
213219
utmSource="compass"
214220
utmMedium="product"
215221
onSignalMount={(id) => {

0 commit comments

Comments
 (0)