fix: use cookie/urlParam-based fallback by default with redirect action#328
fix: use cookie/urlParam-based fallback by default with redirect action#328tyiuhc wants to merge 107 commits into
Conversation
… sites Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unconditional
console.errorcalls pollute customer browser consoles- Removed all three console.error calls in cookie operations (storeRedirectImpressions, fireStoredRedirectImpressions, and cleanup) and replaced them with silent failure as the cookie storage is a non-critical fallback mechanism.
Or push these changes by commenting:
@cursor push 9ead4e2377
Preview (9ead4e2377)
diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts
--- a/packages/experiment-tag/src/experiment.ts
+++ b/packages/experiment-tag/src/experiment.ts
@@ -1179,11 +1179,8 @@
const redirects = storedRedirects || {};
redirects[flagKey] = impression;
await storage.set(storageKey, redirects);
- } catch (error) {
- console.error(
- `Failed to store redirect impression in cookie for ${flagKey}:`,
- error,
- );
+ } catch {
+ // Fail silently - cookie storage is a fallback mechanism
}
}
@@ -1225,11 +1222,8 @@
});
try {
cookieImpressions = (await cookieStorage.get(storageKey)) || {};
- } catch (error) {
- console.error(
- `Failed to retrieve redirect impressions from cookie ${storageKey}:`,
- error,
- );
+ } catch {
+ // Fail silently - cookie storage is a fallback mechanism
}
// Merge with priority: url > session > cookie
@@ -1261,11 +1255,8 @@
const cleanup = async () => {
removeStorageItem('sessionStorage', storageKey);
- await cookieStorage.remove(storageKey).catch((error) => {
- console.error(
- `Failed to remove redirect impressions from cookie ${storageKey}:`,
- error,
- );
+ await cookieStorage.remove(storageKey).catch(() => {
+ // Fail silently - cookie storage is a fallback mechanism
});
};You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Redirect config flags ignored
- Added guards checking
this.config.redirectConfig?.encodeRedirectInUrl !== falseandthis.config.redirectConfig?.encodeRedirectInCookie !== falseto respect explicit opt-out while maintaining default-enabled behavior.
- Added guards checking
Or push these changes by commenting:
@cursor push 9f8b10e1b1
Preview (9f8b10e1b1)
diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts
--- a/packages/experiment-tag/src/experiment.ts
+++ b/packages/experiment-tag/src/experiment.ts
@@ -909,29 +909,33 @@
// Embed impression data in redirect URL for cross-domain and
// cookie-blocked environments. Merge with any existing param in case
// multiple redirect experiments fire in sequence.
- const targetUrlObj = new URL(targetUrl);
- let urlPayload: Record<string, StoredRedirectImpression> = {};
- const existingEncoded = targetUrlObj.searchParams.get(
- REDIRECT_IMPRESSION_PARAM,
- );
- if (existingEncoded) {
- try {
- urlPayload = JSON.parse(atob(existingEncoded));
- } catch (error) {
- console.error('Failed to decode existing AMP_REDIRECT param:', error);
+ if (this.config.redirectConfig?.encodeRedirectInUrl !== false) {
+ const targetUrlObj = new URL(targetUrl);
+ let urlPayload: Record<string, StoredRedirectImpression> = {};
+ const existingEncoded = targetUrlObj.searchParams.get(
+ REDIRECT_IMPRESSION_PARAM,
+ );
+ if (existingEncoded) {
+ try {
+ urlPayload = JSON.parse(atob(existingEncoded));
+ } catch (error) {
+ console.error('Failed to decode existing AMP_REDIRECT param:', error);
+ }
}
+ urlPayload[flagKey] = {
+ redirectUrl,
+ variantKey: variant.key || '',
+ ...(variant.expKey !== undefined ? { expKey: variant.expKey } : {}),
+ ...(variant.metadata !== undefined
+ ? { metadata: variant.metadata }
+ : {}),
+ };
+ targetUrlObj.searchParams.set(
+ REDIRECT_IMPRESSION_PARAM,
+ btoa(JSON.stringify(urlPayload)),
+ );
+ targetUrl = targetUrlObj.toString();
}
- urlPayload[flagKey] = {
- redirectUrl,
- variantKey: variant.key || '',
- ...(variant.expKey !== undefined ? { expKey: variant.expKey } : {}),
- ...(variant.metadata !== undefined ? { metadata: variant.metadata } : {}),
- };
- targetUrlObj.searchParams.set(
- REDIRECT_IMPRESSION_PARAM,
- btoa(JSON.stringify(urlPayload)),
- );
- targetUrl = targetUrlObj.toString();
// set previous url - relevant for SPA if redirect happens before push/replaceState is complete
this.previousUrl = this.globalScope.location.href;
@@ -1169,25 +1173,29 @@
setStorageItem('sessionStorage', storageKey, stored);
// Also write to cookie to enable cross-subdomain tracking
- const domain = await getTopLevelDomain(this.globalScope.location.hostname);
- const storage = new CookieStorage<Record<string, StoredRedirectImpression>>(
- {
+ if (this.config.redirectConfig?.encodeRedirectInCookie !== false) {
+ const domain = await getTopLevelDomain(
+ this.globalScope.location.hostname,
+ );
+ const storage = new CookieStorage<
+ Record<string, StoredRedirectImpression>
+ >({
...(domain && { domain }),
sameSite: 'Lax',
expirationDays: 1 / 1440, // 1 minute
- },
- );
+ });
- try {
- const storedRedirects = await storage.get(storageKey);
- const redirects = storedRedirects || {};
- redirects[flagKey] = impression;
- await storage.set(storageKey, redirects);
- } catch (error) {
- console.error(
- `Failed to store redirect impression in cookie for ${flagKey}:`,
- error,
- );
+ try {
+ const storedRedirects = await storage.get(storageKey);
+ const redirects = storedRedirects || {};
+ redirects[flagKey] = impression;
+ await storage.set(storageKey, redirects);
+ } catch (error) {
+ console.error(
+ `Failed to store redirect impression in cookie for ${flagKey}:`,
+ error,
+ );
+ }
}
}You can send follow-ups to the cloud agent here.
528f239 to
ce31466
Compare
… unload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Redirect stall hangs startup
- Removed the never-resolving promise after location.replace() and replaced it with a simple return statement, allowing startup to complete normally when navigation doesn't occur immediately.
Or push these changes by commenting:
@cursor push cc22a41767
Preview (cc22a41767)
diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts
--- a/packages/experiment-tag/src/experiment.ts
+++ b/packages/experiment-tag/src/experiment.ts
@@ -999,10 +999,7 @@
return;
}
this.globalScope.location.replace(targetUrl);
- // location.replace() queues navigation but JS continues — stall so applyVariants
- // never resolves and removeAntiFlickerCss() is never called during the unload window.
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- await new Promise(() => {});
+ return;
}
private handleMutate(action, flagKey: string, variant: Variant) {You can send follow-ups to the cloud agent here.
…g promise Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Custom redirect skips remote flags
- Moved isRedirecting = true to only execute after the custom handler check, ensuring SPAs using custom redirect handlers will continue to fetch remote flags after client-side navigation.
Or push these changes by commenting:
@cursor push ba7cf0cad9
Preview (ba7cf0cad9)
diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts
--- a/packages/experiment-tag/src/experiment.ts
+++ b/packages/experiment-tag/src/experiment.ts
@@ -995,14 +995,18 @@
// set previous url - relevant for SPA if redirect happens before push/replaceState is complete
this.previousUrl = this.globalScope.location.href;
await setMarketingCookie(this.apiKey, this.globalScope.location.hostname);
- // Mark redirect as in-flight so start() skips removeAntiFlickerCss and
- // further processing after applyVariants returns.
- this.isRedirecting = true;
// perform redirection
if (this.customRedirectHandler) {
+ // Custom redirect handlers may perform client-side routing without a
+ // page reload, so we should NOT set isRedirecting. This allows start()
+ // to continue and fetch remote flags after the SPA navigation.
this.customRedirectHandler(targetUrl);
return;
}
+ // Mark redirect as in-flight so start() skips removeAntiFlickerCss and
+ // further processing after applyVariants returns. Only set for full-page
+ // navigation since the page will unload anyway.
+ this.isRedirecting = true;
this.globalScope.location.replace(targetUrl);
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 27bd6d0. Configure here.
| } | ||
|
|
||
| if ( | ||
| this.isRedirecting || |
There was a problem hiding this comment.
Custom redirect skips remote flags
Medium Severity
When a redirect action runs during start(), isRedirecting makes start() return early with isRunning true and never calls fetchRemoteFlags. That fits full-page navigation, but if setRedirectHandler performs client-side routing without a reload, remote experiments never load for the rest of the session because nothing else fetches remote flags.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 27bd6d0. Configure here.



Summary
Branch script to use cookie+urlParam-based fallback for redirect actions by default.
Checklist
Note
Medium Risk
Default-on
AMP_REDIRECTchanges production redirect URLs and impression plumbing;start()now exits early when a redirect fires during variant apply, altering remote flag fetch timing.Overview
Default redirect impression fallback now enables URL encoding:
Defaults.redirectConfig.encodeRedirectInUrlistrue(wasfalse), so cross-subdomain redirects embedAMP_REDIRECTon the destination URL by default, alongside the existing cookie-based path.Redirect-in-flight handling adds
isRedirecting, set inhandleRedirectbefore navigation. Duringstart(), that flag keeps anti-flicker CSS in place and short-circuits after local/preview variant application—skippingremoveAntiFlickerCss(when not remote-blocking) and not fetching/applying remote flags while a redirect is pending.Reviewed by Cursor Bugbot for commit 27bd6d0. Bugbot is set up for automated code reviews on this repo. Configure here.