diff --git a/packages/frontend/editor-ui/src/init.test.ts b/packages/frontend/editor-ui/src/init.test.ts index 22a740162b..9abb300cc1 100644 --- a/packages/frontend/editor-ui/src/init.test.ts +++ b/packages/frontend/editor-ui/src/init.test.ts @@ -1,23 +1,25 @@ -import { useUsersStore } from '@/stores/users.store'; +import { mockedStore, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import { EnterpriseEditionFeature } from '@/constants'; +import { initializeAuthenticatedFeatures, initializeCore, state } from '@/init'; +import { UserManagementAuthenticationMethod } from '@/Interface'; import { useCloudPlanStore } from '@/stores/cloudPlan.store'; -import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; -import { useRootStore } from '@n8n/stores/useRootStore'; -import { state, initializeAuthenticatedFeatures, initializeCore } from '@/init'; -import { createTestingPinia } from '@pinia/testing'; -import { setActivePinia } from 'pinia'; import { useSettingsStore } from '@/stores/settings.store'; +import { useSourceControlStore } from '@/stores/sourceControl.store'; +import { useSSOStore } from '@/stores/sso.store'; +import { useUIStore } from '@/stores/ui.store'; +import { useUsersStore } from '@/stores/users.store'; import { useVersionsStore } from '@/stores/versions.store'; +import type { Cloud, CurrentUserResponse } from '@n8n/rest-api-client'; +import type { IUser } from '@n8n/rest-api-client/api/users'; +import { STORES } from '@n8n/stores'; +import { useRootStore } from '@n8n/stores/useRootStore'; +import { createTestingPinia } from '@pinia/testing'; import { AxiosError } from 'axios'; import merge from 'lodash/merge'; -import { mockedStore, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; -import { STORES } from '@n8n/stores'; -import { useSSOStore } from '@/stores/sso.store'; -import { UserManagementAuthenticationMethod } from '@/Interface'; -import type { IUser } from '@n8n/rest-api-client/api/users'; -import { EnterpriseEditionFeature } from '@/constants'; -import { useUIStore } from '@/stores/ui.store'; -import type { Cloud } from '@n8n/rest-api-client'; +import { setActivePinia } from 'pinia'; +import { mock } from 'vitest-mock-extended'; +import { telemetry } from './plugins/telemetry'; const showMessage = vi.fn(); const showToast = vi.fn(); @@ -34,19 +36,16 @@ vi.mock('@/stores/users.store', () => ({ }), })); -vi.mock('@n8n/stores/useRootStore', () => ({ - useRootStore: vi.fn(), -})); - describe('Init', () => { - let settingsStore: ReturnType; + let settingsStore: ReturnType>; let cloudPlanStore: ReturnType>; - let sourceControlStore: ReturnType; - let usersStore: ReturnType; - let nodeTypesStore: ReturnType; - let versionsStore: ReturnType; - let ssoStore: ReturnType; - let uiStore: ReturnType; + let sourceControlStore: ReturnType>; + let usersStore: ReturnType>; + let nodeTypesStore: ReturnType>; + let versionsStore: ReturnType>; + let ssoStore: ReturnType>; + let uiStore: ReturnType>; + let rootStore: ReturnType>; beforeEach(() => { setActivePinia( @@ -57,15 +56,16 @@ describe('Init', () => { }), ); - settingsStore = useSettingsStore(); + settingsStore = mockedStore(useSettingsStore); cloudPlanStore = mockedStore(useCloudPlanStore); - sourceControlStore = useSourceControlStore(); - nodeTypesStore = useNodeTypesStore(); - usersStore = useUsersStore(); - versionsStore = useVersionsStore(); - versionsStore = useVersionsStore(); - ssoStore = useSSOStore(); - uiStore = useUIStore(); + sourceControlStore = mockedStore(useSourceControlStore); + nodeTypesStore = mockedStore(useNodeTypesStore); + usersStore = mockedStore(useUsersStore); + versionsStore = mockedStore(useVersionsStore); + versionsStore = mockedStore(useVersionsStore); + ssoStore = mockedStore(useSSOStore); + uiStore = mockedStore(useUIStore); + rootStore = mockedStore(useRootStore); }); describe('initializeCore()', () => { @@ -118,6 +118,19 @@ describe('Init', () => { expect(registerLogoutHookSpy).toHaveBeenCalled(); }); + it('should correctly identify the user for telemetry', async () => { + const telemetryIdentifySpy = vi.spyOn(telemetry, 'identify'); + usersStore.registerLoginHook.mockImplementation((hook) => + hook(mock({ id: 'userId' })), + ); + rootStore.instanceId = 'testInstanceId'; + rootStore.versionCli = '1.102.0'; + + await initializeCore(); + + expect(telemetryIdentifySpy).toHaveBeenCalledWith('testInstanceId', 'userId', '1.102.0'); + }); + it('should initialize ssoStore with settings SSO configuration', async () => { const saml = { loginEnabled: true, loginLabel: '' }; const ldap = { loginEnabled: false, loginLabel: '' }; @@ -155,12 +168,10 @@ describe('Init', () => { describe('initializeAuthenticatedFeatures()', () => { beforeEach(() => { - vi.spyOn(settingsStore, 'isCloudDeployment', 'get').mockReturnValue(true); - vi.spyOn(settingsStore, 'isTemplatesEnabled', 'get').mockReturnValue(true); - vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true); - vi.mocked(useRootStore).mockReturnValue({ defaultLocale: 'es' } as ReturnType< - typeof useRootStore - >); + settingsStore.isCloudDeployment = true; + settingsStore.isTemplatesEnabled = true; + sourceControlStore.isEnterpriseSourceControlEnabled = true; + rootStore.defaultLocale = 'es'; }); afterEach(() => { @@ -172,9 +183,7 @@ describe('Init', () => { const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); - vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType< - typeof useUsersStore - >); + usersStore.currentUser = null; await initializeAuthenticatedFeatures(false); expect(cloudStoreSpy).not.toHaveBeenCalled(); @@ -188,9 +197,7 @@ describe('Init', () => { const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); - vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< - typeof useUsersStore - >); + usersStore.currentUser = mock({ id: '123', globalScopes: ['*'] }); await initializeAuthenticatedFeatures(false); @@ -211,9 +218,7 @@ describe('Init', () => { const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); - vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< - typeof useUsersStore - >); + usersStore.currentUser = mock({ id: '123', globalScopes: ['*'] }); await initializeAuthenticatedFeatures(false); @@ -230,9 +235,7 @@ describe('Init', () => { const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); - vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< - typeof useUsersStore - >); + usersStore.currentUser = mock({ id: '123', globalScopes: ['*'] }); await initializeAuthenticatedFeatures(false); @@ -244,9 +247,7 @@ describe('Init', () => { it('should handle source control initialization error', async () => { vi.spyOn(cloudPlanStore, 'initialize').mockResolvedValue(); - vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< - typeof useUsersStore - >); + usersStore.currentUser = mock({ id: '123', globalScopes: ['*'] }); vi.spyOn(sourceControlStore, 'getPreferences').mockRejectedValueOnce( new AxiosError('Something went wrong', '404'), ); diff --git a/packages/frontend/editor-ui/src/init.ts b/packages/frontend/editor-ui/src/init.ts index de471dd80e..1813c1a7f3 100644 --- a/packages/frontend/editor-ui/src/init.ts +++ b/packages/frontend/editor-ui/src/init.ts @@ -1,32 +1,32 @@ -import { h } from 'vue'; -import { useCloudPlanStore } from '@/stores/cloudPlan.store'; -import { useNodeTypesStore } from '@/stores/nodeTypes.store'; -import { useRootStore } from '@n8n/stores/useRootStore'; -import { useSettingsStore } from '@/stores/settings.store'; -import { useSourceControlStore } from '@/stores/sourceControl.store'; -import { useUsersStore } from '@/stores/users.store'; -import { useExternalHooks } from '@/composables/useExternalHooks'; -import { useVersionsStore } from '@/stores/versions.store'; -import { useProjectsStore } from '@/stores/projects.store'; -import { useRolesStore } from './stores/roles.store'; -import { useInsightsStore } from '@/features/insights/insights.store'; -import { useToast } from '@/composables/useToast'; -import { useI18n } from '@n8n/i18n'; import SourceControlInitializationErrorMessage from '@/components/SourceControlInitializationErrorMessage.vue'; -import { useSSOStore } from '@/stores/sso.store'; -import { EnterpriseEditionFeature, VIEWS } from '@/constants'; -import type { UserManagementAuthenticationMethod } from '@/Interface'; -import { useUIStore } from '@/stores/ui.store'; -import type { BannerName } from '@n8n/api-types'; -import { useNpsSurveyStore } from '@/stores/npsSurvey.store'; -import { usePostHog } from '@/stores/posthog.store'; +import { useExternalHooks } from '@/composables/useExternalHooks'; import { useTelemetry } from '@/composables/useTelemetry'; -import { useRBACStore } from '@/stores/rbac.store'; +import { useToast } from '@/composables/useToast'; +import { EnterpriseEditionFeature, VIEWS } from '@/constants'; +import { useInsightsStore } from '@/features/insights/insights.store'; +import type { UserManagementAuthenticationMethod } from '@/Interface'; import { + registerModuleModals, registerModuleProjectTabs, registerModuleResources, - registerModuleModals, } from '@/moduleInitializer/moduleInitializer'; +import { useCloudPlanStore } from '@/stores/cloudPlan.store'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import { useNpsSurveyStore } from '@/stores/npsSurvey.store'; +import { usePostHog } from '@/stores/posthog.store'; +import { useProjectsStore } from '@/stores/projects.store'; +import { useRBACStore } from '@/stores/rbac.store'; +import { useSettingsStore } from '@/stores/settings.store'; +import { useSourceControlStore } from '@/stores/sourceControl.store'; +import { useSSOStore } from '@/stores/sso.store'; +import { useUIStore } from '@/stores/ui.store'; +import { useUsersStore } from '@/stores/users.store'; +import { useVersionsStore } from '@/stores/versions.store'; +import type { BannerName } from '@n8n/api-types'; +import { useI18n } from '@n8n/i18n'; +import { useRootStore } from '@n8n/stores/useRootStore'; +import { h } from 'vue'; +import { useRolesStore } from './stores/roles.store'; export const state = { initialized: false, @@ -209,7 +209,7 @@ function registerAuthenticationHooks() { usersStore.registerLoginHook((user) => { RBACStore.setGlobalScopes(user.globalScopes ?? []); - telemetry.identify(rootStore.instanceId, user.id); + telemetry.identify(rootStore.instanceId, user.id, rootStore.versionCli); postHogStore.init(user.featureFlags); npsSurveyStore.setupNpsSurveyOnLogin(user.id, user.settings); void settingsStore.getModuleSettings(); diff --git a/packages/frontend/editor-ui/src/stores/assistant.store.test.ts b/packages/frontend/editor-ui/src/stores/assistant.store.test.ts index e0e0b709aa..14c61efa07 100644 --- a/packages/frontend/editor-ui/src/stores/assistant.store.test.ts +++ b/packages/frontend/editor-ui/src/stores/assistant.store.test.ts @@ -13,7 +13,7 @@ import { usePostHog } from './posthog.store'; import { useSettingsStore } from '@/stores/settings.store'; import { defaultSettings } from '../__tests__/defaults'; import merge from 'lodash/merge'; -import { DEFAULT_POSTHOG_SETTINGS } from './posthog.test'; +import { DEFAULT_POSTHOG_SETTINGS } from './posthog.store.test'; import { VIEWS } from '@/constants'; import { reactive } from 'vue'; import * as chatAPI from '@/api/ai'; diff --git a/packages/frontend/editor-ui/src/stores/builder.store.test.ts b/packages/frontend/editor-ui/src/stores/builder.store.test.ts index cf8ed8388a..4e9e7f029f 100644 --- a/packages/frontend/editor-ui/src/stores/builder.store.test.ts +++ b/packages/frontend/editor-ui/src/stores/builder.store.test.ts @@ -8,7 +8,7 @@ import { usePostHog } from './posthog.store'; import { useSettingsStore } from '@/stores/settings.store'; import { defaultSettings } from '../__tests__/defaults'; import merge from 'lodash/merge'; -import { DEFAULT_POSTHOG_SETTINGS } from './posthog.test'; +import { DEFAULT_POSTHOG_SETTINGS } from './posthog.store.test'; import { WORKFLOW_BUILDER_EXPERIMENT, DEFAULT_NEW_WORKFLOW_NAME } from '@/constants'; import { reactive } from 'vue'; import * as chatAPI from '@/api/ai'; diff --git a/packages/frontend/editor-ui/src/stores/posthog.test.ts b/packages/frontend/editor-ui/src/stores/posthog.store.test.ts similarity index 96% rename from packages/frontend/editor-ui/src/stores/posthog.test.ts rename to packages/frontend/editor-ui/src/stores/posthog.store.test.ts index fe1857d726..79fc5a14a5 100644 --- a/packages/frontend/editor-ui/src/stores/posthog.test.ts +++ b/packages/frontend/editor-ui/src/stores/posthog.store.test.ts @@ -20,6 +20,7 @@ export const DEFAULT_POSTHOG_SETTINGS: FrontendSettings['posthog'] = { }; const CURRENT_USER_ID = '1'; const CURRENT_INSTANCE_ID = '456'; +const CURRENT_VERSION_CLI = '1.100.0'; function setSettings(overrides?: Partial) { useSettingsStore().setSettings({ @@ -30,6 +31,7 @@ function setSettings(overrides?: Partial) { } as FrontendSettings); useRootStore().setInstanceId(CURRENT_INSTANCE_ID); + useRootStore().setVersionCli(CURRENT_VERSION_CLI); } function setCurrentUser() { @@ -123,6 +125,7 @@ describe('Posthog store', () => { const userId = `${CURRENT_INSTANCE_ID}#${CURRENT_USER_ID}`; expect(window.posthog?.identify).toHaveBeenCalledWith(userId, { instance_id: CURRENT_INSTANCE_ID, + version_cli: CURRENT_VERSION_CLI, }); }); diff --git a/packages/frontend/editor-ui/src/stores/posthog.store.ts b/packages/frontend/editor-ui/src/stores/posthog.store.ts index 965160c1ef..2c3216efc8 100644 --- a/packages/frontend/editor-ui/src/stores/posthog.store.ts +++ b/packages/frontend/editor-ui/src/stores/posthog.store.ts @@ -81,7 +81,11 @@ export const usePostHog = defineStore('posthog', () => { const identify = () => { const instanceId = rootStore.instanceId; const user = usersStore.currentUser; - const traits: Record = { instance_id: instanceId }; + const versionCli = rootStore.versionCli; + const traits: Record = { + instance_id: instanceId, + version_cli: versionCli, + }; if (user && typeof user.createdAt === 'string') { traits.created_at_timestamp = new Date(user.createdAt).getTime(); diff --git a/packages/frontend/editor-ui/src/stores/users.store.ts b/packages/frontend/editor-ui/src/stores/users.store.ts index d6d92c2dbd..fde5c79be8 100644 --- a/packages/frontend/editor-ui/src/stores/users.store.ts +++ b/packages/frontend/editor-ui/src/stores/users.store.ts @@ -37,7 +37,7 @@ const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Own const _isDefaultUser = (user: IUserResponse | null) => _isInstanceOwner(user) && _isPendingUser(user); -type LoginHook = (user: CurrentUserResponse) => void; +export type LoginHook = (user: CurrentUserResponse) => void; type LogoutHook = () => void; export const useUsersStore = defineStore(STORES.USERS, () => {