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
79 changes: 78 additions & 1 deletion clevertap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Comment on lines +228 to +230
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment on MUTE_EXPIRY_KEY is incorrect.

The comment says "Flag key for Encryption in Transit JSONP fallback" but MUTE_EXPIRY_KEY is for SDK muting (progressive muting for churned customers). CT_EIT_FALLBACK on line 230 is the actual EIT fallback key.

Proposed fix
-  const MUTE_EXPIRY_KEY = 'WZRK_MUTE_EXPIRY'; // Flag key for Encryption in Transit JSONP fallback (session-level)
-
-  const CT_EIT_FALLBACK = 'CT_EIT_FALLBACK';
+  // 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';
🤖 Prompt for AI Agents
In `@clevertap.js` around lines 228 - 230, The comment for MUTE_EXPIRY_KEY is
incorrect; update the comment next to the constant MUTE_EXPIRY_KEY to describe
that it is the flag key used for SDK muting/progressive muting (for churned
customers) rather than "Encryption in Transit JSONP fallback", and if needed
move or add the EIT fallback description to the CT_EIT_FALLBACK constant so
CT_EIT_FALLBACK is documented as the Encryption in Transit JSONP fallback key;
update only the inline comments for MUTE_EXPIRY_KEY and CT_EIT_FALLBACK to
reflect these correct responsibilities.

const WEB_NATIVE_TEMPLATES = {
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the setMuteExpiry function to see how it stores the mute expiry
rg -n -B2 -A10 'const setMuteExpiry' clevertap.js

Repository: 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 -40

Repository: CleverTap/clevertap-web-sdk

Length of output: 220


🏁 Script executed:

# Find StorageManager.removeCookie implementation
rg -n -B2 -A8 'removeCookie' clevertap.js

Repository: CleverTap/clevertap-web-sdk

Length of output: 3945


🏁 Script executed:

# Find saveToLSorCookie implementation
rg -n -B2 -A15 'saveToLSorCookie' clevertap.js | head -60

Repository: CleverTap/clevertap-web-sdk

Length of output: 1984


🏁 Script executed:

# Find $ct.broadDomain initialization
rg -n 'broadDomain\s*[:=]' clevertap.js

Repository: 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.js

Repository: 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 -80

Repository: 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.js

Repository: CleverTap/clevertap-web-sdk

Length of output: 735


🏁 Script executed:

# Find the remove method implementation
rg -n -B2 -A8 'static remove\s*\(' clevertap.js

Repository: 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 -30

Repository: 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 -50

Repository: CleverTap/clevertap-web-sdk

Length of output: 776


Mute state not cleared from cookies when localStorage is unavailable.

setMuteExpiry uses saveToLSorCookie which falls back to cookies (domain: window.location.hostname) when localStorage is unavailable. However, clearMuteExpiry only removes from localStorage via StorageManager.remove(), leaving the mute state persisted in cookies. This means accounts cannot be "unmuted" on devices without localStorage support.

The proposed fix should use window.location.hostname as the domain (matching where the cookie is stored), not $ct.broadDomain:

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
In `@clevertap.js` around lines 7574 - 7580, clearMuteExpiry currently only
removes MUTE_EXPIRY_KEY from localStorage and globalCache, leaving a cookie set
by saveToLSorCookie when localStorage is unavailable; update clearMuteExpiry to
also remove the cookie using the same domain used by saveToLSorCookie
(window.location.hostname) rather than $ct.broadDomain, ensuring you reference
MUTE_EXPIRY_KEY and remove the cookie before deleting
$ct.globalCache[MUTE_EXPIRY_KEY]; keep the StorageManager.remove call for
localStorage support intact.

const $ct = {
globalCache: {
gcookie: null,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: 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.js

Repository: 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.js

Repository: 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.js

Repository: 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.js

Repository: 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.js

Repository: 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.js

Repository: CleverTap/clevertap-web-sdk

Length of output: 924


setMuteExpiry() is not exposed to $WZRK_WR and cannot be called from the server as documented.

The method is defined on the CleverTapAPI class but is missing from the api object assignments. Only getMuteExpiry and isMuted are exposed to window.$WZRK_WR (lines 18306-18310). The setMuteExpiry method must be assigned to the api object for server-side calls to work, e.g.:

api.setMuteExpiry = this.setMuteExpiry;

This assignment needs to be added in the api setup section around line 18310.

🤖 Prompt for AI Agents
In `@clevertap.js` around lines 7960 - 7974, The setMuteExpiry method on
CleverTapAPI is not being exposed on the api object used to populate
window.$WZRK_WR, so server calls cannot reach it; add an assignment to expose it
(e.g., assign api.setMuteExpiry = this.setMuteExpiry) alongside the existing
api.getMuteExpiry and api.isMuted bindings in the api setup where the $WZRK_WR
methods are assembled so the server can call
CleverTapAPI.prototype.setMuteExpiry; ensure the binding references the same
instance (this) as the other methods.


}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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]();
Expand Down Expand Up @@ -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;
Expand Down
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.

9 changes: 8 additions & 1 deletion src/clevertap.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
ISOLATE_COOKIE
} from './util/constants'
import { EMBED_ERROR } from './util/messages'
import { StorageManager, $ct } from './util/storage'
import { StorageManager, $ct, getMuteExpiry, isMuted } from './util/storage'
import { addToURL, getDomain, getURLParams } from './util/url'
import { getCampaignObjForLc, setEnum, handleEmailSubscription, closeIframe } from './util/clevertap'
import { compressData } from './util/encoder'
Expand Down Expand Up @@ -695,6 +695,13 @@ export default class CleverTap {
$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

if (clevertap.account?.[0].id) {
Expand Down
9 changes: 8 additions & 1 deletion src/modules/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isObjectEmpty, isValueValid, removeUnsupportedChars, safeJSONParse } fr
import { getNow } from '../util/datetime'
import { compressData } from '../util/encoder'
import RequestDispatcher from '../util/requestDispatcher'
import { StorageManager, $ct } from '../util/storage'
import { StorageManager, $ct, isMuted } from '../util/storage'
import { addToURL } from '../util/url'
import { getCampaignObjForLc } from '../util/clevertap'

Expand Down Expand Up @@ -139,6 +139,13 @@ export default class RequestManager {
* @param {Boolean} sendOULFlag - true in case of a On User Login request
*/
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()) {
this.#logger.debug('Request dropped - SDK is muted')
return
}

const now = getNow()

// Get the next available request number that doesn't conflict with existing backups
Expand Down
2 changes: 2 additions & 0 deletions src/util/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export const ENCRYPTION_KEY_NAME = 'WZRK_ENCRYPTION_KEY'
// Flag key for optional sub-domain profile isolation
export const ISOLATE_COOKIE = 'WZRK_ISOLATE_SD'

// SDK Muting - Progressive muting for churned customers
export const MUTE_EXPIRY_KEY = 'WZRK_MUTE_EXPIRY'
// Flag key for Encryption in Transit JSONP fallback (session-level)
export const CT_EIT_FALLBACK = 'CT_EIT_FALLBACK'

Expand Down
22 changes: 20 additions & 2 deletions src/util/requestDispatcher.js
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'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -281,6 +288,17 @@ export default class RequestDispatcher {

fetch(encryptedUrl, fetchOptions)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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
In `@src/util/requestDispatcher.js` at line 289, The fetch(encryptedUrl,
fetchOptions) call currently has no timeout; wrap it with an AbortController:
create a controller, add its signal to fetchOptions (signal: controller.signal),
start a setTimeout (e.g., 10000 ms or a configurable constant) that calls
controller.abort() to cancel the request, clear that timeout on successful
resolution or rejection, and ensure you handle the abort error path (e.g., treat
as timeout) in the same function where fetch(encryptedUrl, fetchOptions) is
invoked so callers receive a clear timeout error instead of hanging.

.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) {
Expand Down
23 changes: 22 additions & 1 deletion src/util/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align getMuteExpiry with its JSDoc (“null if not muted”).

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* 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
* Get the mute expiry timestamp
* `@returns` {number|null} epoch timestamp in ms, or null if not muted
*/
export const getMuteExpiry = () => {
const muteExpiry = StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY)
if (!muteExpiry || muteExpiry <= 0) {
return null
}
return Date.now() < muteExpiry ? muteExpiry : null
}
🤖 Prompt for AI Agents
In `@src/util/storage.js` around lines 323 - 327, getMuteExpiry currently returns
whatever StorageManager.readFromLSorCookie(MUTE_EXPIRY_KEY) provides (possibly
an expired timestamp or string); change it so it reads the stored value,
coerces/parses it to a numeric epoch ms, and returns null if missing or if
parsed value <= Date.now() (otherwise return the numeric timestamp). Update
getMuteExpiry to use MUTE_EXPIRY_KEY and StorageManager.readFromLSorCookie,
guard against non-numeric values, and ensure callers receive null for expired or
absent mutes.

}

export const $ct = {
globalCache: {
gcookie: null,
Expand Down
Loading