Skip to content

Commit

Permalink
feat: add remaining analytics events (#16831)
Browse files Browse the repository at this point in the history
# Overview

This PR adds and adjusts the AI Client analytics events tracking.


![image](https://github.com/user-attachments/assets/03b90a9a-3b18-436f-a7c8-034c42d23c91)


## Test Plan and Hands on Testing

Unit tests for every event and manually tested.

## Changelog

- Add analytics

## Review requests



## Risk assessment

- low
  • Loading branch information
fbelginetw authored Nov 14, 2024
1 parent 63f8b8e commit a5b9716
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import type * as React from 'react'
import { screen } from '@testing-library/react'
import { describe, it, beforeEach } from 'vitest'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest'
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'

import { ChatDisplay } from '../index'
import { useForm, FormProvider } from 'react-hook-form'

const mockUseTrackEvent = vi.fn()

vi.mock('../../../resources/hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

vi.mock('../../../hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

const RenderChatDisplay = (props: React.ComponentProps<typeof ChatDisplay>) => {
const methods = useForm({
defaultValues: {},
Expand Down Expand Up @@ -38,6 +48,11 @@ describe('ChatDisplay', () => {
chatId: 'mockId',
}
})

afterEach(() => {
vi.clearAllMocks()
})

it('should display response from the backend and label', () => {
render(props)
screen.getByText('OpentronsAI')
Expand All @@ -62,4 +77,58 @@ describe('ChatDisplay', () => {
// const display = screen.getByTextId('ChatDisplay_from_user')
// expect(display).toHaveStyle(`background-color: ${COLORS.blue}`)
})

it('should call trackEvent when regenerate button is clicked', () => {
render(props)
// eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/non-nullable-type-assertion-style
const regeneratePath = document.querySelector(
'[aria-roledescription="reload"]'
) as Element
fireEvent.click(regeneratePath)

expect(mockUseTrackEvent).toHaveBeenCalledWith({
name: 'regenerate-protocol',
properties: {},
})
})

it('should call trackEvent when download button is clicked', () => {
URL.createObjectURL = vi.fn()
window.URL.revokeObjectURL = vi.fn()
HTMLAnchorElement.prototype.click = vi.fn()

render(props)
// eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/non-nullable-type-assertion-style
const downloadPath = document.querySelector(
'[aria-roledescription="download"]'
) as Element
fireEvent.click(downloadPath)

expect(mockUseTrackEvent).toHaveBeenCalledWith({
name: 'download-protocol',
properties: {},
})
})

it('should call trackEvent when copy button is clicked', async () => {
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: async () => {},
},
})

render(props)
// eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/non-nullable-type-assertion-style
const copyPath = document.querySelector(
'[aria-roledescription="content-copy"]'
) as Element
fireEvent.click(copyPath)

await waitFor(() => {
expect(mockUseTrackEvent).toHaveBeenCalledWith({
name: 'copy-protocol',
properties: {},
})
})
})
})
15 changes: 15 additions & 0 deletions opentrons-ai-client/src/molecules/ChatDisplay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from '../../resources/atoms'
import { delay } from 'lodash'
import { useFormContext } from 'react-hook-form'
import { useTrackEvent } from '../../resources/hooks/useTrackEvent'

interface ChatDisplayProps {
chat: ChatData
Expand All @@ -58,6 +59,7 @@ const StyledIcon = styled(Icon)`

export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
const { t } = useTranslation('protocol_generator')
const trackEvent = useTrackEvent()
const [isCopied, setIsCopied] = useState<boolean>(false)
const [, setRegenerateProtocol] = useAtom(regenerateProtocolAtom)
const [createProtocolChat] = useAtom(createProtocolChatAtom)
Expand Down Expand Up @@ -96,6 +98,10 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
}
setScrollToBottom(!scrollToBottom)
setValue('userPrompt', prompt)
trackEvent({
name: 'regenerate-protocol',
properties: {},
})
}

const handleFileDownload = (): void => {
Expand All @@ -112,13 +118,22 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element {
a.download = 'OpentronsAI.py'
a.click()
window.URL.revokeObjectURL(url)

trackEvent({
name: 'download-protocol',
properties: {},
})
}

const handleClickCopy = async (): Promise<void> => {
const lastCodeBlock = document.querySelector(`#${chatId}`)
const code = lastCodeBlock?.textContent ?? ''
await navigator.clipboard.writeText(code)
setIsCopied(true)
trackEvent({
name: 'copy-protocol',
properties: {},
})
}

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { FeedbackModal } from '..'
import { renderWithProviders } from '../../../__testing-utils__'
import { screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { i18n } from '../../../i18n'
import { feedbackModalAtom } from '../../../resources/atoms'

const mockUseTrackEvent = vi.fn()

vi.mock('../../../resources/hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

vi.mock('../../../hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

const initialValues: Array<[any, any]> = [[feedbackModalAtom, true]]

const render = (): ReturnType<typeof renderWithProviders> => {
Expand Down Expand Up @@ -33,4 +43,26 @@ describe('FeedbackModal', () => {
// check if the feedbackModalAtom is set to false
expect(feedbackModalAtom.read).toBe(false)
})

it('should track event when feedback is sent', async () => {
render()
const feedbackInput = screen.getByRole('textbox')
fireEvent.change(feedbackInput, {
target: { value: 'This is a test feedback' },
})
const sendFeedbackButton = screen.getByRole('button', {
name: 'Send feedback',
})

fireEvent.click(sendFeedbackButton)

await waitFor(() => {
expect(mockUseTrackEvent).toHaveBeenCalledWith({
name: 'feedback-sent',
properties: {
feedback: 'This is a test feedback',
},
})
})
})
})
8 changes: 8 additions & 0 deletions opentrons-ai-client/src/molecules/FeedbackModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import {
LOCAL_FEEDBACK_END_POINT,
} from '../../resources/constants'
import { useApiCall } from '../../resources/hooks'
import { useTrackEvent } from '../../resources/hooks/useTrackEvent'

export function FeedbackModal(): JSX.Element {
const { t } = useTranslation('protocol_generator')
const trackEvent = useTrackEvent()

const [feedbackValue, setFeedbackValue] = useState<string>('')
const [, setShowFeedbackModal] = useAtom(feedbackModalAtom)
Expand Down Expand Up @@ -58,6 +60,12 @@ export function FeedbackModal(): JSX.Element {
},
}
await callApi(config as AxiosRequestConfig)
trackEvent({
name: 'feedback-sent',
properties: {
feedback: feedbackValue,
},
})
setShowFeedbackModal(false)
} catch (err: any) {
console.error(`error: ${err.message}`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import type * as React from 'react'
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
import { FormProvider, useForm } from 'react-hook-form'
import { fireEvent, screen } from '@testing-library/react'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { InputPrompt } from '../index'

const mockUseTrackEvent = vi.fn()

vi.mock('../../../resources/hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

vi.mock('../../../hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

const WrappingForm = (wrappedComponent: {
children: React.ReactNode
}): JSX.Element => {
Expand Down Expand Up @@ -44,4 +54,21 @@ describe('InputPrompt', () => {
})

// ToDo (kk:04/19/2024) add more test cases

it('should track event when send button is clicked', async () => {
render()
const textbox = screen.getByRole('textbox')
fireEvent.change(textbox, { target: { value: ['test'] } })
const sendButton = screen.getByRole('button')
fireEvent.click(sendButton)

await waitFor(() => {
expect(mockUseTrackEvent).toHaveBeenCalledWith({
name: 'chat-submitted',
properties: {
chat: 'test',
},
})
})
})
})
15 changes: 15 additions & 0 deletions opentrons-ai-client/src/molecules/InputPrompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ import type {
CreatePrompt,
UpdatePrompt,
} from '../../resources/types'
import { useTrackEvent } from '../../resources/hooks/useTrackEvent'

export function InputPrompt(): JSX.Element {
const { t } = useTranslation('protocol_generator')
const { register, watch, reset, setValue } = useFormContext()
const trackEvent = useTrackEvent()

const [updateProtocol] = useAtom(updateProtocolChatAtom)
const [createProtocol] = useAtom(createProtocolChatAtom)
Expand Down Expand Up @@ -138,6 +140,12 @@ export function InputPrompt(): JSX.Element {
{ role: 'user', content: watchUserPrompt },
])
await callApi(config as AxiosRequestConfig)
trackEvent({
name: 'chat-submitted',
properties: {
chat: watchUserPrompt,
},
})
setSubmitted(true)
} catch (err: any) {
console.error(`error: ${err.message}`)
Expand Down Expand Up @@ -182,6 +190,13 @@ export function InputPrompt(): JSX.Element {
{ role: 'assistant', content: reply },
])
setChatData(chatData => [...chatData, assistantResponse])
trackEvent({
name: 'generated-protocol',
properties: {
createOrUpdate: isNewProtocol ? 'create' : 'update',
protocol: reply,
},
})
setSubmitted(false)
}
}, [data, isLoading, submitted])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('CreateProtocol', () => {
expect(mockNavigate).toHaveBeenCalledWith('/chat')
expect(mockUseTrackEvent).toHaveBeenCalledWith({
name: 'submit-prompt',
properties: { prompt: expect.any(String) },
properties: { isCreateOrUpdate: 'create', prompt: expect.any(String) },
})
})
})
1 change: 1 addition & 0 deletions opentrons-ai-client/src/pages/CreateProtocol/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export function CreateProtocol(): JSX.Element | null {
trackEvent({
name: 'submit-prompt',
properties: {
isCreateOrUpdate: 'create',
prompt: chatPromptData,
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,53 @@ describe('Update Protocol', () => {
})
expect(mockNavigate).toHaveBeenCalledWith('/chat')
})

it('should call trackEvent when submit prompt button is clicked', async () => {
render()

// upload file
const blobParts: BlobPart[] = [
'x = 1\n',
'x = 2\n',
'x = 3\n',
'x = 4\n',
'print("x is 1.")\n',
]
const file = new File(blobParts, 'test-file.py', { type: 'text/python' })
fireEvent.drop(screen.getByTestId('file_drop_zone'), {
dataTransfer: {
files: [file],
},
})

// input description
const describeInput = screen.getByRole('textbox')
fireEvent.change(describeInput, { target: { value: 'Test description' } })

expect(screen.getByDisplayValue('Test description')).toBeInTheDocument()

// select update type
const applicationDropdown = screen.getByText('Select an option')
fireEvent.click(applicationDropdown)

const basicOtherOption = screen.getByText('Other')
fireEvent.click(basicOtherOption)

const submitPromptButton = screen.getByText('Submit prompt')
await waitFor(() => {
expect(submitPromptButton).toBeEnabled()
})

fireEvent.click(submitPromptButton)

await waitFor(() => {
expect(mockUseTrackEvent).toHaveBeenCalledWith({
name: 'submit-prompt',
properties: {
isCreateOrUpdate: 'update',
prompt: expect.any(String),
},
})
})
})
})
1 change: 1 addition & 0 deletions opentrons-ai-client/src/pages/UpdateProtocol/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export function UpdateProtocol(): JSX.Element {
trackEvent({
name: 'submit-prompt',
properties: {
isCreateOrUpdate: 'update',
prompt: chatPrompt,
},
})
Expand Down

0 comments on commit a5b9716

Please sign in to comment.