-
Notifications
You must be signed in to change notification settings - Fork 19
progressive muting implementation #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -223,7 +223,9 @@ | |
| const CUSTOM_CT_ID_PREFIX = '_w_'; | ||
| const BLOCK_REQUEST_COOKIE = 'WZRK_BLOCK'; | ||
|
|
||
| const ISOLATE_COOKIE = 'WZRK_ISOLATE_SD'; // Flag key for Encryption in Transit JSONP fallback (session-level) | ||
| const ISOLATE_COOKIE = 'WZRK_ISOLATE_SD'; // SDK Muting - Progressive muting for churned customers | ||
|
|
||
| const MUTE_EXPIRY_KEY = 'WZRK_MUTE_EXPIRY'; // Flag key for Encryption in Transit JSONP fallback (session-level) | ||
|
|
||
| const CT_EIT_FALLBACK = 'CT_EIT_FALLBACK'; | ||
| const WEB_NATIVE_TEMPLATES = { | ||
|
|
@@ -7582,6 +7584,39 @@ | |
| } | ||
|
|
||
| } | ||
| /** | ||
| * Check if SDK is currently muted (for churned accounts) | ||
| * @returns {boolean} true if SDK is muted and should not send requests | ||
| */ | ||
|
|
||
| const isMuted = () => { | ||
| const muteExpiry = StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY); | ||
|
|
||
| if (!muteExpiry || muteExpiry <= 0) { | ||
| return false; | ||
| } | ||
|
|
||
| return Date.now() < muteExpiry; | ||
| }; | ||
| /** | ||
| * Get the mute expiry timestamp | ||
| * @returns {number|null} epoch timestamp in ms, or null if not muted | ||
| */ | ||
|
|
||
| const getMuteExpiry = () => { | ||
| return StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY) || null; | ||
| }; | ||
| /** | ||
| * Clear mute state (called when account is reactivated) | ||
| */ | ||
|
|
||
| const clearMuteExpiry = () => { | ||
| if (StorageManager._isLocalStorageSupported()) { | ||
| StorageManager.remove(MUTE_EXPIRY_KEY); | ||
| } | ||
|
|
||
| delete $ct.globalCache[MUTE_EXPIRY_KEY]; | ||
| }; | ||
|
Comment on lines
+7613
to
+7619
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find the setMuteExpiry function to see how it stores the mute expiry
rg -n -B2 -A10 'const setMuteExpiry' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 53 🏁 Script executed: # Find StorageManager.remove implementation
rg -n -B2 -A8 'remove\s*[:=].*function|remove\s*\(.*\)\s*{' clevertap.js | head -40Repository: CleverTap/clevertap-web-sdk Length of output: 220 🏁 Script executed: # Find StorageManager.removeCookie implementation
rg -n -B2 -A8 'removeCookie' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 3945 🏁 Script executed: # Find saveToLSorCookie implementation
rg -n -B2 -A15 'saveToLSorCookie' clevertap.js | head -60Repository: CleverTap/clevertap-web-sdk Length of output: 1984 🏁 Script executed: # Find $ct.broadDomain initialization
rg -n 'broadDomain\s*[:=]' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 286 🏁 Script executed: # Find setMuteExpiry function and MUTE_EXPIRY_KEY usage
rg -n -B2 -A10 'setMuteExpiry|MUTE_EXPIRY_KEY' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 2395 🏁 Script executed: # Look at the readFromLSorCookie to understand the dual-storage retrieval
rg -n -B2 -A15 'readFromLSorCookie' clevertap.js | head -80Repository: CleverTap/clevertap-web-sdk Length of output: 2670 🏁 Script executed: # Find StorageManager.remove implementation
rg -n -B2 -A8 'static remove' clevertap.js | grep -A10 'remove\('Repository: CleverTap/clevertap-web-sdk Length of output: 94 🏁 Script executed: # Also check what domain is used when saveToLSorCookie saves to cookies
rg -n -B5 -A10 'createCookie.*property.*encodeURIComponent' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 735 🏁 Script executed: # Find the remove method implementation
rg -n -B2 -A8 'static remove\s*\(' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 318 🏁 Script executed: # Also check what the save() method does
rg -n -B2 -A8 'static save\s*\(' clevertap.js | head -30Repository: CleverTap/clevertap-web-sdk Length of output: 417 🏁 Script executed: # Check if there are other examples of clearing cookies saved via saveToLSorCookie
rg -n -B5 -A5 'saveToLSorCookie' clevertap.js | grep -A10 -B10 'removeCookie\|remove'Repository: CleverTap/clevertap-web-sdk Length of output: 2223 🏁 Script executed: # Check createCookie to understand domain handling
rg -n -B2 -A20 'static createCookie' clevertap.js | head -50Repository: CleverTap/clevertap-web-sdk Length of output: 776 Mute state not cleared from cookies when localStorage is unavailable.
The proposed fix should use Corrected fix const clearMuteExpiry = () => {
if (StorageManager._isLocalStorageSupported()) {
StorageManager.remove(MUTE_EXPIRY_KEY);
+ } else {
+ StorageManager.removeCookie(MUTE_EXPIRY_KEY, window.location.hostname);
}
delete $ct.globalCache[MUTE_EXPIRY_KEY];
};🤖 Prompt for AI Agents |
||
| const $ct = { | ||
| globalCache: { | ||
| gcookie: null, | ||
|
|
@@ -7843,6 +7878,9 @@ | |
|
|
||
|
|
||
| s(global, session, resume, respNumber, optOutResponse) { | ||
| // Clear any existing mute state - receiving a valid session response | ||
| // means the account is active/reactivated | ||
| clearMuteExpiry(); | ||
| let oulReq = false; | ||
| let newGuid = false; // for a scenario when OUL request is true from client side | ||
| // but resume is returned as false from server end | ||
|
|
@@ -7958,6 +7996,21 @@ | |
|
|
||
| $ct.globalCache.RESP_N = respNumber; | ||
| } | ||
| /** | ||
| * Set SDK mute expiry timestamp (for churned accounts) | ||
| * Called by server when account is blocked/muted | ||
| * Server calls: $WZRK_WR.setMuteExpiry(1737100800000) | ||
| * @param {number} muteExpiryMs - Epoch timestamp in ms until which SDK should be muted | ||
| */ | ||
|
|
||
|
|
||
| setMuteExpiry(muteExpiryMs) { | ||
| if (typeof muteExpiryMs === 'number' && muteExpiryMs > 0) { | ||
| StorageManager.saveToLSorCookie(MUTE_EXPIRY_KEY, muteExpiryMs); | ||
|
|
||
| _classPrivateFieldLooseBase(this, _logger$a)[_logger$a].info("SDK muted until: ".concat(new Date(muteExpiryMs).toISOString())); | ||
| } | ||
| } | ||
|
Comment on lines
+7999
to
+8013
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for setMuteExpiry definitions and usages
echo "=== Searching for setMuteExpiry definitions ==="
rg -n 'setMuteExpiry' clevertap.js | head -20
echo ""
echo "=== Searching for $WZRK_WR assignments and setup ==="
rg -n '\$WZRK_WR' clevertap.js | head -30
echo ""
echo "=== Looking for window.$WZRK_WR setup ==="
rg -n 'window\.\$WZRK_WR' clevertap.js | head -20Repository: CleverTap/clevertap-web-sdk Length of output: 674 🏁 Script executed: #!/bin/bash
# Check what 'api' is and how setMuteExpiry class is related
echo "=== Looking for the class containing setMuteExpiry ==="
rg -n -B30 'setMuteExpiry(muteExpiryMs)' clevertap.js | head -60
echo ""
echo "=== Checking the api object definition and what it contains ==="
rg -n 'const api' clevertap.js
rg -n 'let api' clevertap.js
rg -n 'var api' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 261 🏁 Script executed: #!/bin/bash
# Read the context around the setMuteExpiry method definition
echo "=== Context around setMuteExpiry method (lines 7950-7980) ==="
sed -n '7950,7980p' clevertap.js
echo ""
echo "=== Find the class that contains this method ==="
sed -n '7900,7970p' clevertap.js | grep -n 'class\|setMuteExpiry'
echo ""
echo "=== Check what api object is assigned to $WZRK_WR ==="
sed -n '18300,18320p' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 2972 🏁 Script executed: #!/bin/bash
# Search for where methods are assigned to the api object
echo "=== Find all api.* assignments ==="
rg -n 'api\.' clevertap.js | grep -E '(api\.\w+\s*=|getMuteExpiry|isMuted|setMuteExpiry)' | head -30
echo ""
echo "=== Look at the full context of api object setup (lines 18200-18330) ==="
sed -n '18200,18330p' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 5241 🏁 Script executed: #!/bin/bash
# Search for setMuteExpiry assignment to api object
echo "=== Search for setMuteExpiry being assigned to api ==="
rg -n 'api\.setMuteExpiry' clevertap.js
echo ""
echo "=== Check if setMuteExpiry is bound to the api object anywhere ==="
rg -n 'setMuteExpiry' clevertap.js -A3 -B3 | head -50
echo ""
echo "=== Look at complete api assignments section (18300-18320) ==="
sed -n '18300,18320p' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 1460 🏁 Script executed: #!/bin/bash
# Find which class contains the setMuteExpiry method
echo "=== Find the class definition containing setMuteExpiry ==="
sed -n '7800,7970p' clevertap.js | grep -n 'class\|setMuteExpiry' | tail -20
echo ""
echo "=== Look for where 'this' refers in context, find class name ==="
sed -n '7700,7800p' clevertap.js | grep -n '^class\|^ *class'
echo ""
echo "=== Check the structure: is setMuteExpiry on the main class? ==="
sed -n '7960,7980p' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 3164 🏁 Script executed: #!/bin/bash
# Confirm that setMuteExpiry is not in the api assignments
echo "=== Complete api assignments for muting-related functions ==="
sed -n '18299,18315p' clevertap.js
echo ""
echo "=== Verify setMuteExpiry is a method on CleverTapAPI ==="
sed -n '7968,7973p' clevertap.jsRepository: CleverTap/clevertap-web-sdk Length of output: 924
The method is defined on the This assignment needs to be added in the api setup section around line 18310. 🤖 Prompt for AI Agents |
||
|
|
||
| } | ||
|
|
||
|
|
@@ -9482,6 +9535,13 @@ | |
| var _fireRequest2 = function _fireRequest2(url, tries, skipARP, sendOULFlag, evtName) { | ||
| var _window$location$orig, _window, _window$location, _window2, _window2$location, _window$clevertap, _window$wizrocket; | ||
|
|
||
| // Check if SDK is muted (for churned accounts) - drop request silently | ||
| if (isMuted()) { | ||
| const muteExpiry = getMuteExpiry(); | ||
| this.logger.debug('Request dropped - SDK is muted until ' + new Date(muteExpiry).toISOString()); | ||
| return; | ||
| } | ||
|
|
||
| if (_classPrivateFieldLooseBase(this, _dropRequestDueToOptOut)[_dropRequestDueToOptOut]()) { | ||
| this.logger.debug('req dropped due to optout cookie: ' + this.device.gcookie); | ||
| return; | ||
|
|
@@ -18070,6 +18130,14 @@ | |
|
|
||
|
|
||
| saveAndFireRequest(url, override, sendOULFlag, evtName) { | ||
| // Check if SDK is muted (for churned accounts) - drop request silently | ||
| // Unlike offline mode, muted requests are NOT saved to backup | ||
| if (isMuted()) { | ||
| _classPrivateFieldLooseBase(this, _logger$3)[_logger$3].debug('Request dropped - SDK is muted'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const now = getNow(); // Get the next available request number that doesn't conflict with existing backups | ||
|
|
||
| const nextReqN = _classPrivateFieldLooseBase(this, _getNextAvailableReqN)[_getNextAvailableReqN](); | ||
|
|
@@ -19694,6 +19762,15 @@ | |
| if (profile[categoryLongKey]) { | ||
| $ct.updatedCategoryLong = profile[categoryLongKey]; | ||
| } | ||
| }; // SDK Muting - for churned accounts (progressive muting) | ||
|
|
||
|
|
||
| api.getMuteExpiry = () => { | ||
| return getMuteExpiry(); | ||
| }; | ||
|
|
||
| api.isMuted = () => { | ||
| return isMuted(); | ||
| }; | ||
|
|
||
| window.$CLTP_WR = window.$WZRK_WR = api; | ||
|
|
||
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
|
|
||
| import { ARP_COOKIE, MAX_TRIES, OPTOUT_COOKIE_ENDSWITH, USEIP_KEY, MAX_DELAY_FREQUENCY, PUSH_DELAY_MS, WZRK_FETCH, CT_EIT_FALLBACK } from './constants' | ||
| import { ARP_COOKIE, MAX_TRIES, OPTOUT_COOKIE_ENDSWITH, USEIP_KEY, MAX_DELAY_FREQUENCY, PUSH_DELAY_MS, WZRK_FETCH, CT_EIT_FALLBACK, MUTE_EXPIRY_KEY } from './constants' | ||
| import { isString, isValueValid } from './datatypes' | ||
| import { compressData } from './encoder' | ||
| import { StorageManager, $ct } from './storage' | ||
| import { StorageManager, $ct, isMuted, getMuteExpiry } from './storage' | ||
| import { addToURL } from './url' | ||
| import encryptionInTransitInstance from './security/encryptionInTransit' | ||
|
|
||
|
|
@@ -136,6 +136,13 @@ export default class RequestDispatcher { | |
|
|
||
| // ANCHOR - Requests get fired from here | ||
| static #fireRequest (url, tries, skipARP, sendOULFlag, evtName) { | ||
| // Check if SDK is muted (for churned accounts) - drop request silently | ||
| if (isMuted()) { | ||
| const muteExpiry = getMuteExpiry() | ||
| this.logger.debug('Request dropped - SDK is muted until ' + new Date(muteExpiry).toISOString()) | ||
| return | ||
| } | ||
|
|
||
| if (this.#dropRequestDueToOptOut()) { | ||
| this.logger.debug('req dropped due to optout cookie: ' + this.device.gcookie) | ||
| return | ||
|
|
@@ -281,6 +288,17 @@ export default class RequestDispatcher { | |
|
|
||
| fetch(encryptedUrl, fetchOptions) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add timeout to fetch request. The fetch call has no timeout and could hang indefinitely on network issues, leading to poor user experience. 🔧 Proposed fix using AbortController+ static FETCH_TIMEOUT_MS = 30000
+
static handleFetchResponse (encryptedUrl, originalUrl, retryCount = 0) {
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), this.FETCH_TIMEOUT_MS)
+
const fetchOptions = {
method: 'GET',
headers: {
Accept: 'application/json',
'X-CleverTap-Encryption-Enabled': 'true'
- }
+ },
+ signal: controller.signal
}
fetch(encryptedUrl, fetchOptions)
.then((response) => {
+ clearTimeout(timeoutId)🤖 Prompt for AI Agents |
||
| .then((response) => { | ||
| // Check for SDK mute headers (progressive muting for churned accounts) | ||
| // X-WZRK-MUTE-DURATION contains epoch timestamp in ms | ||
| const muteDurationHeader = response.headers.get('X-WZRK-MUTE-DURATION') | ||
| if (muteDurationHeader) { | ||
| const muteExpiryMs = parseInt(muteDurationHeader, 10) | ||
| if (!isNaN(muteExpiryMs) && muteExpiryMs > 0) { | ||
| StorageManager.saveToLSorCookie(MUTE_EXPIRY_KEY, muteExpiryMs) | ||
| this.logger.info(`SDK muted until: ${new Date(muteExpiryMs).toISOString()}`) | ||
| } | ||
| } | ||
|
|
||
| if (!response.ok) { | ||
| // Handle 402 (Payment Required) or 419: Encryption not enabled for account | ||
| if (response.status === 402 || response.status === 419) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,8 @@ import { | |||||||||||||||||||||||||||||||
| KCOOKIE_NAME, | ||||||||||||||||||||||||||||||||
| LCOOKIE_NAME, | ||||||||||||||||||||||||||||||||
| BLOCK_REQUEST_COOKIE, | ||||||||||||||||||||||||||||||||
| ISOLATE_COOKIE | ||||||||||||||||||||||||||||||||
| ISOLATE_COOKIE, | ||||||||||||||||||||||||||||||||
| MUTE_EXPIRY_KEY | ||||||||||||||||||||||||||||||||
| } from './constants' | ||||||||||||||||||||||||||||||||
| import encryption from '../modules/security/Encryption' | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -306,6 +307,26 @@ export class StorageManager { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Check if SDK is currently muted (for churned accounts) | ||||||||||||||||||||||||||||||||
| * @returns {boolean} true if SDK is muted and should not send requests | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| export const isMuted = () => { | ||||||||||||||||||||||||||||||||
| const muteExpiry = StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY) | ||||||||||||||||||||||||||||||||
| if (!muteExpiry || muteExpiry <= 0) { | ||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return Date.now() < muteExpiry | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Get the mute expiry timestamp | ||||||||||||||||||||||||||||||||
| * @returns {number|null} epoch timestamp in ms, or null if not muted | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| export const getMuteExpiry = () => { | ||||||||||||||||||||||||||||||||
| return StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY) || null | ||||||||||||||||||||||||||||||||
|
Comment on lines
+323
to
+327
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align Right now an expired timestamp still gets returned. Either update the JSDoc or guard against expired values so callers don’t treat an expired mute as active. ✅ Suggested fix (return null when expired) export const getMuteExpiry = () => {
- return StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY) || null
+ const muteExpiry = StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY)
+ if (!muteExpiry || muteExpiry <= 0) {
+ return null
+ }
+ return Date.now() < muteExpiry ? muteExpiry : null
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export const $ct = { | ||||||||||||||||||||||||||||||||
| globalCache: { | ||||||||||||||||||||||||||||||||
| gcookie: null, | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment on
MUTE_EXPIRY_KEYis incorrect.The comment says "Flag key for Encryption in Transit JSONP fallback" but
MUTE_EXPIRY_KEYis for SDK muting (progressive muting for churned customers).CT_EIT_FALLBACKon line 230 is the actual EIT fallback key.Proposed fix
🤖 Prompt for AI Agents