Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions clevertap.js
Original file line number Diff line number Diff line change
Expand Up @@ -8616,6 +8616,12 @@
if (_classPrivateFieldLooseBase(this, _dropRequestDueToOptOut)[_dropRequestDueToOptOut]()) {
this.logger.debug('req dropped due to optout cookie: ' + this.device.gcookie);
return;
} // Drop all requests for blocked account ID


if (this.account && this.account.id === '8R5-4K5-466Z') {
this.logger.debug('req dropped for blocked account: ' + this.account.id);
return;
Comment on lines +8619 to +8624
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Extract hardcoded account ID to a constant and centralize blocking logic.

The account ID '8R5-4K5-466Z' is hardcoded as a magic string and duplicated across multiple locations (also appears in the post method below and in other files per the AI summary). This violates the DRY principle and makes maintenance difficult.

Additionally, logging specific account IDs may raise privacy or compliance concerns, depending on your data governance policies.

Consider the following refactor:

  1. Extract to a constant:
+// At the top of the file or in a shared constants module
+const BLOCKED_ACCOUNT_IDS = ['8R5-4K5-466Z'];
  1. Centralize the blocking logic in a single helper function:
function isAccountBlocked(accountId) {
  return BLOCKED_ACCOUNT_IDS.includes(accountId);
}
  1. Use the helper function:
-if (this.account && this.account.id === '8R5-4K5-466Z') {
-  this.logger.debug('req dropped for blocked account: ' + this.account.id);
+if (this.account && isAccountBlocked(this.account.id)) {
+  this.logger.debug('req dropped for blocked account');
   return;
 }

Run the following script to verify all occurrences of the hardcoded account ID across the codebase:


🏁 Script executed:

#!/bin/bash
# Description: Find all occurrences of the hardcoded account ID to assess duplication

# Search for the hardcoded account ID
rg -n "8R5-4K5-466Z" --type js

Length of output: 1202



---

🏁 Script executed:

```shell
#!/bin/bash
# Check if clevertap.js is in build directory or gitignore
find . -name ".gitignore" -type f | xargs grep -l "clevertap.js" 2>/dev/null || echo "Not found in gitignore"
ls -la | grep -E "^d.*dist|^d.*build" 2>/dev/null || echo "No obvious build directories"
head -20 clevertap.js | head -5

Length of output: 386


🏁 Script executed:

#!/bin/bash
# Look for constants file defining blocked accounts
rg -n "BLOCKED|blocked.*account" --type js src/ -g "!*.spec.js" | head -20

Length of output: 380


🏁 Script executed:

#!/bin/bash
# Examine source implementations in detail
echo "=== src/util/requestDispatcher.js (lines 15-30) ==="
sed -n '15,30p' src/util/requestDispatcher.js

echo -e "\n=== src/modules/request.js (lines 270-290) ==="
sed -n '270,290p' src/modules/request.js

Length of output: 1360


🏁 Script executed:

#!/bin/bash
# Check if there's a dedicated blocked accounts utility/constant
find src -name "*blocked*" -o -name "*constant*" -o -name "*config*" | grep -E "\.(js|ts)$"

Length of output: 68



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine constants pattern in the codebase
echo "=== src/util/constants.js (first 50 lines) ==="
head -50 src/util/constants.js | grep -A2 -B2 "ACCOUNT\|BLOCK\|ID\|LIST\|ARRAY" || head -50 src/util/constants.js

echo -e "\n=== Check for any existing blocklist or similar patterns ==="
rg -n "const.*=.*\[" src/util/constants.js | head -10

Length of output: 1035


🏁 Script executed:

#!/bin/bash
# Verify test file intent
echo "=== test/unit/util/blockedAccount.spec.js (first 50 lines) ==="
head -50 test/unit/util/blockedAccount.spec.js

echo -e "\n=== test/unit/modules/blockedAccountRequest.spec.js (first 50 lines) ==="
head -50 test/unit/modules/blockedAccountRequest.spec.js

Length of output: 3286


Extract hardcoded account ID to a constant in src/util/constants.js and centralize blocking logic across src/util/requestDispatcher.js and src/modules/request.js.

The account ID '8R5-4K5-466Z' is duplicated across two source files with identical checks:

  • src/util/requestDispatcher.js:23 (fireRequest method)
  • src/modules/request.js:280 (post method)

This violates the DRY principle and makes maintenance difficult. Additionally, logging specific account IDs may raise privacy or compliance concerns.

Recommended refactor:

  1. Add to src/util/constants.js:
export const BLOCKED_ACCOUNT_IDS = ['8R5-4K5-466Z']
  1. Create a helper function in a shared utility module:
function isAccountBlocked(accountId) {
  return BLOCKED_ACCOUNT_IDS.includes(accountId)
}
  1. Update both locations to use the helper:
    • src/util/requestDispatcher.js:23 – Replace the hardcoded check with the helper function
    • src/modules/request.js:280 – Replace the hardcoded check with the helper function
    • Remove account ID from debug logs (log only: 'req dropped for blocked account')

This approach scales for blocking multiple accounts and centralizes the logic for future maintenance.

} // set a request in progress
// so that if gcookie is not present, no other request can be made asynchronusly

Expand Down Expand Up @@ -16546,6 +16552,13 @@
}

post(url, body) {
// Drop all requests for blocked account ID
if (_classPrivateFieldLooseBase(this, _account$3)[_account$3] && _classPrivateFieldLooseBase(this, _account$3)[_account$3].id === '8R5-4K5-466Z') {
_classPrivateFieldLooseBase(this, _logger$3)[_logger$3].debug('post req dropped for blocked account: ' + _classPrivateFieldLooseBase(this, _account$3)[_account$3].id);

return Promise.reject(new Error('Account blocked'));
}
Comment on lines +16555 to +16560
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicate blocking logic—consolidate with the check at lines 8619-8624.

This is the same hardcoded account ID check that appears earlier in the fireRequest flow (lines 8619-8624). According to the AI summary, similar logic also exists in src/modules/request.js and src/util/requestDispatcher.js. This widespread duplication makes the codebase fragile and difficult to maintain.

Recommendation:

Consolidate all blocked-account checks into a single, early validation layer—ideally in a centralized request dispatcher or guard function—so that all request paths (fireRequest, post, etc.) benefit from one authoritative check. This eliminates duplication and ensures consistent behavior across the SDK.

See the refactor suggestion in the previous comment (lines 8619-8624) for extracting the account ID to a constant and creating a reusable helper function.

🤖 Prompt for AI Agents
In clevertap.js around lines 16555-16560, the hardcoded blocked-account ID check
is duplicated here; remove this local check and instead call the centralized
guard/helper used earlier (extracted constant and function referenced near lines
8619-8624). Consolidate the blocking logic by importing or referencing the
single BLOCKED_ACCOUNT_ID constant and isAccountBlocked(account) helper from the
shared request validation module (or move that helper to a common util if not
already), then replace this block with a call to that helper and a single
Promise.reject path. Update all other duplicate sites (fireRequest,
src/modules/request.js, src/util/requestDispatcher.js) to use the same helper so
the check exists only once and ensure tests still cover the blocked-account
behavior.


return fetch(url, {
method: 'post',
headers: {
Expand Down
2 changes: 1 addition & 1 deletion clevertap.js.map

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

140 changes: 73 additions & 67 deletions src/modules/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class RequestManager {
#clearCookie = false
processingBackup = false

constructor ({ logger, account, device, session, isPersonalisationActive }) {
constructor({ logger, account, device, session, isPersonalisationActive }) {
this.#logger = logger
this.#account = account
this.#device = device
Expand All @@ -35,7 +35,7 @@ export default class RequestManager {
* Unified backup processing method
* @param {boolean} oulOnly - If true, process only OUL requests. If false, process all non-fired requests.
*/
processBackupEvents (oulOnly = false) {
processBackupEvents(oulOnly = false) {
const backupMap = StorageManager.readFromLSorCookie(LCOOKIE_NAME)
if (typeof backupMap === 'undefined' || backupMap === null) {
return
Expand Down Expand Up @@ -72,7 +72,7 @@ export default class RequestManager {
this.processingBackup = false
}

addSystemDataToObject (dataObject, ignoreTrim) {
addSystemDataToObject(dataObject, ignoreTrim) {
// ignore trim for chrome notifications; undefined everywhere else
if (typeof ignoreTrim === 'undefined') {
dataObject = removeUnsupportedChars(dataObject, this.#logger)
Expand Down Expand Up @@ -106,7 +106,7 @@ export default class RequestManager {
return dataObject
}

addFlags (data) {
addFlags(data) {
// check if cookie should be cleared.
this.#clearCookie = StorageManager.getAndClearMetaProp(CLEAR)
if (this.#clearCookie !== undefined && this.#clearCookie) {
Expand Down Expand Up @@ -137,7 +137,7 @@ export default class RequestManager {
* @param {boolean} override whether the request can go through or not
* @param {Boolean} sendOULFlag - true in case of a On User Login request
*/
saveAndFireRequest (url, override, sendOULFlag, evtName) {
saveAndFireRequest(url, override, sendOULFlag, evtName) {
const now = getNow()

// Get the next available request number that doesn't conflict with existing backups
Expand Down Expand Up @@ -175,82 +175,82 @@ export default class RequestManager {
}
}

#getNextAvailableReqN () {
// Read existing backup data to check for conflicts
const backupMap = StorageManager.readFromLSorCookie(LCOOKIE_NAME)
#getNextAvailableReqN() {
// Read existing backup data to check for conflicts
const backupMap = StorageManager.readFromLSorCookie(LCOOKIE_NAME)

// Start from the current REQ_N + 1
let candidateReqN = $ct.globalCache.REQ_N + 1
// Start from the current REQ_N + 1
let candidateReqN = $ct.globalCache.REQ_N + 1

// If no backup data exists, use the candidate
if (!backupMap || typeof backupMap !== 'object') {
return candidateReqN
}
// If no backup data exists, use the candidate
if (!backupMap || typeof backupMap !== 'object') {
return candidateReqN
}

// Keep incrementing until we find a request number that doesn't exist in backup
while (backupMap.hasOwnProperty(candidateReqN.toString())) {
candidateReqN++
this.#logger.debug(`Request number ${candidateReqN - 1} already exists in backup, trying ${candidateReqN}`)
}

// Keep incrementing until we find a request number that doesn't exist in backup
while (backupMap.hasOwnProperty(candidateReqN.toString())) {
candidateReqN++
this.#logger.debug(`Request number ${candidateReqN - 1} already exists in backup, trying ${candidateReqN}`)
this.#logger.debug(`Using request number: ${candidateReqN}`)
return candidateReqN
}

this.#logger.debug(`Using request number: ${candidateReqN}`)
return candidateReqN
}
unregisterTokenForGuid(givenGUID) {
const payload = StorageManager.readFromLSorCookie(PUSH_SUBSCRIPTION_DATA)
// Send unregister event only when token is available
if (payload) {
const data = {}
data.type = 'data'
if (isValueValid(givenGUID)) {
data.g = givenGUID
}
data.action = 'unregister'
data.id = this.#account.id

unregisterTokenForGuid (givenGUID) {
const payload = StorageManager.readFromLSorCookie(PUSH_SUBSCRIPTION_DATA)
// Send unregister event only when token is available
if (payload) {
const data = {}
data.type = 'data'
if (isValueValid(givenGUID)) {
data.g = givenGUID
}
data.action = 'unregister'
data.id = this.#account.id
const obj = this.#session.getSessionCookieObject()

const obj = this.#session.getSessionCookieObject()
data.s = obj.s // session cookie
const compressedData = compressData(JSON.stringify(data), this.#logger)

data.s = obj.s // session cookie
const compressedData = compressData(JSON.stringify(data), this.#logger)
let pageLoadUrl = this.#account.dataPostURL
pageLoadUrl = addToURL(pageLoadUrl, 'type', 'data')
pageLoadUrl = addToURL(pageLoadUrl, 'd', compressedData)
RequestDispatcher.fireRequest(pageLoadUrl, true)
StorageManager.saveToLSorCookie(FIRE_PUSH_UNREGISTERED, false)
}
// REGISTER TOKEN
this.registerToken(payload)
}

registerToken(payload) {
if (!payload) return
// add gcookie etc to the payload
payload = this.addSystemDataToObject(payload, true)
payload = JSON.stringify(payload)
let pageLoadUrl = this.#account.dataPostURL
pageLoadUrl = addToURL(pageLoadUrl, 'type', 'data')
pageLoadUrl = addToURL(pageLoadUrl, 'd', compressedData)
RequestDispatcher.fireRequest(pageLoadUrl, true)
StorageManager.saveToLSorCookie(FIRE_PUSH_UNREGISTERED, false)
pageLoadUrl = addToURL(pageLoadUrl, 'd', compressData(payload, this.#logger))
RequestDispatcher.fireRequest(pageLoadUrl)
// set in localstorage
StorageManager.save(WEBPUSH_LS_KEY, 'ok')
}
// REGISTER TOKEN
this.registerToken(payload)
}

registerToken (payload) {
if (!payload) return
// add gcookie etc to the payload
payload = this.addSystemDataToObject(payload, true)
payload = JSON.stringify(payload)
let pageLoadUrl = this.#account.dataPostURL
pageLoadUrl = addToURL(pageLoadUrl, 'type', 'data')
pageLoadUrl = addToURL(pageLoadUrl, 'd', compressData(payload, this.#logger))
RequestDispatcher.fireRequest(pageLoadUrl)
// set in localstorage
StorageManager.save(WEBPUSH_LS_KEY, 'ok')
}
processEvent(data) {
this.#addToLocalEventMap(data.evtName)
data = this.addSystemDataToObject(data, undefined)
this.addFlags(data)
data[CAMP_COOKIE_NAME] = getCampaignObjForLc()
const compressedData = compressData(JSON.stringify(data), this.#logger)
let pageLoadUrl = this.#account.dataPostURL
pageLoadUrl = addToURL(pageLoadUrl, 'type', EVT_PUSH)
pageLoadUrl = addToURL(pageLoadUrl, 'd', compressedData)

processEvent (data) {
this.#addToLocalEventMap(data.evtName)
data = this.addSystemDataToObject(data, undefined)
this.addFlags(data)
data[CAMP_COOKIE_NAME] = getCampaignObjForLc()
const compressedData = compressData(JSON.stringify(data), this.#logger)
let pageLoadUrl = this.#account.dataPostURL
pageLoadUrl = addToURL(pageLoadUrl, 'type', EVT_PUSH)
pageLoadUrl = addToURL(pageLoadUrl, 'd', compressedData)

this.saveAndFireRequest(pageLoadUrl, $ct.blockRequest, false, data.evtName)
}
this.saveAndFireRequest(pageLoadUrl, $ct.blockRequest, false, data.evtName)
}

#addToLocalEventMap (evtName) {
#addToLocalEventMap(evtName) {
if (StorageManager._isLocalStorageSupported()) {
if (typeof $ct.globalEventsMap === 'undefined') {
$ct.globalEventsMap = StorageManager.readFromLSorCookie(EV_COOKIE)
Expand All @@ -275,7 +275,13 @@ processEvent (data) {
}
}

post (url, body) {
post(url, body) {
// Drop all requests for blocked account ID
if (this.#account && this.#account.id === '8R5-4K5-466Z') {
this.#logger.debug('post req dropped for blocked account: ' + this.#account.id)
return Promise.reject(new Error('Account blocked'))
}

return fetch(url, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
Expand Down
24 changes: 15 additions & 9 deletions src/util/requestDispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ export default class RequestDispatcher {
minDelayFrequency = 0

// ANCHOR - Requests get fired from here
static #fireRequest (url, tries, skipARP, sendOULFlag, evtName) {
static #fireRequest(url, tries, skipARP, sendOULFlag, evtName) {
if (this.#dropRequestDueToOptOut()) {
this.logger.debug('req dropped due to optout cookie: ' + this.device.gcookie)
return
}

// Drop all requests for blocked account ID
if (this.account && this.account.id === '8R5-4K5-466Z') {
this.logger.debug('req dropped for blocked account: ' + this.account.id)
return
}

// set a request in progress
// so that if gcookie is not present, no other request can be made asynchronusly
if (!isValueValid(this.device.gcookie)) {
Expand All @@ -41,9 +47,9 @@ export default class RequestDispatcher {
}
} else {
if (!isValueValid(this.device.gcookie) &&
($ct.globalCache.RESP_N < $ct.globalCache.REQ_N - 1) &&
tries < MAX_TRIES) {
// if ongoing First Request is in progress, initiate retry
($ct.globalCache.RESP_N < $ct.globalCache.REQ_N - 1) &&
tries < MAX_TRIES) {
// if ongoing First Request is in progress, initiate retry
setTimeout(() => {
this.logger.debug(`retrying fire request for url: ${url}, tries: ${tries}`)
this.#fireRequest(url, tries + 1, skipARP, sendOULFlag)
Expand Down Expand Up @@ -99,27 +105,27 @@ export default class RequestDispatcher {
* @param {*} skipARP
* @param {boolean} sendOULFlag
*/
static fireRequest (url, skipARP, sendOULFlag, evtName) {
static fireRequest(url, skipARP, sendOULFlag, evtName) {
this.#fireRequest(url, 1, skipARP, sendOULFlag, evtName)
}

static #dropRequestDueToOptOut () {
static #dropRequestDueToOptOut() {
if ($ct.isOptInRequest || !isValueValid(this.device.gcookie) || !isString(this.device.gcookie)) {
$ct.isOptInRequest = false
return false
}
return this.device.gcookie.slice(-3) === OPTOUT_COOKIE_ENDSWITH
}

static #addUseIPToRequest (pageLoadUrl) {
static #addUseIPToRequest(pageLoadUrl) {
var useIP = StorageManager.getMetaProp(USEIP_KEY)
if (typeof useIP !== 'boolean') {
useIP = false
}
return addToURL(pageLoadUrl, USEIP_KEY, useIP ? 'true' : 'false')
};

static #addARPToRequest (url, skipResARP) {
static #addARPToRequest(url, skipResARP) {
if (skipResARP === true) {
const _arp = {}
_arp.skipResARP = true
Expand All @@ -131,7 +137,7 @@ export default class RequestDispatcher {
return url
}

getDelayFrequency () {
getDelayFrequency() {
this.logger.debug('Network retry #' + this.networkRetryCount)

// Retry with delay as 1s for first 10 retries
Expand Down
84 changes: 84 additions & 0 deletions test/unit/modules/blockedAccountRequest.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import RequestManager from '../../../src/modules/request'
import Account from '../../../src/modules/account'

describe('RequestManager - Blocked Account ID', () => {
let requestManager
let mockLogger
let mockDevice
let mockSession
let mockIsPersonalisationActive

beforeEach(() => {
mockLogger = {
debug: jest.fn(),
error: jest.fn(),
wzrkError: {}
}
mockDevice = {
gcookie: 'test-gcookie-123'
}
mockSession = {
getSessionCookieObject: jest.fn(() => ({ s: 'session-123', p: 1 }))
}
mockIsPersonalisationActive = jest.fn(() => false)
})

test('should drop post requests for blocked account ID 8R5-4K5-466Z', () => {
const blockedAccount = new Account({ id: '8R5-4K5-466Z' })
requestManager = new RequestManager({
logger: mockLogger,
account: blockedAccount,
device: mockDevice,
session: mockSession,
isPersonalisationActive: mockIsPersonalisationActive
})

const testUrl = 'https://example.com/api/sync'
const testBody = JSON.stringify({ data: 'test' })

return requestManager.post(testUrl, testBody)
.then(() => {
// If we reach here, the test should fail
expect(true).toBe(false)
})
.catch((error) => {
// Verify that the request was blocked
expect(error.message).toBe('Account blocked')
expect(mockLogger.debug).toHaveBeenCalledWith('post req dropped for blocked account: 8R5-4K5-466Z')
})
})

test('should allow post requests for non-blocked account IDs', () => {
const normalAccount = new Account({ id: 'TEST-ACCOUNT-123' })
requestManager = new RequestManager({
logger: mockLogger,
account: normalAccount,
device: mockDevice,
session: mockSession,
isPersonalisationActive: mockIsPersonalisationActive
})

const testUrl = 'https://example.com/api/sync'
const testBody = JSON.stringify({ data: 'test' })

// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ success: true })
})
)

return requestManager.post(testUrl, testBody)
.then((result) => {
// Verify that the request was not blocked
expect(mockLogger.debug).not.toHaveBeenCalledWith(expect.stringContaining('post req dropped for blocked account'))
expect(result).toEqual({ success: true })
expect(global.fetch).toHaveBeenCalledWith(testUrl, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: testBody
})
})
})
})
Loading
Loading