From f95602bda08eeb00457952e82c0a0778a905dcc5 Mon Sep 17 00:00:00 2001 From: logonoff Date: Tue, 5 May 2026 13:17:54 -0400 Subject: [PATCH] OCPBUGS-85042: Remove PII from events --- .../src/hooks/__tests__/useTelemetry.spec.ts | 69 ++++++++++++++++--- .../console-shared/src/hooks/useTelemetry.ts | 36 ++++++++-- .../src/listeners/segment.ts | 5 +- 3 files changed, 92 insertions(+), 18 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts index 646f87e9ca9..5de45c06bbc 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/useTelemetry.spec.ts @@ -26,8 +26,16 @@ jest.mock('@console/shared/src/hooks/useUserSettings', () => ({ jest.mock('@console/shared/src/hooks/useUser', () => ({ useUser: jest.fn(() => ({ - user: {}, - userResource: {}, + user: { + username: 'shadowman', + }, + userResource: { + metadata: { + annotations: { + 'toolchain.dev.openshift.com/sso-user-id': 'sandbox-user-id-123', + }, + }, + }, userResourceLoaded: true, userResourceError: null, username: 'testuser', @@ -36,22 +44,15 @@ jest.mock('@console/shared/src/hooks/useUser', () => ({ })), })); -const mockUserResource = {}; - const exampleReturnValue = { - accountMail: undefined, + accountMailDomain: '', clusterId: undefined, clusterType: undefined, consoleVersion: undefined, organizationId: undefined, path: undefined, - userResource: mockUserResource, }; -jest.mock('@console/internal/components/utils/k8s-get-hook', () => ({ - useK8sGet: () => [mockUserResource, true], -})); - const mockUserSettings = useUserSettings as jest.Mock; const useResolvedExtensionsMock = useResolvedExtensions as jest.Mock; @@ -227,6 +228,7 @@ describe('useTelemetry', () => { ...exampleReturnValue, clusterType: 'DEVSANDBOX', consoleVersion: 'x.y.z', + sandboxUserId: 'sandbox-user-id-123', }); }); @@ -353,4 +355,51 @@ describe('useTelemetry', () => { fireTelemetryEvent('test 11'); expect(listener).toHaveBeenCalledTimes(1); }); + + it('anonymizes email to only return the domain', () => { + const email = 'shadowman@redhat.com'; + const expectedDomain = 'redhat.com'; + + window.SERVER_FLAGS = { + ...originServerFlags, + telemetry: { + ACCOUNT_MAIL: email, + }, + }; + updateClusterPropertiesFromTests(); + const { result } = testHook(() => useTelemetry()); + const fireTelemetryEvent = result.current; + fireTelemetryEvent('test 12'); + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith('test 12', { + ...exampleReturnValue, + accountMailDomain: expectedDomain, + }); + + // assert PII is not sent + const callArgs = listener.mock.calls[0][1]; + expect(callArgs.user).toBeUndefined(); + expect(callArgs.userResource).toBeUndefined(); + expect(callArgs.accountMail).toBeUndefined(); + expect(callArgs.sandboxUserId).toBeUndefined(); + }); + + ['invalid-email', 'shadowman@', 'red@hat@redhat.com', '@@', ''].forEach((email) => { + it(`should not extract domains from invalid emails (${email})`, () => { + window.SERVER_FLAGS = { + ...originServerFlags, + telemetry: { + ACCOUNT_MAIL: email, + }, + }; + updateClusterPropertiesFromTests(); + const { result } = testHook(() => useTelemetry()); + const fireTelemetryEvent = result.current; + fireTelemetryEvent('test 12'); + expect(listener).toHaveBeenCalledWith('test 12', { + ...exampleReturnValue, + accountMailDomain: '', + }); + }); + }); }); diff --git a/frontend/packages/console-shared/src/hooks/useTelemetry.ts b/frontend/packages/console-shared/src/hooks/useTelemetry.ts index e318cfe15e6..c42133f8143 100644 --- a/frontend/packages/console-shared/src/hooks/useTelemetry.ts +++ b/frontend/packages/console-shared/src/hooks/useTelemetry.ts @@ -20,12 +20,13 @@ export interface ClusterProperties { clusterType?: string; consoleVersion?: string; organizationId?: string; - accountMail?: string; + accountMailDomain?: string; } export type TelemetryEventProperties = { user?: UserInfo; - userResource?: UserKind; + /** Only sent if in a sandbox cluster */ + sandboxUserId?: string; } & ClusterProperties & Record; @@ -36,6 +37,11 @@ export interface TelemetryEvent { let telemetryEvents: TelemetryEvent[] = []; +const getEmailDomain = (email: string = ''): string => { + const emailParts = email.split('@'); + return emailParts.length === 2 ? emailParts[1] : ''; +}; + export const getClusterProperties = () => { const clusterProperties: ClusterProperties = {}; clusterProperties.clusterId = window.SERVER_FLAGS.telemetry?.CLUSTER_ID; @@ -50,7 +56,7 @@ export const getClusterProperties = () => { clusterProperties.consoleVersion = window.SERVER_FLAGS.releaseVersion || window.SERVER_FLAGS.consoleVersion; clusterProperties.organizationId = window.SERVER_FLAGS.telemetry?.ORGANIZATION_ID; - clusterProperties.accountMail = window.SERVER_FLAGS.telemetry?.ACCOUNT_MAIL; + clusterProperties.accountMailDomain = getEmailDomain(window.SERVER_FLAGS.telemetry?.ACCOUNT_MAIL); return clusterProperties; }; @@ -70,6 +76,20 @@ let clusterProperties = getClusterProperties(); export const updateClusterPropertiesFromTests = () => (clusterProperties = getClusterProperties()); +const injectSandboxProperties = ( + properties: TelemetryEventProperties, + userResource: UserKind, +): TelemetryEventProperties => { + if (clusterProperties.clusterType === 'DEVSANDBOX') { + return { + ...properties, + sandboxUserId: + userResource?.metadata?.annotations?.['toolchain.dev.openshift.com/sso-user-id'], + }; + } + return properties; +}; + export const useTelemetry = () => { // TODO use useDynamicPluginInfo() hook to tell whether all dynamic plugins have been processed // to avoid firing telemetry events multiple times whenever a dynamic plugin loads asynchronously @@ -93,7 +113,10 @@ export const useTelemetry = () => { userResourceIsLoaded ) { telemetryEvents.forEach(({ eventType, event }) => { - extensions.forEach((e) => e.properties.listener(eventType, { ...event, userResource })); + // Sends the telemetry event to all the listeners + extensions.forEach((e) => + e.properties.listener(eventType, injectSandboxProperties(event, userResource)), + ); }); telemetryEvents = []; } @@ -124,7 +147,10 @@ export const useTelemetry = () => { return; } - extensions.forEach((e) => e.properties.listener(eventType, { ...event, userResource })); + // Sends the telemetry event to all the listeners + extensions.forEach((e) => + e.properties.listener(eventType, injectSandboxProperties(event, userResource)), + ); }, [extensions, currentUserPreferenceTelemetryValue, userResource, userResourceIsLoaded], ); diff --git a/frontend/packages/console-telemetry-plugin/src/listeners/segment.ts b/frontend/packages/console-telemetry-plugin/src/listeners/segment.ts index 20f15c7b4cb..a240363a6cb 100644 --- a/frontend/packages/console-telemetry-plugin/src/listeners/segment.ts +++ b/frontend/packages/console-telemetry-plugin/src/listeners/segment.ts @@ -47,7 +47,7 @@ export const eventListener: TelemetryEventListener = async ( switch (eventType) { case 'identify': { - const { user, userResource, ...otherProperties }: TelemetryEventProperties = properties; + const { user, sandboxUserId, ...otherProperties }: TelemetryEventProperties = properties; const clusterId = otherProperties?.clusterId; const organizationId = otherProperties?.organizationId; const username = user?.username; @@ -67,8 +67,7 @@ export const eventListener: TelemetryEventListener = async ( // anonymize user ID if cluster is not a DEVSANDBOX cluster if (getClusterProperties().clusterType === 'DEVSANDBOX') { - processedUserId = - userResource?.metadata?.annotations?.['toolchain.dev.openshift.com/sso-user-id']; + processedUserId = sandboxUserId; } else { processedUserId = await anonymizeId(userId); }