From 14c9973b30aebcff9d146be245e9c34957f00509 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 1 Jun 2026 11:24:03 -0700 Subject: [PATCH] [AI-FSSDK] [FSSDK-12721] Skip ODP identify event for single-identifier users --- lib/odp/odp_manager.spec.ts | 74 ++++++++++++++++++++++++++++++++++--- lib/odp/odp_manager.ts | 23 +++++++++++- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts index 9ae0daf69..0ef982804 100644 --- a/lib/odp/odp_manager.spec.ts +++ b/lib/odp/odp_manager.spec.ts @@ -618,7 +618,7 @@ describe('DefaultOdpManager', () => { expect(identifiers).toEqual(new Map([['fs_user_id', 'user'], ['vuid', 'vuid_a']])); }); - it('sends identified event when called with just fs_user_id in first parameter', async () => { + it('does not send identified event when called with just fs_user_id and no stored vuid', async () => { const eventManager = getMockOdpEventManager(); eventManager.onRunning.mockReturnValue(Promise.resolve()); @@ -634,12 +634,10 @@ describe('DefaultOdpManager', () => { await odpManager.onRunning(); odpManager.identifyUser('user'); - expect(mockSendEvents).toHaveBeenCalledOnce(); - const { identifiers } = mockSendEvents.mock.calls[0][0]; - expect(identifiers).toEqual(new Map([['fs_user_id', 'user']])); + expect(mockSendEvents).not.toHaveBeenCalled(); }); - it('sends identified event when called with just vuid in first parameter', async () => { + it('does not send identified event when called with just vuid in first parameter and no stored vuid', async () => { const eventManager = getMockOdpEventManager(); eventManager.onRunning.mockReturnValue(Promise.resolve()); @@ -655,9 +653,73 @@ describe('DefaultOdpManager', () => { await odpManager.onRunning(); odpManager.identifyUser('vuid_a'); + expect(mockSendEvents).not.toHaveBeenCalled(); + }); + + it('sends identified event when called with fs_user_id and stored vuid is available', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + // Set stored vuid first so identifyUser has 2 identifiers + odpManager.setVuid('vuid_stored'); + + // Clear the sendEvent calls from setVuid's client_initialized event + mockSendEvents.mockClear(); + + odpManager.identifyUser('user'); expect(mockSendEvents).toHaveBeenCalledOnce(); const { identifiers } = mockSendEvents.mock.calls[0][0]; - expect(identifiers).toEqual(new Map([['vuid', 'vuid_a']])); + expect(identifiers).toEqual(new Map([['fs_user_id', 'user'], ['vuid', 'vuid_stored']])); + }); + + it('does not count empty identifier values toward the minimum', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + // Call with fs_user_id and empty vuid - should not send (only 1 valid identifier) + odpManager.identifyUser('user', ''); + expect(mockSendEvents).not.toHaveBeenCalled(); + }); + + it('logs debug message when skipping identify event due to single identifier', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const logger = getMockLogger(); + const odpManager = new DefaultOdpManager({ + logger, + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.identifyUser('user'); + expect(logger.debug).toHaveBeenCalledWith('ODP identify event is not dispatched (only one identifier provided).'); }); it('should reject onRunning() if stopped in new state', async () => { diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 7525d0efb..c0404f0ab 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -212,7 +212,7 @@ export class DefaultOdpManager extends BaseService implements OdpManager { identifyUser(userId: string, vuid?: string): void { const identifiers = new Map(); - + let finalUserId: Maybe = userId; let finalVuid: Maybe = vuid; @@ -229,7 +229,26 @@ export class DefaultOdpManager extends BaseService implements OdpManager { identifiers.set(ODP_USER_KEY.FS_USER_ID, finalUserId); } - const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, identifiers); + // Include stored vuid if not already present to get accurate identifier count + if (!identifiers.has(ODP_USER_KEY.VUID) && this.vuid) { + identifiers.set(ODP_USER_KEY.VUID, this.vuid); + } + + // Filter out identifiers with empty/null values + const validIdentifiers = new Map(); + identifiers.forEach((value, key) => { + if (value) { + validIdentifiers.set(key, value); + } + }); + + // Only send identify event when there are 2+ valid identifiers to link + if (validIdentifiers.size < 2) { + this.logger?.debug('ODP identify event is not dispatched (only one identifier provided).'); + return; + } + + const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, validIdentifiers); this.sendEvent(event); }