From d5da516dd362d31f8017be82ddc528670a2f7d1b Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Wed, 29 Apr 2026 00:29:56 +0100 Subject: [PATCH] Updated UUA naming --- README.md | 14 +- react-example/src/index.tsx | 2 +- react-example/src/indexWithoutJWT.tsx | 2 +- src/authorization/authorization.test.ts | 2 +- src/authorization/authorization.ts | 21 +- .../authorization.unknown.test.ts | 2 +- src/commerce/commerce.ts | 6 +- src/constants.ts | 8 +- src/events/events.ts | 4 +- .../tests/consentTracking.test.ts | 2 +- .../tests/unknownUserEventManager.test.ts | 238 ++++++++++-------- .../tests/userMergeScenarios.test.ts | 38 +-- .../tests/userUpdate.test.ts | 2 +- .../validateCustomEventUserUpdateAPI.test.ts | 8 +- .../unknownUserEventManager.ts | 41 +-- src/users/users.ts | 4 +- src/utils/commonFunctions.test.ts | 37 +++ src/utils/commonFunctions.ts | 24 +- src/utils/config.test.ts | 30 +++ src/utils/config.ts | 22 +- 20 files changed, 336 insertions(+), 171 deletions(-) create mode 100644 src/utils/commonFunctions.test.ts diff --git a/README.md b/README.md index d6adb437..c173a3cf 100644 --- a/README.md +++ b/README.md @@ -1944,7 +1944,7 @@ Configuration options to pass to [`initializeWithConfig`](#initializewithconfig) type Options = { logLevel: 'none' | 'verbose'; baseURL: string; - enableUnknownActivation: boolean; + enableUnknownUserActivation: boolean; isEuIterableService: boolean; dangerouslyAllowJsPopups: boolean; eventThresholdLimit?: number; @@ -2460,7 +2460,7 @@ import { initializeWithConfig } from '@iterable/web-sdk'; const { setEmail, setUserID, setVisitorUsageTracked, clearVisitorEventsAndUserData } = initializeWithConfig({ authToken: '', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { mergeOnUnknownToKnown: true, // default replayOnVisitorToKnown: true // default @@ -2487,7 +2487,7 @@ const { setEmail, setUserID, setVisitorUsageTracked, clearVisitorEventsAndUserDa ### Persistence and restoration across sessions - Storage: the unknown user id is stored in `localStorage` under a project-scoped key. It has no TTL and persists across reloads and restarts until explicitly cleared. -- Automatic restoration: on initialize (with `enableUnknownActivation: true`), the SDK restores the unknown id from storage and automatically applies it to outgoing requests for supported endpoints. +- Automatic restoration: on initialize (with `enableUnknownUserActivation: true`), the SDK restores the unknown id from storage and automatically applies it to outgoing requests for supported endpoints. - JWT mode: when using a JWT-enabled API key and consent is present, the SDK generates a JWT for the restored unknown id and attaches it so requests are authenticated. - Lifecycle end: the unknown id is cleared when you identify (merge then clear), revoke consent via `setVisitorUsageTracked(false)`, call `clearVisitorEventsAndUserData()`, or when browser storage is cleared. @@ -2495,7 +2495,9 @@ const { setEmail, setUserID, setVisitorUsageTracked, clearVisitorEventsAndUserDa ```ts type Options = { - enableUnknownActivation: boolean; // Enable UUA (default: false) + enableUnknownUserActivation: boolean; // Enable UUA (default: false) + /** @deprecated Use `enableUnknownUserActivation` instead. */ + enableUnknownActivation?: boolean; eventThresholdLimit?: number; // Queue flush threshold (default provided by SDK) onUnknownUserCreated?: (userId: string) => void; // Callback when unknown user id is created identityResolution?: { @@ -2509,7 +2511,7 @@ type Options = { ### Logic flow (what happens under the hood) 1. Initialization - - When `enableUnknownActivation` is true, SDK fetches unknown user criteria and starts an unknown session. + - When `enableUnknownUserActivation` is true, SDK fetches unknown user criteria and starts an unknown session. - If an unknown user id exists in storage, SDK restores it and authenticates appropriately. 2. Event collection (unknown) - When consent is set via `setVisitorUsageTracked(true)`, SDK queues unknown events locally. @@ -2521,7 +2523,7 @@ type Options = { 4. Replay (optional) - If `replayOnVisitorToKnown` is true, SDK replays queued unknown events under the known identity and clears the unknown queue. 5. Cleanup - - After merge attempt (successful or skipped), SDK clears the stored unknown user id and related anonymous state. + - After merge attempt (successful or skipped), SDK clears the stored unknown user id and related unknown state. Notes: - UUA works for Events, In-App, Embedded, Commerce, and Users APIs; the SDK ensures requests are authenticated correctly pre/post identification. diff --git a/react-example/src/index.tsx b/react-example/src/index.tsx index 511fd035..1ddfd967 100644 --- a/react-example/src/index.tsx +++ b/react-example/src/index.tsx @@ -59,7 +59,7 @@ const HomeLink = styled(Link)` configOptions: { isEuIterableService: false, dangerouslyAllowJsPopups: true, - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true diff --git a/react-example/src/indexWithoutJWT.tsx b/react-example/src/indexWithoutJWT.tsx index 38ba196e..c328c2ad 100644 --- a/react-example/src/indexWithoutJWT.tsx +++ b/react-example/src/indexWithoutJWT.tsx @@ -46,7 +46,7 @@ const HomeLink = styled(Link)` configOptions: { isEuIterableService: false, dangerouslyAllowJsPopups: true, - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true diff --git a/src/authorization/authorization.test.ts b/src/authorization/authorization.test.ts index 4cf8bbe1..756b6c85 100644 --- a/src/authorization/authorization.test.ts +++ b/src/authorization/authorization.test.ts @@ -30,7 +30,7 @@ jest.mock('../utils/config', () => ({ ...jest.requireActual('../utils/config'), default: { getConfig: jest.fn((key) => { - if (key === 'enableUnknownActivation') { + if (key === 'enableUnknownUserActivation') { return false; } return undefined; diff --git a/src/authorization/authorization.ts b/src/authorization/authorization.ts index 24a6bdb7..00b5ba13 100644 --- a/src/authorization/authorization.ts +++ b/src/authorization/authorization.ts @@ -7,6 +7,7 @@ import { IS_PRODUCTION, STATIC_HEADERS, SHARED_PREF_UNKNOWN_USER_ID, + LEGACY_SHARED_PREF_UNKNOWN_USER_ID, RouteConfig, SHARED_PREFS_CRITERIA, SHARED_PREF_CONSENT_TIMESTAMP, @@ -14,6 +15,7 @@ import { SHARED_PREF_USER_ID, RETRY_USER_ATTEMPTS } from '../constants'; +import { migrateLegacyKey } from '../utils/commonFunctions'; import { UnknownUserMerge } from '../unknownUserTracking/unknownUserMerge'; import { UnknownUserEventManager, @@ -227,7 +229,7 @@ const clearUnknownUser = () => { }; const getUnknownUserId = () => { - if (config.getConfig('enableUnknownActivation')) { + if (config.getConfig('enableUnknownUserActivation')) { const unknownUser = localStorage.getItem(SHARED_PREF_UNKNOWN_USER_ID); return unknownUser === undefined ? null : unknownUser; } @@ -341,7 +343,7 @@ const initializeEmailUser = (email: string) => { }; const syncEvents = () => { - if (config.getConfig('enableUnknownActivation')) { + if (config.getConfig('enableUnknownUserActivation')) { unknownUserManager.syncEvents(); } }; @@ -350,7 +352,7 @@ const handleConsentTracking = ( isUserKnown = false, isMergeOperation = false ) => { - if (config.getConfig('enableUnknownActivation')) { + if (config.getConfig('enableUnknownUserActivation')) { unknownUserManager.handleConsentTracking(isUserKnown, isMergeOperation); } }; @@ -396,6 +398,11 @@ export function initialize( ) { apiKey = authToken; generateJWTGlobal = generateJWT; + // One-shot migration from the legacy non-prefixed unknown user id key. + migrateLegacyKey( + LEGACY_SHARED_PREF_UNKNOWN_USER_ID, + SHARED_PREF_UNKNOWN_USER_ID + ); const logLevel = config.getConfig('logLevel'); if (!generateJWT && IS_PRODUCTION) { /* only let people use non-JWT mode if running the app locally */ @@ -485,7 +492,7 @@ export function initialize( const enableUnknownTracking = () => { try { - if (config.getConfig('enableUnknownActivation')) { + if (config.getConfig('enableUnknownUserActivation')) { unknownUserManager.getUnknownCriteria(); unknownUserManager.updateUnknownSession(); const unknownUserId = getUnknownUserId(); @@ -504,14 +511,16 @@ export function initialize( isEmail: boolean, merge?: boolean ): Promise<{ success: boolean; mergePerformed: boolean }> => { - const enableUnknownActivation = config.getConfig('enableUnknownActivation'); + const enableUnknownUserActivation = config.getConfig( + 'enableUnknownUserActivation' + ); const destinationUserId = isEmail ? null : emailOrUserId; const destinationEmail = isEmail ? emailOrUserId : null; // Only merge if there's an unknown user that was successfully created via /session const unknownUserId = getUnknownUserId(); - if (unknownUserId !== null && merge && enableUnknownActivation) { + if (unknownUserId !== null && merge && enableUnknownUserActivation) { const unknownUserMerge = new UnknownUserMerge(); try { await unknownUserMerge.mergeUnknownUser( diff --git a/src/authorization/authorization.unknown.test.ts b/src/authorization/authorization.unknown.test.ts index b866d35b..f6d5bd66 100644 --- a/src/authorization/authorization.unknown.test.ts +++ b/src/authorization/authorization.unknown.test.ts @@ -26,7 +26,7 @@ import { updateUser } from '../users'; jest.mock('../utils/config', () => { const getConfig = (key: string) => { - if (key === 'enableUnknownActivation') return true; + if (key === 'enableUnknownUserActivation') return true; if (key === 'baseURL') return 'https://api.iterable.com'; if (key === 'logLevel') return 'none'; if (key === 'identityResolution') { diff --git a/src/commerce/commerce.ts b/src/commerce/commerce.ts index f09c2524..36352863 100644 --- a/src/commerce/commerce.ts +++ b/src/commerce/commerce.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { ENDPOINTS, AUA_WARNING } from '../constants'; +import { ENDPOINTS, UUA_WARNING } from '../constants'; import { baseIterableRequest } from '../request'; import { TrackPurchaseRequestParams, UpdateCartRequestParams } from './types'; import { IterableResponse } from '../types'; @@ -16,7 +16,7 @@ export const updateCart = (payload: UpdateCartRequestParams) => { if (canTrackUnknownUser()) { const unknownUserEventManager = new UnknownUserEventManager(); unknownUserEventManager.trackUnknownUpdateCart(payload); - return Promise.reject(AUA_WARNING); + return Promise.reject(UUA_WARNING); } return baseIterableRequest({ @@ -44,7 +44,7 @@ export const trackPurchase = (payload: TrackPurchaseRequestParams) => { if (canTrackUnknownUser()) { const unknownUserEventManager = new UnknownUserEventManager(); unknownUserEventManager.trackUnknownPurchaseEvent(payload); - return Promise.reject(AUA_WARNING); + return Promise.reject(UUA_WARNING); } return baseIterableRequest({ diff --git a/src/constants.ts b/src/constants.ts index e86bb7f5..f097d0ba 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -286,7 +286,8 @@ export const SHARED_PREFS_EVENT_LIST_KEY = 'itbl_event_list'; export const SHARED_PREFS_USER_UPDATE_OBJECT_KEY = 'itbl_user_update_object'; export const SHARED_PREFS_CRITERIA = 'criteria'; export const SHARED_PREFS_UNKNOWN_SESSIONS = 'itbl_unknown_sessions'; -export const SHARED_PREF_UNKNOWN_USER_ID = 'unknown_userId'; +export const SHARED_PREF_UNKNOWN_USER_ID = 'itbl_userid_unknown'; +export const LEGACY_SHARED_PREF_UNKNOWN_USER_ID = 'unknown_userId'; export const SHARED_PREF_UNKNOWN_USAGE_TRACKED = 'itbl_unknown_usage_tracked'; export const SHARED_PREF_CONSENT_TIMESTAMP = 'itbl_consent_timestamp'; export const SHARED_PREF_USER_TOKEN = 'itbl_auth_token'; @@ -313,6 +314,9 @@ export const PURCHASE_ITEM_PREFIX = `${PURCHASE_ITEM}.`; export const INITIALIZE_ERROR = new Error( 'Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods' ); -export const AUA_WARNING = new Error( +export const UUA_WARNING = new Error( 'This event was stored locally because you have Unknown User Activation enabled. If this was unintentional, please check your SDK configuration settings.' ); + +/** @deprecated Use `UUA_WARNING` instead. */ +export const AUA_WARNING = UUA_WARNING; diff --git a/src/events/events.ts b/src/events/events.ts index adfabcbe..b1927f2e 100644 --- a/src/events/events.ts +++ b/src/events/events.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { ENDPOINTS, AUA_WARNING } from '../constants'; +import { ENDPOINTS, UUA_WARNING } from '../constants'; import { baseIterableRequest } from '../request'; import { InAppTrackRequestParams } from './inapp/types'; import { IterableResponse } from '../types'; @@ -14,7 +14,7 @@ export const track = (payload: InAppTrackRequestParams) => { if (canTrackUnknownUser()) { const unknownUserEventManager = new UnknownUserEventManager(); unknownUserEventManager.trackUnknownEvent(payload); - return Promise.reject(AUA_WARNING); + return Promise.reject(UUA_WARNING); } return baseIterableRequest({ method: 'POST', diff --git a/src/unknownUserTracking/tests/consentTracking.test.ts b/src/unknownUserTracking/tests/consentTracking.test.ts index e85572cd..6fa8a81f 100644 --- a/src/unknownUserTracking/tests/consentTracking.test.ts +++ b/src/unknownUserTracking/tests/consentTracking.test.ts @@ -65,7 +65,7 @@ describe('Consent Tracking', () => { mergeOnUnknownToKnown: true }; } - if (key === 'enableUnknownActivation') { + if (key === 'enableUnknownUserActivation') { return true; } return undefined; diff --git a/src/unknownUserTracking/tests/unknownUserEventManager.test.ts b/src/unknownUserTracking/tests/unknownUserEventManager.test.ts index 2cb8c291..348f880e 100644 --- a/src/unknownUserTracking/tests/unknownUserEventManager.test.ts +++ b/src/unknownUserTracking/tests/unknownUserEventManager.test.ts @@ -60,7 +60,7 @@ describe('UnknownUserEventManager', () => { // Mock config for unknown user activation mockConfig.getConfig.mockImplementation((key) => { - if (key === 'enableUnknownActivation') { + if (key === 'enableUnknownUserActivation') { return true; } return undefined; @@ -103,6 +103,38 @@ describe('UnknownUserEventManager', () => { ); }); + it('should migrate legacy snake_case session fields to camelCase', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_UNKNOWN_SESSIONS) { + return JSON.stringify({ + itbl_unknown_sessions: { + number_of_sessions: 3, + first_session: 100, + last_session: 200 + } + }); + } + if (key === SHARED_PREF_CONSENT_TIMESTAMP) return '1234567890'; + return null; + }); + + unknownUserEventManager.updateUnknownSession(); + + const setCall = (localStorage.setItem as jest.Mock).mock.calls.find( + ([key]) => key === SHARED_PREFS_UNKNOWN_SESSIONS + ); + expect(setCall).toBeDefined(); + const written = JSON.parse(setCall[1]); + expect(written.itbl_unknown_sessions).toEqual({ + totalUnknownSessionCount: 4, + firstUnknownSession: 100, + lastUnknownSession: expect.any(Number) + }); + expect(written.itbl_unknown_sessions.number_of_sessions).toBeUndefined(); + expect(written.itbl_unknown_sessions.first_session).toBeUndefined(); + expect(written.itbl_unknown_sessions.last_session).toBeUndefined(); + }); + it('should set criteria data in localStorage when baseIterableRequest succeeds', async () => { const mockResponse = { data: { criteria: 'mockCriteria' } }; (baseIterableRequest as jest.Mock).mockResolvedValueOnce(mockResponse); @@ -826,105 +858,107 @@ describe('UnknownUserEventManager', () => { }) ); - // Should call updateUser for the user data - expect(mockBaseIterableRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'POST', - url: expect.stringContaining('/users/update'), - data: expect.objectContaining({ - dataFields: { - firstName: 'John', - lastName: 'Doe' - } - }) - }) - ); - }); - - it('should replay profile events when criteria are met and createUnknownUser is called', async () => { - const userUpdateData = { - firstName: 'John', - lastName: 'Doe', - age: 30, - eventType: 'userUpdate' - }; - - // Mock localStorage to simulate stored user update data - (localStorage.getItem as jest.Mock).mockImplementation((key) => { - if (key === SHARED_PREFS_USER_UPDATE_OBJECT_KEY) { - return JSON.stringify(userUpdateData); - } - if (key === SHARED_PREFS_UNKNOWN_SESSIONS) { - return JSON.stringify({ - itbl_unknown_sessions: { - number_of_sessions: 1, - first_session: 123456789, - last_session: 123456789 - } - }); - } - if (key === SHARED_PREF_UNKNOWN_USAGE_TRACKED) { - return 'true'; - } - if (key === SHARED_PREF_CONSENT_TIMESTAMP) { - return '1234567890'; // Mock consent timestamp - } - if (key === SHARED_PREFS_EVENT_LIST_KEY) { - return JSON.stringify([]); - } - return null; - }); - - // Mock window object for the test - global.window = Object.create({ - location: { hostname: 'test.example.com' }, - navigator: { userAgent: 'test-user-agent' } - }); - - // Mock successful session creation response - mockBaseIterableRequest.mockResolvedValue({ - status: 200, - data: { success: true } - } as any); - - // Call createUnknownUser to simulate criteria being met - await unknownUserEventManager.createUnknownUser('123'); - - // Verify that the session endpoint was called - expect(mockBaseIterableRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'POST', - url: expect.stringContaining('/unknownuser/events/session'), - data: expect.objectContaining({ - user: expect.objectContaining({ - dataFields: { - firstName: 'John', - lastName: 'Doe', - age: 30 - } - }) - }) - }) - ); - - // Verify that the /users/update endpoint was also called during syncEvents - expect(mockBaseIterableRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'POST', - url: expect.stringContaining('/users/update'), - data: expect.objectContaining({ - dataFields: { - firstName: 'John', - lastName: 'Doe', - age: 30 - }, - preferUserId: true - }) - }) - ); - - // Verify that the user update data was removed after syncing - expect(localStorage.removeItem).toHaveBeenCalledWith(SHARED_PREFS_USER_UPDATE_OBJECT_KEY); - }); - }); + // Should call updateUser for the user data + expect(mockBaseIterableRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: expect.stringContaining('/users/update'), + data: expect.objectContaining({ + dataFields: { + firstName: 'John', + lastName: 'Doe' + } + }) + }) + ); + }); + + it('should replay profile events when criteria are met and createUnknownUser is called', async () => { + const userUpdateData = { + firstName: 'John', + lastName: 'Doe', + age: 30, + eventType: 'userUpdate' + }; + + // Mock localStorage to simulate stored user update data + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_USER_UPDATE_OBJECT_KEY) { + return JSON.stringify(userUpdateData); + } + if (key === SHARED_PREFS_UNKNOWN_SESSIONS) { + return JSON.stringify({ + itbl_unknown_sessions: { + number_of_sessions: 1, + first_session: 123456789, + last_session: 123456789 + } + }); + } + if (key === SHARED_PREF_UNKNOWN_USAGE_TRACKED) { + return 'true'; + } + if (key === SHARED_PREF_CONSENT_TIMESTAMP) { + return '1234567890'; // Mock consent timestamp + } + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([]); + } + return null; + }); + + // Mock window object for the test + global.window = Object.create({ + location: { hostname: 'test.example.com' }, + navigator: { userAgent: 'test-user-agent' } + }); + + // Mock successful session creation response + mockBaseIterableRequest.mockResolvedValue({ + status: 200, + data: { success: true } + } as any); + + // Call createUnknownUser to simulate criteria being met + await unknownUserEventManager.createUnknownUser('123'); + + // Verify that the session endpoint was called + expect(mockBaseIterableRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: expect.stringContaining('/unknownuser/events/session'), + data: expect.objectContaining({ + user: expect.objectContaining({ + dataFields: { + firstName: 'John', + lastName: 'Doe', + age: 30 + } + }) + }) + }) + ); + + // Verify that the /users/update endpoint was also called during syncEvents + expect(mockBaseIterableRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: expect.stringContaining('/users/update'), + data: expect.objectContaining({ + dataFields: { + firstName: 'John', + lastName: 'Doe', + age: 30 + }, + preferUserId: true + }) + }) + ); + + // Verify that the user update data was removed after syncing + expect(localStorage.removeItem).toHaveBeenCalledWith( + SHARED_PREFS_USER_UPDATE_OBJECT_KEY + ); + }); + }); }); diff --git a/src/unknownUserTracking/tests/userMergeScenarios.test.ts b/src/unknownUserTracking/tests/userMergeScenarios.test.ts index 67142c0b..59658246 100644 --- a/src/unknownUserTracking/tests/userMergeScenarios.test.ts +++ b/src/unknownUserTracking/tests/userMergeScenarios.test.ts @@ -121,7 +121,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: false, mergeOnUnknownToKnown: false @@ -159,7 +159,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true @@ -199,7 +199,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: false @@ -252,7 +252,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: false @@ -306,7 +306,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true @@ -362,7 +362,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true + enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test @@ -398,7 +398,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: false @@ -458,7 +458,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true @@ -513,7 +513,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true @@ -559,7 +559,7 @@ describe('UserMergeScenariosTests', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: false @@ -594,7 +594,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: false, mergeOnUnknownToKnown: false @@ -632,7 +632,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true @@ -673,7 +673,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true + enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test @@ -732,7 +732,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true @@ -786,7 +786,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true + enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test @@ -822,7 +822,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: false @@ -884,7 +884,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true @@ -923,7 +923,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: false @@ -985,7 +985,7 @@ describe('UserMergeScenariosTests', () => { const { setEmail, logout } = initializeWithConfig({ authToken: '123', configOptions: { - enableUnknownActivation: true, + enableUnknownUserActivation: true, identityResolution: { replayOnVisitorToKnown: true, mergeOnUnknownToKnown: true diff --git a/src/unknownUserTracking/tests/userUpdate.test.ts b/src/unknownUserTracking/tests/userUpdate.test.ts index d1e32b3b..dd2b91fb 100644 --- a/src/unknownUserTracking/tests/userUpdate.test.ts +++ b/src/unknownUserTracking/tests/userUpdate.test.ts @@ -120,7 +120,7 @@ describe('UserUpdate', () => { const { logout } = initializeWithConfig({ authToken: '123', - configOptions: { enableUnknownActivation: true } + configOptions: { enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test setTypeOfAuth(null); // Explicitly set type of auth to null after logout diff --git a/src/unknownUserTracking/tests/validateCustomEventUserUpdateAPI.test.ts b/src/unknownUserTracking/tests/validateCustomEventUserUpdateAPI.test.ts index 7926cf3c..22625c98 100644 --- a/src/unknownUserTracking/tests/validateCustomEventUserUpdateAPI.test.ts +++ b/src/unknownUserTracking/tests/validateCustomEventUserUpdateAPI.test.ts @@ -171,7 +171,7 @@ describe('validateCustomEventUserUpdateAPI', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', - configOptions: { enableUnknownActivation: true } + configOptions: { enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test setTypeOfAuth(null); // Explicitly set type of auth to null after logout @@ -282,7 +282,7 @@ describe('validateCustomEventUserUpdateAPI', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', - configOptions: { enableUnknownActivation: true } + configOptions: { enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test setTypeOfAuth(null); // Explicitly set type of auth to null after logout @@ -385,7 +385,7 @@ describe('validateCustomEventUserUpdateAPI', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', - configOptions: { enableUnknownActivation: true } + configOptions: { enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test @@ -468,7 +468,7 @@ describe('validateCustomEventUserUpdateAPI', () => { const { setUserID, logout } = initializeWithConfig({ authToken: '123', - configOptions: { enableUnknownActivation: true } + configOptions: { enableUnknownUserActivation: true } }); logout(); // logout to remove logged in users before this test diff --git a/src/unknownUserTracking/unknownUserEventManager.ts b/src/unknownUserTracking/unknownUserEventManager.ts index 85c4f97b..a0951c5e 100644 --- a/src/unknownUserTracking/unknownUserEventManager.ts +++ b/src/unknownUserTracking/unknownUserEventManager.ts @@ -99,7 +99,7 @@ export function registerUnknownUserIdSetter( export function isUnknownUsageTracked(): boolean { // Check both configuration AND user consent - const isEnabled = config.getConfig('enableUnknownActivation') || false; + const isEnabled = config.getConfig('enableUnknownUserActivation') || false; if (!isEnabled) return false; // Also check if user has given consent (consent timestamp exists) @@ -119,6 +119,10 @@ export class UnknownUserEventManager { ); let unknownSessionInfo: { itbl_unknown_sessions?: { + totalUnknownSessionCount?: number; + firstUnknownSession?: number; + lastUnknownSession?: number; + // Legacy snake_case keys, read for one-shot migration only. number_of_sessions?: number; first_session?: number; last_session?: number; @@ -129,20 +133,20 @@ export class UnknownUserEventManager { unknownSessionInfo = JSON.parse(strUnknownSessionInfo); } - // Update existing values or set them if they don't exist - unknownSessionInfo.itbl_unknown_sessions = - unknownSessionInfo.itbl_unknown_sessions || {}; - unknownSessionInfo.itbl_unknown_sessions.number_of_sessions = - (unknownSessionInfo.itbl_unknown_sessions.number_of_sessions || 0) + 1; - unknownSessionInfo.itbl_unknown_sessions.first_session = - unknownSessionInfo.itbl_unknown_sessions.first_session || - this.getCurrentTime(); - unknownSessionInfo.itbl_unknown_sessions.last_session = + const inner = unknownSessionInfo.itbl_unknown_sessions || {}; + const previousCount = + inner.totalUnknownSessionCount ?? inner.number_of_sessions ?? 0; + const previousFirst = + inner.firstUnknownSession ?? + inner.first_session ?? this.getCurrentTime(); - // Update the structure to the desired format const outputObject = { - itbl_unknown_sessions: unknownSessionInfo.itbl_unknown_sessions + itbl_unknown_sessions: { + totalUnknownSessionCount: previousCount + 1, + firstUnknownSession: previousFirst, + lastUnknownSession: this.getCurrentTime() + } }; localStorage.setItem( @@ -282,11 +286,18 @@ export class UnknownUserEventManager { platform: WEB_PLATFORM }, unknownSessionContext: { - totalUnknownSessionCount: userDataJson.number_of_sessions || 0, + totalUnknownSessionCount: + userDataJson.totalUnknownSessionCount ?? + userDataJson.number_of_sessions ?? + 0, lastUnknownSession: - userDataJson.last_session || this.getCurrentTime(), + userDataJson.lastUnknownSession ?? + userDataJson.last_session ?? + this.getCurrentTime(), firstUnknownSession: - userDataJson.first_session || this.getCurrentTime(), + userDataJson.firstUnknownSession ?? + userDataJson.first_session ?? + this.getCurrentTime(), matchedCriteriaId: parseInt(criteriaId, 10) } }; diff --git a/src/users/users.ts b/src/users/users.ts index 46c11620..67240c30 100644 --- a/src/users/users.ts +++ b/src/users/users.ts @@ -6,7 +6,7 @@ import { UpdateSubscriptionParams, UpdateUserParams } from './types'; import { updateSubscriptionsSchema, updateUserSchema } from './users.schema'; import { UnknownUserEventManager } from '../unknownUserTracking/unknownUserEventManager'; import { canTrackUnknownUser } from '../utils/commonFunctions'; -import { AUA_WARNING, ENDPOINTS } from '../constants'; +import { UUA_WARNING, ENDPOINTS } from '../constants'; export const updateUserEmail = (newEmail: string) => baseIterableRequest({ @@ -31,7 +31,7 @@ export const updateUser = (payloadParam: UpdateUserParams = {}) => { if (canTrackUnknownUser()) { const unknownUserEventManager = new UnknownUserEventManager(); unknownUserEventManager.trackUnknownUpdateUser(payload); - return Promise.reject(AUA_WARNING); + return Promise.reject(UUA_WARNING); } return baseIterableRequest({ method: 'POST', diff --git a/src/utils/commonFunctions.test.ts b/src/utils/commonFunctions.test.ts new file mode 100644 index 00000000..68fe4477 --- /dev/null +++ b/src/utils/commonFunctions.test.ts @@ -0,0 +1,37 @@ +import { migrateLegacyKey } from './commonFunctions'; + +describe('migrateLegacyKey', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('moves legacy value to the new key and removes the old key', () => { + localStorage.setItem('unknown_userId', 'abc-123'); + migrateLegacyKey('unknown_userId', 'itbl_userid_unknown'); + expect(localStorage.getItem('itbl_userid_unknown')).toBe('abc-123'); + expect(localStorage.getItem('unknown_userId')).toBeNull(); + }); + + it('does not overwrite an existing value at the new key', () => { + localStorage.setItem('itbl_userid_unknown', 'new-value'); + localStorage.setItem('unknown_userId', 'legacy-value'); + migrateLegacyKey('unknown_userId', 'itbl_userid_unknown'); + expect(localStorage.getItem('itbl_userid_unknown')).toBe('new-value'); + // Legacy is still cleaned up so we don't keep stale data around. + expect(localStorage.getItem('unknown_userId')).toBeNull(); + }); + + it('is a no-op when neither key has a value', () => { + migrateLegacyKey('unknown_userId', 'itbl_userid_unknown'); + expect(localStorage.getItem('itbl_userid_unknown')).toBeNull(); + expect(localStorage.getItem('unknown_userId')).toBeNull(); + }); + + it('is idempotent across multiple calls', () => { + localStorage.setItem('unknown_userId', 'abc-123'); + migrateLegacyKey('unknown_userId', 'itbl_userid_unknown'); + migrateLegacyKey('unknown_userId', 'itbl_userid_unknown'); + expect(localStorage.getItem('itbl_userid_unknown')).toBe('abc-123'); + expect(localStorage.getItem('unknown_userId')).toBeNull(); + }); +}); diff --git a/src/utils/commonFunctions.ts b/src/utils/commonFunctions.ts index d7d1e3d8..47e3507b 100644 --- a/src/utils/commonFunctions.ts +++ b/src/utils/commonFunctions.ts @@ -2,4 +2,26 @@ import config from './config'; import { getTypeOfAuth } from './typeOfAuth'; export const canTrackUnknownUser = (): boolean => - config.getConfig('enableUnknownActivation') && getTypeOfAuth() === null; + config.getConfig('enableUnknownUserActivation') && getTypeOfAuth() === null; + +/** + * One-shot localStorage key migration. If a value exists at `oldKey` and not + * at `newKey`, copy it over and delete the old one. Safe to call repeatedly. + */ +export const migrateLegacyKey = (oldKey: string, newKey: string): void => { + if (typeof localStorage === 'undefined') return; + try { + const existing = localStorage.getItem(newKey); + if (existing != null) { + localStorage.removeItem(oldKey); + return; + } + const legacy = localStorage.getItem(oldKey); + if (legacy != null) { + localStorage.setItem(newKey, legacy); + localStorage.removeItem(oldKey); + } + } catch (_e) { + /* localStorage may be unavailable / quota-exceeded; ignore */ + } +}; diff --git a/src/utils/config.test.ts b/src/utils/config.test.ts index 624d15ca..fdae1f5a 100644 --- a/src/utils/config.test.ts +++ b/src/utils/config.test.ts @@ -11,4 +11,34 @@ describe('Config', () => { expect(config.getConfig('logLevel')).toBe('verbose'); expect(config.getConfig('baseURL')).toBe('https://google.com'); }); + + describe('enableUnknownActivation deprecation alias', () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + config.setConfig({ enableUnknownUserActivation: false }); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('forwards legacy enableUnknownActivation to enableUnknownUserActivation', () => { + config.setConfig({ enableUnknownActivation: true }); + expect(config.getConfig('enableUnknownUserActivation')).toBe(true); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('enableUnknownActivation') + ); + }); + + it('prefers enableUnknownUserActivation when both are supplied', () => { + config.setConfig({ + enableUnknownActivation: false, + enableUnknownUserActivation: true + }); + expect(config.getConfig('enableUnknownUserActivation')).toBe(true); + expect(warnSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 28285ebc..62f47bae 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -8,7 +8,12 @@ export type IdentityResolution = { export type Options = { logLevel: 'none' | 'verbose'; baseURL: string; - enableUnknownActivation: boolean; + enableUnknownUserActivation: boolean; + /** + * @deprecated Use `enableUnknownUserActivation` instead. This alias is kept + * for backward compatibility and will be removed in the next major version. + */ + enableUnknownActivation?: boolean; isEuIterableService: boolean; dangerouslyAllowJsPopups: boolean; eventThresholdLimit?: number; @@ -20,7 +25,7 @@ const _config = () => { let options: Options = { logLevel: 'none', baseURL: BASE_URL, - enableUnknownActivation: false, + enableUnknownUserActivation: false, isEuIterableService: false, dangerouslyAllowJsPopups: false, eventThresholdLimit: DEFAULT_EVENT_THRESHOLD_LIMIT, @@ -35,9 +40,20 @@ const _config = () => { return { getConfig, setConfig: (newOptions: Partial) => { + const { enableUnknownActivation, ...rest } = newOptions; + if ( + enableUnknownActivation !== undefined && + rest.enableUnknownUserActivation === undefined + ) { + // eslint-disable-next-line no-console + console.warn( + '[Iterable] `enableUnknownActivation` is deprecated. Use `enableUnknownUserActivation` instead.' + ); + rest.enableUnknownUserActivation = enableUnknownActivation; + } options = { ...options, - ...newOptions, + ...rest, identityResolution: { ...options.identityResolution, ...newOptions.identityResolution