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:
Guillaume Jacquart 2026-04-29 17:12:48 +02:00 committed by GitHub
parent 9a91c83a27
commit 40da23f688
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 30 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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 });
});
});

View File

@ -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,