diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx index 104085fcb15..af4c4feb5d0 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx @@ -5,6 +5,10 @@ import { describe, it, vi, afterEach, beforeEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' +import { + ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + useTrackEvent, +} from '/app/redux/analytics' import { getAppLanguage, getStoredSystemLanguage, @@ -16,6 +20,7 @@ import { SystemLanguagePreferenceModal } from '..' vi.mock('react-router-dom') vi.mock('/app/redux/config') vi.mock('/app/redux/shell') +vi.mock('/app/redux/analytics') const render = () => { return renderWithProviders(, { @@ -24,6 +29,7 @@ const render = () => { } const mockNavigate = vi.fn() +const mockTrackEvent = vi.fn() const MOCK_DEFAULT_LANGUAGE = 'en-US' @@ -33,6 +39,7 @@ describe('SystemLanguagePreferenceModal', () => { vi.mocked(getSystemLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) vi.mocked(getStoredSystemLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) vi.mocked(useNavigate).mockReturnValue(mockNavigate) + vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) }) afterEach(() => { vi.resetAllMocks() @@ -68,6 +75,14 @@ describe('SystemLanguagePreferenceModal', () => { 'language.systemLanguage', MOCK_DEFAULT_LANGUAGE ) + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: MOCK_DEFAULT_LANGUAGE, + systemLanguage: MOCK_DEFAULT_LANGUAGE, + modalType: 'appBootModal', + }, + }) }) it('should default to English (US) if system language is unsupported', () => { @@ -90,6 +105,14 @@ describe('SystemLanguagePreferenceModal', () => { MOCK_DEFAULT_LANGUAGE ) expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'es-MX') + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: MOCK_DEFAULT_LANGUAGE, + systemLanguage: 'es-MX', + modalType: 'appBootModal', + }, + }) }) it('should set a supported app language when system language is an unsupported locale of the same language', () => { @@ -112,6 +135,14 @@ describe('SystemLanguagePreferenceModal', () => { MOCK_DEFAULT_LANGUAGE ) expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-GB') + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: MOCK_DEFAULT_LANGUAGE, + systemLanguage: 'en-GB', + modalType: 'appBootModal', + }, + }) }) it('should render the correct header, description, and buttons when system language changes', () => { @@ -139,6 +170,14 @@ describe('SystemLanguagePreferenceModal', () => { 'language.systemLanguage', 'zh-CN' ) + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: 'zh-CN', + systemLanguage: 'zh-CN', + modalType: 'systemLanguageUpdateModal', + }, + }) fireEvent.click(secondaryButton) expect(updateConfigValue).toHaveBeenNthCalledWith( 3, @@ -168,6 +207,14 @@ describe('SystemLanguagePreferenceModal', () => { 'language.systemLanguage', 'zh-Hant' ) + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: 'zh-CN', + systemLanguage: 'zh-Hant', + modalType: 'systemLanguageUpdateModal', + }, + }) fireEvent.click(secondaryButton) expect(updateConfigValue).toHaveBeenNthCalledWith( 3, diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx index d3b04f19061..f135c0fe10a 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx @@ -16,6 +16,10 @@ import { } from '@opentrons/components' import { LANGUAGES } from '/app/i18n' +import { + ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + useTrackEvent, +} from '/app/redux/analytics' import { getAppLanguage, getStoredSystemLanguage, @@ -32,7 +36,7 @@ type ArrayElement< export function SystemLanguagePreferenceModal(): JSX.Element | null { const { i18n, t } = useTranslation(['app_settings', 'shared', 'branded']) - + const trackEvent = useTrackEvent() const [currentOption, setCurrentOption] = useState( LANGUAGES[0] ) @@ -66,6 +70,16 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { const handlePrimaryClick = (): void => { dispatch(updateConfigValue('language.appLanguage', currentOption.value)) dispatch(updateConfigValue('language.systemLanguage', systemLanguage)) + trackEvent({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: currentOption.value, + systemLanguage, + modalType: showUpdateModal + ? 'systemLanguageUpdateModal' + : 'appBootModal', + }, + }) } const handleDropdownClick = (value: string): void => { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx index 49f58e26993..50af850a44f 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx @@ -1,7 +1,8 @@ -import { Fragment } from 'react' +import { Fragment, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' +import uuidv1 from 'uuid/v4' import { BORDERS, @@ -14,6 +15,8 @@ import { } from '@opentrons/components' import { LANGUAGES } from '/app/i18n' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { getAppLanguage, updateConfigValue } from '/app/redux/config' @@ -42,16 +45,31 @@ interface LanguageSettingProps { setCurrentOption: SetSettingOption } +const uuid: () => string = uuidv1 + export function LanguageSetting({ setCurrentOption, }: LanguageSettingProps): JSX.Element { const { t } = useTranslation('app_settings') const dispatch = useDispatch() + const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() + + let transactionId = '' + useEffect(() => { + transactionId = uuid() + }, []) const appLanguage = useSelector(getAppLanguage) const handleChange = (event: ChangeEvent): void => { dispatch(updateConfigValue('language.appLanguage', event.target.value)) + trackEventWithRobotSerial({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS, + properties: { + language: event.target.value, + transactionId, + }, + }) } return ( diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx index 80d35ebea15..fe90eb2e1cb 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx @@ -10,14 +10,18 @@ import { SIMPLIFIED_CHINESE_DISPLAY_NAME, SIMPLIFIED_CHINESE, } from '/app/i18n' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { getAppLanguage, updateConfigValue } from '/app/redux/config' import { renderWithProviders } from '/app/__testing-utils__' import { LanguageSetting } from '../LanguageSetting' vi.mock('/app/redux/config') +vi.mock('/app/redux-resources/analytics') const mockSetCurrentOption = vi.fn() +const mockTrackEvent = vi.fn() const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -32,6 +36,9 @@ describe('LanguageSetting', () => { setCurrentOption: mockSetCurrentOption, } vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEvent, + }) }) it('should render text and buttons', () => { @@ -49,6 +56,13 @@ describe('LanguageSetting', () => { 'language.appLanguage', SIMPLIFIED_CHINESE ) + expect(mockTrackEvent).toHaveBeenCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS, + properties: { + language: SIMPLIFIED_CHINESE, + transactionId: expect.anything(), + }, + }) }) it('should call mock function when tapping back button', () => { diff --git a/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx b/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx index 85b24816da6..eed4f5be96a 100644 --- a/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx +++ b/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx @@ -1,8 +1,9 @@ // app info card with version and updated -import { useState } from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' +import uuidv1 from 'uuid/v4' import { ALIGN_CENTER, @@ -41,6 +42,7 @@ import { import { useTrackEvent, ANALYTICS_APP_UPDATE_NOTIFICATIONS_TOGGLED, + ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS, } from '/app/redux/analytics' import { getAppLanguage, updateConfigValue } from '/app/redux/config' import { UpdateAppModal } from '/app/organisms/Desktop/UpdateAppModal' @@ -55,6 +57,7 @@ const GITHUB_LINK = 'https://github.com/Opentrons/opentrons/blob/edge/app-shell/build/release-notes.md' const ENABLE_APP_UPDATE_NOTIFICATIONS = 'Enable app update notifications' +const uuid: () => string = uuidv1 export function GeneralSettings(): JSX.Element { const { t } = useTranslation(['app_settings', 'shared', 'branded']) @@ -68,9 +71,19 @@ export function GeneralSettings(): JSX.Element { const appLanguage = useSelector(getAppLanguage) const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage) - + let transactionId = '' + useEffect(() => { + transactionId = uuid() + }, []) const handleDropdownClick = (value: string): void => { dispatch(updateConfigValue('language.appLanguage', value)) + trackEvent({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS, + properties: { + language: value, + transactionId, + }, + }) } const [showUpdateBanner, setShowUpdateBanner] = useState( diff --git a/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx b/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx index 43d17a4d2cb..a06f4204bd7 100644 --- a/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx +++ b/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx @@ -12,6 +12,10 @@ import { US_ENGLISH_DISPLAY_NAME, } from '/app/i18n' import { getAlertIsPermanentlyIgnored } from '/app/redux/alerts' +import { + ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS, + useTrackEvent, +} from '/app/redux/analytics' import { getAppLanguage, updateConfigValue } from '/app/redux/config' import * as Shell from '/app/redux/shell' import { GeneralSettings } from '../GeneralSettings' @@ -32,11 +36,14 @@ const render = (): ReturnType => { ) } +const mockTrackEvent = vi.fn() + describe('GeneralSettings', () => { beforeEach(() => { vi.mocked(Shell.getAvailableShellUpdate).mockReturnValue(null) vi.mocked(getAlertIsPermanentlyIgnored).mockReturnValue(false) vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH) + vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) }) afterEach(() => { vi.resetAllMocks() @@ -118,5 +125,12 @@ describe('GeneralSettings', () => { 'language.appLanguage', SIMPLIFIED_CHINESE ) + expect(mockTrackEvent).toHaveBeenCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS, + properties: { + language: SIMPLIFIED_CHINESE, + transactionId: expect.anything(), + }, + }) }) }) diff --git a/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx b/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx index 8508a7b4d08..fa5c793e2d2 100644 --- a/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx +++ b/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx @@ -1,10 +1,12 @@ -import { vi, it, describe, expect } from 'vitest' +import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' -import { updateConfigValue } from '/app/redux/config' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' +import { updateConfigValue, getAppLanguage } from '/app/redux/config' import { ChooseLanguage } from '..' import type { NavigateFunction } from 'react-router-dom' @@ -18,6 +20,9 @@ vi.mock('react-router-dom', async importOriginal => { } }) vi.mock('/app/redux/config') +vi.mock('/app/redux-resources/analytics') + +const mockTrackEvent = vi.fn() const render = () => { return renderWithProviders( @@ -31,6 +36,12 @@ const render = () => { } describe('ChooseLanguage', () => { + beforeEach(() => { + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEvent, + }) + vi.mocked(getAppLanguage).mockReturnValue('en-US') + }) it('should render text, language options, and continue button', () => { render() screen.getByText('Choose your language') @@ -54,6 +65,12 @@ describe('ChooseLanguage', () => { it('should call mockNavigate when tapping continue', () => { render() fireEvent.click(screen.getByRole('button', { name: 'Continue' })) + expect(mockTrackEvent).toHaveBeenCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW, + properties: { + language: 'en-US', + }, + }) expect(mockNavigate).toHaveBeenCalledWith('/welcome') }) }) diff --git a/app/src/pages/ODD/ChooseLanguage/index.tsx b/app/src/pages/ODD/ChooseLanguage/index.tsx index d0110e68591..8ecb87451f7 100644 --- a/app/src/pages/ODD/ChooseLanguage/index.tsx +++ b/app/src/pages/ODD/ChooseLanguage/index.tsx @@ -13,6 +13,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { MediumButton } from '/app/atoms/buttons' import { LANGUAGES, US_ENGLISH } from '/app/i18n' import { RobotSetupHeader } from '/app/organisms/ODD/RobotSetupHeader' @@ -24,6 +26,7 @@ export function ChooseLanguage(): JSX.Element { const { i18n, t } = useTranslation(['app_settings', 'shared']) const navigate = useNavigate() const dispatch = useDispatch() + const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() const appLanguage = useSelector(getAppLanguage) @@ -69,6 +72,12 @@ export function ChooseLanguage(): JSX.Element { { + trackEventWithRobotSerial({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW, + properties: { + language: appLanguage, + }, + }) navigate('/welcome') }} width="100%" diff --git a/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx b/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx index f769dc005c4..c12bcd5dbeb 100644 --- a/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx +++ b/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx @@ -12,6 +12,7 @@ import { useTrackEvent, ANALYTICS_PROTOCOL_RUN_ACTION, } from '/app/redux/analytics' +import { getAppLanguage } from '/app/redux/config' import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' import { useRobot } from '/app/redux-resources/robots' @@ -23,6 +24,7 @@ vi.mock('../useProtocolRunAnalyticsData') vi.mock('/app/redux/discovery') vi.mock('/app/redux/pipettes') vi.mock('/app/redux/analytics') +vi.mock('/app/redux/config') vi.mock('/app/redux/robot-settings') const RUN_ID = 'runId' @@ -55,6 +57,7 @@ describe('useTrackProtocolRunEvent hook', () => { ) vi.mocked(useRobot).mockReturnValue(mockConnectableRobot) vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) + vi.mocked(getAppLanguage).mockReturnValue('en-US') when(vi.mocked(useProtocolRunAnalyticsData)) .calledWith(RUN_ID, mockConnectableRobot) @@ -88,7 +91,11 @@ describe('useTrackProtocolRunEvent hook', () => { ) expect(mockTrackEvent).toHaveBeenCalledWith({ name: ANALYTICS_PROTOCOL_RUN_ACTION.START, - properties: { ...PROTOCOL_PROPERTIES, transactionId: RUN_ID }, + properties: { + ...PROTOCOL_PROPERTIES, + transactionId: RUN_ID, + appLanguage: 'en-US', + }, }) }) diff --git a/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts b/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts index 05c3ce16746..0603994d4b4 100644 --- a/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts +++ b/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts @@ -1,5 +1,7 @@ +import { useSelector } from 'react-redux' import { useTrackEvent } from '/app/redux/analytics' import { useProtocolRunAnalyticsData } from './useProtocolRunAnalyticsData' +import { getAppLanguage } from '/app/redux/config' import { useRobot } from '/app/redux-resources/robots' interface ProtocolRunAnalyticsEvent { @@ -21,7 +23,7 @@ export function useTrackProtocolRunEvent( runId, robot ) - + const appLanguage = useSelector(getAppLanguage) const trackProtocolRunEvent: TrackProtocolRunEvent = ({ name, properties = {}, @@ -37,6 +39,7 @@ export function useTrackProtocolRunEvent( // It's sometimes unavoidable (namely on the desktop app) to prevent sending an event multiple times. // In these circumstances, we need an idempotency key to accurately filter events in Mixpanel. transactionId: runId, + appLanguage, }, }) }) diff --git a/app/src/redux/analytics/constants.ts b/app/src/redux/analytics/constants.ts index cde9b0a1d59..aadeb7c6696 100644 --- a/app/src/redux/analytics/constants.ts +++ b/app/src/redux/analytics/constants.ts @@ -103,3 +103,15 @@ export const ANALYTICS_QUICK_TRANSFER_RERUN = 'quickTransferReRunFromSummary' */ export const ANALYTICS_RESOURCE_MONITOR_REPORT: 'analytics:RESOURCE_MONITOR_REPORT' = 'analytics:RESOURCE_MONITOR_REPORT' + +/** + * Internationalization Analytics + */ +export const ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW: 'languageUpdatedOddUnboxingFlow' = + 'languageUpdatedOddUnboxingFlow' +export const ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS: 'languageUpdatedOddSettings' = + 'languageUpdatedOddSettings' +export const ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL: 'languageUpdatedDesktopAppModal' = + 'languageUpdatedDesktopAppModal' +export const ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS: 'languageUpdatedDesktopAppSettings' = + 'languageUpdatedDesktopAppSettings'