Skip to content
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Change Log
All notable changes to this project will be documented in this file.

## [2.5.4] 19th Feb 2026
- Added Clevertap Custom Id Support in On User Login.

## [2.5.3] 16th Feb 2026
- Fixed repeated geolocation permission prompt on every page navigation in Safari. The SDK now caches the user's accept/deny response in localStorage, preventing redundant prompts.

Expand Down
372 changes: 204 additions & 168 deletions clevertap.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion clevertap.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions clevertap.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clevertap-web-sdk",
"version": "2.5.3",
"version": "2.5.4",
"description": "",
"main": "clevertap.js",
"scripts": {
Expand Down
9 changes: 8 additions & 1 deletion src/modules/logger.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
CLEVERTAP_ERROR_PREFIX
CLEVERTAP_ERROR_PREFIX,
CLEVERTAP_INFO_PREFIX
} from '../util/messages'

export const logLevels = {
Expand Down Expand Up @@ -71,6 +72,12 @@ export class Logger {
this.error(`${CLEVERTAP_ERROR_PREFIX} ${code}: ${description}`)
}

reportInfo (code, description) {
this.wzrkError.c = code
this.wzrkError.d = description
this.info(`${CLEVERTAP_INFO_PREFIX} ${code}: ${description}`)
}

#log (level, message) {
if (window.console) {
try {
Expand Down
33 changes: 28 additions & 5 deletions src/modules/userLogin.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
processGPlusUserObj,
addToLocalProfileMap
} from '../util/clevertap'
import { validateCustomCleverTapID } from '../util/helpers'

export default class UserLoginHandler extends Array {
#request
Expand Down Expand Up @@ -65,7 +66,7 @@ export default class UserLoginHandler extends Array {
#processOUL (profileArr) {
let sendOULFlag = true
StorageManager.saveToLSorCookie(FIRE_PUSH_UNREGISTERED, sendOULFlag)
const addToK = (ids) => {
const addToK = (ids, customIdFlag = false) => {
Copy link
Copy Markdown
Contributor

@ThisIsRaghavGupta ThisIsRaghavGupta Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is default false the base case?

let k = StorageManager.readFromLSorCookie(KCOOKIE_NAME)
const g = StorageManager.readFromLSorCookie(GCOOKIE_NAME)
let kId
Expand Down Expand Up @@ -118,7 +119,10 @@ export default class UserLoginHandler extends Array {
const gFromCache = $ct.LRU_CACHE.get(kId)
$ct.LRU_CACHE.set(kId, gFromCache)
StorageManager.saveToLSorCookie(GCOOKIE_NAME, gFromCache)
this.#device.gcookie = gFromCache
// Only override gcookie if we don't have a customId
if (!customIdFlag) {
this.#device.gcookie = gFromCache
}

const lastK = $ct.LRU_CACHE.getSecondLastKey()
if (StorageManager.readFromLSorCookie(FIRE_PUSH_UNREGISTERED) && lastK !== -1) {
Expand All @@ -127,10 +131,10 @@ export default class UserLoginHandler extends Array {
this.#request.unregisterTokenForGuid(lastGUID)
}
} else {
if (!anonymousUser) {
if (!anonymousUser && !customIdFlag) {
this.clear()
} else {
if ((g) != null) {
if ((g) != null && !customIdFlag) {
this.#device.gcookie = g
StorageManager.saveToLSorCookie(GCOOKIE_NAME, g)
sendOULFlag = false
Expand Down Expand Up @@ -171,12 +175,31 @@ export default class UserLoginHandler extends Array {
}
}
if (profileObj != null && (!isObjectEmpty(profileObj))) { // profile got set from above
let hasCustomId = false
data.type = 'profile'
if (profileObj.tz == null) {
// try to auto capture user timezone if not present
profileObj.tz = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
}

// Handle customId field for setting custom CleverTap ID.
if (profileObj.customId) {
const result = validateCustomCleverTapID(profileObj.customId)
if (result.isValid) {
hasCustomId = true
// Set the custom ID as gcookie
this.#device.gcookie = result.sanitizedId
StorageManager.saveToLSorCookie(GCOOKIE_NAME, result.sanitizedId)
this.#logger.debug('customId set for OUL flow:: ' + result.sanitizedId)
} else {
this.#logger.error('Invalid customId: ' + result.error)
}
delete profileObj.customId
} else if ('customId' in profileObj) {
// Key present but falsy (e.g. '', 0) — remove so it is not sent as a profile field
delete profileObj.customId
}

data.profile = profileObj
const ids = []
if (StorageManager._isLocalStorageSupported()) {
Expand All @@ -193,7 +216,7 @@ export default class UserLoginHandler extends Array {
ids.push('FB:' + profileObj.FBID)
}
if (ids.length > 0) {
addToK(ids)
addToK(ids, hasCustomId)
}
}
addToLocalProfileMap(profileObj, true)
Expand Down
1 change: 1 addition & 0 deletions src/util/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const DATA_NOT_SENT_TEXT = 'This property has been ignored.'
export const INVALID_ACCOUNT = 'Invalid account ID'
export const INVALID_EVENT = 'Event structure not valid. Unable to process event'
export const CLEVERTAP_ERROR_PREFIX = 'CleverTap error:' // Formerly wzrk_error_txt
export const CLEVERTAP_INFO_PREFIX = 'CleverTap info:'
export const EMBED_ERROR = `${CLEVERTAP_ERROR_PREFIX} Incorrect embed script.`
export const EVENT_ERROR = `${CLEVERTAP_ERROR_PREFIX} Event structure not valid. ${DATA_NOT_SENT_TEXT}`
export const GENDER_ERROR = `${CLEVERTAP_ERROR_PREFIX} Gender value should one of the following: m,f,o,u,male,female,unknown,others (case insensitive). ${DATA_NOT_SENT_TEXT}`
Expand Down
10 changes: 5 additions & 5 deletions src/util/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ const cleanNullEmptyValues = (obj, logger = null, currentDepth = 0, maxDepth = 3
if (logger) {
const currentKeyPath = keyPath ? `${keyPath}[${index}]` : `[${index}]`
if (item === null || item === undefined) {
logger.reportError(NULL_VALUE_REMOVED.code, NULL_VALUE_REMOVED.message.replace('%s', currentKeyPath))
logger.reportInfo(NULL_VALUE_REMOVED.code, NULL_VALUE_REMOVED.message.replace('%s', currentKeyPath))
} else {
logger.reportError(EMPTY_VALUE_REMOVED.code, EMPTY_VALUE_REMOVED.message.replace('%s', currentKeyPath))
logger.reportInfo(EMPTY_VALUE_REMOVED.code, EMPTY_VALUE_REMOVED.message.replace('%s', currentKeyPath))
}
}
return
Expand All @@ -132,7 +132,7 @@ const cleanNullEmptyValues = (obj, logger = null, currentDepth = 0, maxDepth = 3
cleanedArray.push(cleanedItem)
} else if (logger) {
const currentKeyPath = keyPath ? `${keyPath}[${index}]` : `[${index}]`
logger.reportError(EMPTY_VALUE_REMOVED.code, EMPTY_VALUE_REMOVED.message.replace('%s', currentKeyPath))
logger.reportInfo(EMPTY_VALUE_REMOVED.code, EMPTY_VALUE_REMOVED.message.replace('%s', currentKeyPath))
}
})

Expand All @@ -156,9 +156,9 @@ const cleanNullEmptyValues = (obj, logger = null, currentDepth = 0, maxDepth = 3
cleanedObj[key] = value
} else if (logger) {
if (value === null || value === undefined) {
logger.reportError(NULL_VALUE_REMOVED.code, NULL_VALUE_REMOVED.message.replace('%s', currentKeyPath))
logger.reportInfo(NULL_VALUE_REMOVED.code, NULL_VALUE_REMOVED.message.replace('%s', currentKeyPath))
} else {
logger.reportError(EMPTY_VALUE_REMOVED.code, EMPTY_VALUE_REMOVED.message.replace('%s', currentKeyPath))
logger.reportInfo(EMPTY_VALUE_REMOVED.code, EMPTY_VALUE_REMOVED.message.replace('%s', currentKeyPath))
}
}
}
Expand Down
27 changes: 26 additions & 1 deletion test/unit/clevertap.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ describe('clevertap.js', function () {
}
mockOUL = {
clear: jest.fn(),
_processOldValues: jest.fn()
_processOldValues: jest.fn(),
push: jest.fn()
}

// Mock RequestDispatcher methods to prevent fetch API calls
Expand Down Expand Up @@ -154,6 +155,30 @@ describe('clevertap.js', function () {
})
})

describe('customId handling', () => {
test('should validate customId field when provided', () => {
validateCustomCleverTapID.mockReturnValue({ isValid: true, sanitizedId: '_w_custom_oul_id' })

// Test the validation function directly
const result = validateCustomCleverTapID('_w_custom_oul_id')

expect(validateCustomCleverTapID).toHaveBeenCalledWith('_w_custom_oul_id')
expect(result.isValid).toBe(true)
expect(result.sanitizedId).toBe('_w_custom_oul_id')
})

test('should handle invalid customId field', () => {
validateCustomCleverTapID.mockReturnValue({ isValid: false, sanitizedId: null, error: 'Invalid custom ID format' })

// Test the validation function directly
const result = validateCustomCleverTapID('invalid_id')

expect(validateCustomCleverTapID).toHaveBeenCalledWith('invalid_id')
expect(result.isValid).toBe(false)
expect(result.error).toBe('Invalid custom ID format')
})
})

describe('init', () => {
beforeEach(() => {
mockSessionObject.getSessionCookieObject.mockReturnValue({})
Expand Down
Loading