fix(editor): Include n8n version in user identify call (no-changelog) (#19306)

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
Elias Meire
2025-09-10 16:27:44 +02:00
committed by GitHub
parent 5c6094dfd7
commit d3ee6512a9
7 changed files with 90 additions and 82 deletions

View File

@@ -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 { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.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 { 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 { 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 { AxiosError } from 'axios';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import { mockedStore, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; import { setActivePinia } from 'pinia';
import { STORES } from '@n8n/stores'; import { mock } from 'vitest-mock-extended';
import { useSSOStore } from '@/stores/sso.store'; import { telemetry } from './plugins/telemetry';
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';
const showMessage = vi.fn(); const showMessage = vi.fn();
const showToast = 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', () => { describe('Init', () => {
let settingsStore: ReturnType<typeof useSettingsStore>; let settingsStore: ReturnType<typeof mockedStore<typeof useSettingsStore>>;
let cloudPlanStore: ReturnType<typeof mockedStore<typeof useCloudPlanStore>>; let cloudPlanStore: ReturnType<typeof mockedStore<typeof useCloudPlanStore>>;
let sourceControlStore: ReturnType<typeof useSourceControlStore>; let sourceControlStore: ReturnType<typeof mockedStore<typeof useSourceControlStore>>;
let usersStore: ReturnType<typeof useUsersStore>; let usersStore: ReturnType<typeof mockedStore<typeof useUsersStore>>;
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>; let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
let versionsStore: ReturnType<typeof useVersionsStore>; let versionsStore: ReturnType<typeof mockedStore<typeof useVersionsStore>>;
let ssoStore: ReturnType<typeof useSSOStore>; let ssoStore: ReturnType<typeof mockedStore<typeof useSSOStore>>;
let uiStore: ReturnType<typeof useUIStore>; let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
let rootStore: ReturnType<typeof mockedStore<typeof useRootStore>>;
beforeEach(() => { beforeEach(() => {
setActivePinia( setActivePinia(
@@ -57,15 +56,16 @@ describe('Init', () => {
}), }),
); );
settingsStore = useSettingsStore(); settingsStore = mockedStore(useSettingsStore);
cloudPlanStore = mockedStore(useCloudPlanStore); cloudPlanStore = mockedStore(useCloudPlanStore);
sourceControlStore = useSourceControlStore(); sourceControlStore = mockedStore(useSourceControlStore);
nodeTypesStore = useNodeTypesStore(); nodeTypesStore = mockedStore(useNodeTypesStore);
usersStore = useUsersStore(); usersStore = mockedStore(useUsersStore);
versionsStore = useVersionsStore(); versionsStore = mockedStore(useVersionsStore);
versionsStore = useVersionsStore(); versionsStore = mockedStore(useVersionsStore);
ssoStore = useSSOStore(); ssoStore = mockedStore(useSSOStore);
uiStore = useUIStore(); uiStore = mockedStore(useUIStore);
rootStore = mockedStore(useRootStore);
}); });
describe('initializeCore()', () => { describe('initializeCore()', () => {
@@ -118,6 +118,19 @@ describe('Init', () => {
expect(registerLogoutHookSpy).toHaveBeenCalled(); expect(registerLogoutHookSpy).toHaveBeenCalled();
}); });
it('should correctly identify the user for telemetry', async () => {
const telemetryIdentifySpy = vi.spyOn(telemetry, 'identify');
usersStore.registerLoginHook.mockImplementation((hook) =>
hook(mock<CurrentUserResponse>({ 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 () => { it('should initialize ssoStore with settings SSO configuration', async () => {
const saml = { loginEnabled: true, loginLabel: '' }; const saml = { loginEnabled: true, loginLabel: '' };
const ldap = { loginEnabled: false, loginLabel: '' }; const ldap = { loginEnabled: false, loginLabel: '' };
@@ -155,12 +168,10 @@ describe('Init', () => {
describe('initializeAuthenticatedFeatures()', () => { describe('initializeAuthenticatedFeatures()', () => {
beforeEach(() => { beforeEach(() => {
vi.spyOn(settingsStore, 'isCloudDeployment', 'get').mockReturnValue(true); settingsStore.isCloudDeployment = true;
vi.spyOn(settingsStore, 'isTemplatesEnabled', 'get').mockReturnValue(true); settingsStore.isTemplatesEnabled = true;
vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true); sourceControlStore.isEnterpriseSourceControlEnabled = true;
vi.mocked(useRootStore).mockReturnValue({ defaultLocale: 'es' } as ReturnType< rootStore.defaultLocale = 'es';
typeof useRootStore
>);
}); });
afterEach(() => { afterEach(() => {
@@ -172,9 +183,7 @@ describe('Init', () => {
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions');
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType< usersStore.currentUser = null;
typeof useUsersStore
>);
await initializeAuthenticatedFeatures(false); await initializeAuthenticatedFeatures(false);
expect(cloudStoreSpy).not.toHaveBeenCalled(); expect(cloudStoreSpy).not.toHaveBeenCalled();
@@ -188,9 +197,7 @@ describe('Init', () => {
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions');
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< usersStore.currentUser = mock<IUser>({ id: '123', globalScopes: ['*'] });
typeof useUsersStore
>);
await initializeAuthenticatedFeatures(false); await initializeAuthenticatedFeatures(false);
@@ -211,9 +218,7 @@ describe('Init', () => {
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions');
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< usersStore.currentUser = mock<IUser>({ id: '123', globalScopes: ['*'] });
typeof useUsersStore
>);
await initializeAuthenticatedFeatures(false); await initializeAuthenticatedFeatures(false);
@@ -230,9 +235,7 @@ describe('Init', () => {
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences'); const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders'); const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions'); const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions');
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< usersStore.currentUser = mock<IUser>({ id: '123', globalScopes: ['*'] });
typeof useUsersStore
>);
await initializeAuthenticatedFeatures(false); await initializeAuthenticatedFeatures(false);
@@ -244,9 +247,7 @@ describe('Init', () => {
it('should handle source control initialization error', async () => { it('should handle source control initialization error', async () => {
vi.spyOn(cloudPlanStore, 'initialize').mockResolvedValue(); vi.spyOn(cloudPlanStore, 'initialize').mockResolvedValue();
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< usersStore.currentUser = mock<IUser>({ id: '123', globalScopes: ['*'] });
typeof useUsersStore
>);
vi.spyOn(sourceControlStore, 'getPreferences').mockRejectedValueOnce( vi.spyOn(sourceControlStore, 'getPreferences').mockRejectedValueOnce(
new AxiosError('Something went wrong', '404'), new AxiosError('Something went wrong', '404'),
); );

View File

@@ -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 SourceControlInitializationErrorMessage from '@/components/SourceControlInitializationErrorMessage.vue';
import { useSSOStore } from '@/stores/sso.store'; import { useExternalHooks } from '@/composables/useExternalHooks';
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 { useTelemetry } from '@/composables/useTelemetry'; 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 { import {
registerModuleModals,
registerModuleProjectTabs, registerModuleProjectTabs,
registerModuleResources, registerModuleResources,
registerModuleModals,
} from '@/moduleInitializer/moduleInitializer'; } 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 = { export const state = {
initialized: false, initialized: false,
@@ -209,7 +209,7 @@ function registerAuthenticationHooks() {
usersStore.registerLoginHook((user) => { usersStore.registerLoginHook((user) => {
RBACStore.setGlobalScopes(user.globalScopes ?? []); RBACStore.setGlobalScopes(user.globalScopes ?? []);
telemetry.identify(rootStore.instanceId, user.id); telemetry.identify(rootStore.instanceId, user.id, rootStore.versionCli);
postHogStore.init(user.featureFlags); postHogStore.init(user.featureFlags);
npsSurveyStore.setupNpsSurveyOnLogin(user.id, user.settings); npsSurveyStore.setupNpsSurveyOnLogin(user.id, user.settings);
void settingsStore.getModuleSettings(); void settingsStore.getModuleSettings();

View File

@@ -13,7 +13,7 @@ import { usePostHog } from './posthog.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { defaultSettings } from '../__tests__/defaults'; import { defaultSettings } from '../__tests__/defaults';
import merge from 'lodash/merge'; 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 { VIEWS } from '@/constants';
import { reactive } from 'vue'; import { reactive } from 'vue';
import * as chatAPI from '@/api/ai'; import * as chatAPI from '@/api/ai';

View File

@@ -8,7 +8,7 @@ import { usePostHog } from './posthog.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { defaultSettings } from '../__tests__/defaults'; import { defaultSettings } from '../__tests__/defaults';
import merge from 'lodash/merge'; 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 { WORKFLOW_BUILDER_EXPERIMENT, DEFAULT_NEW_WORKFLOW_NAME } from '@/constants';
import { reactive } from 'vue'; import { reactive } from 'vue';
import * as chatAPI from '@/api/ai'; import * as chatAPI from '@/api/ai';

View File

@@ -20,6 +20,7 @@ export const DEFAULT_POSTHOG_SETTINGS: FrontendSettings['posthog'] = {
}; };
const CURRENT_USER_ID = '1'; const CURRENT_USER_ID = '1';
const CURRENT_INSTANCE_ID = '456'; const CURRENT_INSTANCE_ID = '456';
const CURRENT_VERSION_CLI = '1.100.0';
function setSettings(overrides?: Partial<FrontendSettings>) { function setSettings(overrides?: Partial<FrontendSettings>) {
useSettingsStore().setSettings({ useSettingsStore().setSettings({
@@ -30,6 +31,7 @@ function setSettings(overrides?: Partial<FrontendSettings>) {
} as FrontendSettings); } as FrontendSettings);
useRootStore().setInstanceId(CURRENT_INSTANCE_ID); useRootStore().setInstanceId(CURRENT_INSTANCE_ID);
useRootStore().setVersionCli(CURRENT_VERSION_CLI);
} }
function setCurrentUser() { function setCurrentUser() {
@@ -123,6 +125,7 @@ describe('Posthog store', () => {
const userId = `${CURRENT_INSTANCE_ID}#${CURRENT_USER_ID}`; const userId = `${CURRENT_INSTANCE_ID}#${CURRENT_USER_ID}`;
expect(window.posthog?.identify).toHaveBeenCalledWith(userId, { expect(window.posthog?.identify).toHaveBeenCalledWith(userId, {
instance_id: CURRENT_INSTANCE_ID, instance_id: CURRENT_INSTANCE_ID,
version_cli: CURRENT_VERSION_CLI,
}); });
}); });

View File

@@ -81,7 +81,11 @@ export const usePostHog = defineStore('posthog', () => {
const identify = () => { const identify = () => {
const instanceId = rootStore.instanceId; const instanceId = rootStore.instanceId;
const user = usersStore.currentUser; const user = usersStore.currentUser;
const traits: Record<string, string | number> = { instance_id: instanceId }; const versionCli = rootStore.versionCli;
const traits: Record<string, string | number> = {
instance_id: instanceId,
version_cli: versionCli,
};
if (user && typeof user.createdAt === 'string') { if (user && typeof user.createdAt === 'string') {
traits.created_at_timestamp = new Date(user.createdAt).getTime(); traits.created_at_timestamp = new Date(user.createdAt).getTime();

View File

@@ -37,7 +37,7 @@ const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Own
const _isDefaultUser = (user: IUserResponse | null) => const _isDefaultUser = (user: IUserResponse | null) =>
_isInstanceOwner(user) && _isPendingUser(user); _isInstanceOwner(user) && _isPendingUser(user);
type LoginHook = (user: CurrentUserResponse) => void; export type LoginHook = (user: CurrentUserResponse) => void;
type LogoutHook = () => void; type LogoutHook = () => void;
export const useUsersStore = defineStore(STORES.USERS, () => { export const useUsersStore = defineStore(STORES.USERS, () => {