mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
feat(editor): Track IdP role mapping in provisioning telemetry (#29416)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9a91c83a27
commit
40da23f688
|
|
@ -37,6 +37,7 @@ const {
|
|||
mappingMethod,
|
||||
isUserRoleProvisioningChanged,
|
||||
saveProvisioningConfig,
|
||||
trackProvisioningChange,
|
||||
roleAssignmentTransition,
|
||||
storedHasProjectRoles,
|
||||
isDroppingProjectRules,
|
||||
|
|
@ -158,7 +159,7 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false)
|
|||
loginEnabled: ssoStore.isOidcLoginEnabled,
|
||||
authenticationContextClassReference: acrArray,
|
||||
});
|
||||
await saveProvisioningConfig(isDisablingOidcLogin);
|
||||
const provisioningResult = await saveProvisioningConfig(isDisablingOidcLogin);
|
||||
|
||||
// If the user's effective role assignment doesn't include project roles,
|
||||
// discard any project-rule state in the editor (both locally-added and
|
||||
|
|
@ -170,9 +171,12 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false)
|
|||
roleMappingRuleEditorRef.value?.discardProjectRules();
|
||||
}
|
||||
|
||||
if (mappingMethod.value === 'rules_in_n8n') {
|
||||
await roleMappingRuleEditorRef.value?.save();
|
||||
}
|
||||
const ruleSaveResult =
|
||||
mappingMethod.value === 'rules_in_n8n'
|
||||
? await roleMappingRuleEditorRef.value?.save()
|
||||
: undefined;
|
||||
|
||||
trackProvisioningChange(provisioningResult, ruleSaveResult);
|
||||
|
||||
showUserRoleProvisioningDialog.value = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ const {
|
|||
mappingMethod,
|
||||
isUserRoleProvisioningChanged,
|
||||
saveProvisioningConfig,
|
||||
trackProvisioningChange,
|
||||
roleAssignmentTransition,
|
||||
storedHasProjectRoles,
|
||||
isDroppingProjectRules,
|
||||
|
|
@ -244,7 +245,7 @@ const onSave = async (provisioningChangesConfirmed: boolean = false): Promise<bo
|
|||
loginEnabled: samlLoginEnabled.value,
|
||||
});
|
||||
|
||||
await saveProvisioningConfig(isDisablingSamlLogin);
|
||||
const provisioningResult = await saveProvisioningConfig(isDisablingSamlLogin);
|
||||
|
||||
// If the user's effective role assignment doesn't include project roles,
|
||||
// discard any project-rule state in the editor (both locally-added and
|
||||
|
|
@ -256,9 +257,12 @@ const onSave = async (provisioningChangesConfirmed: boolean = false): Promise<bo
|
|||
roleMappingRuleEditorRef.value?.discardProjectRules();
|
||||
}
|
||||
|
||||
if (mappingMethod.value === 'rules_in_n8n') {
|
||||
await roleMappingRuleEditorRef.value?.save();
|
||||
}
|
||||
const ruleSaveResult =
|
||||
mappingMethod.value === 'rules_in_n8n'
|
||||
? await roleMappingRuleEditorRef.value?.save()
|
||||
: undefined;
|
||||
|
||||
trackProvisioningChange(provisioningResult, ruleSaveResult);
|
||||
|
||||
// Update store with saved protocol selection
|
||||
ssoStore.selectedAuthProtocol = SupportedProtocols.SAML;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ import type {
|
|||
} from '@n8n/rest-api-client/api/roleMappingRule';
|
||||
import { useRoleMappingRulesApi } from './useRoleMappingRulesApi';
|
||||
|
||||
export type RoleMappingRulesSaveResult = {
|
||||
createdCount: number;
|
||||
deletedCount: number;
|
||||
instanceRuleCount: number;
|
||||
projectRuleCount: number;
|
||||
};
|
||||
|
||||
function generateLocalId(): string {
|
||||
return `local-${crypto.randomUUID()}`;
|
||||
}
|
||||
|
|
@ -127,7 +134,7 @@ export function useRoleMappingRules() {
|
|||
serverProjectRuleIds = new Set();
|
||||
}
|
||||
|
||||
async function save() {
|
||||
async function save(): Promise<RoleMappingRulesSaveResult> {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
// Defensive re-sync: the server may have removed rules between the
|
||||
|
|
@ -173,6 +180,13 @@ export function useRoleMappingRules() {
|
|||
}
|
||||
|
||||
await loadRules();
|
||||
|
||||
return {
|
||||
createdCount: createRules.length,
|
||||
deletedCount: deleteIds.length,
|
||||
instanceRuleCount: instanceRules.value.length,
|
||||
projectRuleCount: projectRules.value.length,
|
||||
};
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,12 +279,14 @@ describe('useUserRoleProvisioningForm', () => {
|
|||
const { roleAssignment, saveProvisioningConfig } = useUserRoleProvisioningForm('oidc');
|
||||
await vi.waitFor(() => expect(roleAssignment.value).toBe('instance'));
|
||||
|
||||
await saveProvisioningConfig(false);
|
||||
const result = await saveProvisioningConfig(false);
|
||||
|
||||
expect(provisioningApi.saveProvisioningConfig).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ deleteProjectRules: true }),
|
||||
);
|
||||
// Reports a change so the caller fires telemetry — the API call did happen.
|
||||
expect(result).toEqual({ configChanged: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import type {
|
|||
import { type SupportedProtocolType } from '../../sso.store';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import type { RoleMappingRulesSaveResult } from './useRoleMappingRules';
|
||||
|
||||
type TelemetrySettingValue =
|
||||
| 'disabled'
|
||||
| 'instance_role'
|
||||
| 'instance_and_project_roles'
|
||||
| 'expression_based';
|
||||
type TelemetryAssignmentMethod = 'disabled' | 'instance_role' | 'instance_and_project_roles';
|
||||
|
||||
type TelemetryRoleMappingMethod = 'idp_rule_mapping' | 'n8n_rule_mapping';
|
||||
|
||||
export type RoleAssignmentTransitionType = 'none' | 'backup' | 'switchToManual';
|
||||
|
||||
|
|
@ -69,16 +68,20 @@ function getProvisioningConfigFromDropdowns(
|
|||
};
|
||||
}
|
||||
|
||||
function getTelemetrySettingValue(
|
||||
function getTelemetryAssignmentMethod(
|
||||
roleAssignment: RoleAssignmentSetting,
|
||||
mappingMethod: RoleMappingMethodSetting,
|
||||
): TelemetrySettingValue {
|
||||
): TelemetryAssignmentMethod {
|
||||
if (roleAssignment === 'manual') return 'disabled';
|
||||
if (mappingMethod === 'rules_in_n8n') return 'expression_based';
|
||||
if (roleAssignment === 'instance_and_project') return 'instance_and_project_roles';
|
||||
return 'instance_role';
|
||||
}
|
||||
|
||||
function getTelemetryRoleMappingMethod(
|
||||
mappingMethod: RoleMappingMethodSetting,
|
||||
): TelemetryRoleMappingMethod {
|
||||
return mappingMethod === 'rules_in_n8n' ? 'n8n_rule_mapping' : 'idp_rule_mapping';
|
||||
}
|
||||
|
||||
export function useUserRoleProvisioningForm(protocol: SupportedProtocolType) {
|
||||
const provisioningStore = useUserRoleProvisioningStore();
|
||||
const telemetry = useTelemetry();
|
||||
|
|
@ -98,15 +101,28 @@ export function useUserRoleProvisioningForm(protocol: SupportedProtocolType) {
|
|||
);
|
||||
});
|
||||
|
||||
const sendTrackingEventForUserProvisioning = (updatedSetting: TelemetrySettingValue) => {
|
||||
const trackProvisioningChange = (
|
||||
provisioningResult: { configChanged: boolean },
|
||||
ruleSaveResult: RoleMappingRulesSaveResult | undefined,
|
||||
) => {
|
||||
const rulesChanged =
|
||||
(ruleSaveResult?.createdCount ?? 0) > 0 || (ruleSaveResult?.deletedCount ?? 0) > 0;
|
||||
|
||||
if (!provisioningResult.configChanged && !rulesChanged) return;
|
||||
|
||||
telemetry.track('User updated provisioning settings', {
|
||||
instance_id: useRootStore().instanceId,
|
||||
authentication_method: protocol,
|
||||
updated_setting: updatedSetting,
|
||||
assignment_method: getTelemetryAssignmentMethod(roleAssignment.value),
|
||||
role_mapping_method: getTelemetryRoleMappingMethod(mappingMethod.value),
|
||||
instance_rule_count: ruleSaveResult?.instanceRuleCount ?? 0,
|
||||
project_rule_count: ruleSaveResult?.projectRuleCount ?? 0,
|
||||
});
|
||||
};
|
||||
|
||||
const saveProvisioningConfig = async (isDisablingSso: boolean): Promise<void> => {
|
||||
const saveProvisioningConfig = async (
|
||||
isDisablingSso: boolean,
|
||||
): Promise<{ configChanged: boolean }> => {
|
||||
const effectiveRoleAssignment: RoleAssignmentSetting = isDisablingSso
|
||||
? 'manual'
|
||||
: roleAssignment.value;
|
||||
|
|
@ -121,12 +137,12 @@ export function useUserRoleProvisioningForm(protocol: SupportedProtocolType) {
|
|||
const shouldDeleteProjectRules = effectiveRoleAssignment !== 'instance_and_project';
|
||||
|
||||
const stored = storedValues.value;
|
||||
const configUnchanged =
|
||||
effectiveRoleAssignment === stored.roleAssignment &&
|
||||
effectiveMappingMethod === stored.mappingMethod;
|
||||
const configChanged =
|
||||
effectiveRoleAssignment !== stored.roleAssignment ||
|
||||
effectiveMappingMethod !== stored.mappingMethod;
|
||||
|
||||
if (configUnchanged && !shouldDeleteProjectRules) {
|
||||
return;
|
||||
if (!configChanged && !shouldDeleteProjectRules) {
|
||||
return { configChanged: false };
|
||||
}
|
||||
|
||||
await provisioningStore.saveProvisioningConfig({
|
||||
|
|
@ -141,9 +157,11 @@ export function useUserRoleProvisioningForm(protocol: SupportedProtocolType) {
|
|||
storedHasProjectRules.value = false;
|
||||
}
|
||||
|
||||
sendTrackingEventForUserProvisioning(
|
||||
getTelemetrySettingValue(effectiveRoleAssignment, effectiveMappingMethod),
|
||||
);
|
||||
// `shouldDeleteProjectRules` is enough to trigger a server call (to wipe
|
||||
// stale project rules) even when dropdown values haven't changed. The
|
||||
// caller relies on this flag to fire telemetry whenever a save actually
|
||||
// hit the backend.
|
||||
return { configChanged: configChanged || shouldDeleteProjectRules };
|
||||
};
|
||||
|
||||
const roleAssignmentTransition = computed<RoleAssignmentTransitionType>(() => {
|
||||
|
|
@ -201,6 +219,7 @@ export function useUserRoleProvisioningForm(protocol: SupportedProtocolType) {
|
|||
mappingMethod,
|
||||
isUserRoleProvisioningChanged,
|
||||
saveProvisioningConfig,
|
||||
trackProvisioningChange,
|
||||
roleAssignmentTransition,
|
||||
storedHasProjectRoles,
|
||||
isDroppingProjectRules,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user