Skip to content

Conversation

@charliecreates
Copy link
Contributor

Configure client-side opt-in for Netlify Split Testing so devs can preview a branch deploy on the main domain (no redirects) while preserving local storage. Adds pre-mount cookie setter and docs.

Context

We need proxy-level routing between branch deploys without changing origin so browser storage persists. Netlify Split Testing uses the nf_ab cookie to bucket visitors and serves all content for that visitor from the selected branch deploy on the primary domain.

Changes

  • Add a tiny inline script in app/root.tsx that runs in <head> before the app mounts:
    • Reads ?nf_ab=<branch> (or ?ab=<branch>) to set the nf_ab cookie and reload once.
    • Supports opt-out via ?nf_ab=clear (aliases: off, reset, none) and reloads once.
    • Removes the query params after applying to avoid loops / messy URLs.
  • Add docs at docs/split-testing.md with usage, opt-out, and limitations.
  • Link the docs from README.

Notes / limitations

  • Split Testing is configured at the site level in Netlify (UI). Set traffic split to any values (even 0/100); cookie assignment still forces routing.
  • Split Testing only supports branch deploys; Deploy Previews cannot be targeted by nf_ab.
  • When Split Testing is enabled for a site, Netlify does not execute Edge Functions for that site. We currently have an Edge Function mapped at /vibe/* and /firehose; confirm this trade-off is acceptable when Split Testing is active.

Open questions (need confirmation to finalize)

  • Which branch deploy(s) should be included as test candidates right now? (e.g., main and which experimental branch?)
  • Are the query param names okay (nf_ab and ab), or do you want a different alias?

Verification

pnpm check
# result: format ok; types ok; tests: 362 passed, 4 skipped

…t Testing; docs for opt-in/opt-out and limitations
@charliecreates charliecreates bot requested a review from CharlieHelps August 10, 2025 23:36
@netlify
Copy link

netlify bot commented Aug 10, 2025

Deploy Preview for fireproof-ai-builder ready!

Name Link
🔨 Latest commit 970e212
🔍 Latest deploy log https://app.netlify.com/projects/fireproof-ai-builder/deploys/68993c2a680cc7000884f4a7
😎 Deploy Preview https://deploy-preview-207--fireproof-ai-builder.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor Author

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

  • Cookie parsing uses a rigid regex that can miss the cookie if document.cookie lacks spaces after semicolons; make whitespace optional.
  • The script forces a reload even when nothing changes and leaves query params in the URL when values already match; prefer always cleaning the URL and only reloading when the cookie actually changes.
  • Inline dangerouslySetInnerHTML may violate strict CSP; consider externalizing the script or adding a nonce.
  • Optional: support a cookie Domain attribute to improve cross-subdomain behavior and document this host-scoping caveat.
Additional notes (2)
  • Compatibility | app/root.tsx:116-130
    If your site is accessible via both apex and subdomains (e.g., vibes.diy and www.vibes.diy), the cookie as set (no Domain attribute) will be host-scoped and not shared across hosts. Consider supporting an optional Domain attribute so teams can opt into apex-wide scoping when needed.

  • Readability | docs/split-testing.md:35-40
    Consider documenting that the nf_ab cookie is host-scoped by default and won’t apply across apex/subdomains unless you configure a Domain attribute. This can save debugging time if folks switch between www and apex URLs.

Summary of changes
  • Added a new "Developer previews on the main domain (no redirects)" section to README linking to Split Testing docs.
  • Inserted an inline, pre-mount script in app/root.tsx <head> that reads ?nf_ab/?ab query params to set/clear the nf_ab cookie and reload once, then removes those params from the URL.
  • Added docs/split-testing.md describing how to opt-in/out, limitations, and configuration notes for Netlify Split Testing.

app/root.tsx Outdated
Comment on lines 119 to 121
var currentMatch = document.cookie.match(/(?:^|; )nf_ab=([^;]+)/);
var current = currentMatch ? decodeURIComponent(currentMatch[1]) : null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The cookie parsing regex assumes a space after semicolons in document.cookie. Some environments omit that space, which can cause the current cookie to be missed and lead to unnecessary reloads or incorrect comparisons. Make the whitespace optional to be robust across browsers.

Suggestion

Update the regex to allow optional whitespace after semicolons:

  • Current: /(:?^|; )nf_ab=([^;]+)/
  • Suggested: /(:?^|;\s*)nf_ab=([^;]+)/

Example change:

- var currentMatch = document.cookie.match(/(?:^|; )nf_ab=([^;]+)/);
+ var currentMatch = document.cookie.match(/(?:^|;\s*)nf_ab=([^;]+)/);

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion

app/root.tsx Outdated
Comment on lines 133 to 139
if (changed) {
// Remove our params to avoid reload loops and keep nice URLs
sp.delete('nf_ab');
sp.delete('ab');
var newUrl = u.origin + u.pathname + (sp.toString() ? ('?' + sp.toString()) : '') + u.hash;
window.location.replace(newUrl);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When clearing with ?nf_ab=clear but no cookie is set, the script still forces a full reload. Also, when the desired branch already matches the current cookie, the URL params are left in place unless a change occurred. You can avoid an unnecessary reload and still clean the URL by always removing the params and only reloading when the cookie actually changes.

Suggestion

Refactor to always remove the params and only reload when the cookie value changes (set-to-new or cleared-when-present):

-    if (changed) {
-      // Remove our params to avoid reload loops and keep nice URLs
-      sp.delete('nf_ab');
-      sp.delete('ab');
-      var newUrl = u.origin + u.pathname + (sp.toString() ? ('?' + sp.toString()) : '') + u.hash;
-      window.location.replace(newUrl);
-    }
+    // Decide whether we need a reload
+    var shouldReload = false;
+    if (clear) {
+      if (current) shouldReload = true; // only reload if something actually changed
+    } else if (value && value !== current) {
+      shouldReload = true;
+    }
+
+    // Remove our params regardless to keep URLs clean
+    sp.delete('nf_ab');
+    sp.delete('ab');
+    var newUrl = u.origin + u.pathname + (sp.toString() ? ('?' + sp.toString()) : '') + u.hash;
+    if (shouldReload) {
+      window.location.replace(newUrl);
+    } else if (hasNf || hasAb) {
+      // Clean the URL without a reload if nothing changed
+      history.replaceState(null, '', newUrl);
+    }

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion

app/root.tsx Outdated
Comment on lines 102 to 145
<script
// Intentionally inline and first in <head> to run before framework mount
dangerouslySetInnerHTML={{
__html: `(() => {
try {
var u = new URL(window.location.href);
var sp = u.searchParams;
var hasNf = sp.has('nf_ab');
var hasAb = sp.has('ab');
if (!hasNf && !hasAb) return;
var value = hasNf ? sp.get('nf_ab') : sp.get('ab');
var clear = value && /^(clear|off|reset|none)$/i.test(value);
var secure = window.location.protocol === 'https:' ? '; Secure' : '';
var cookieBase = 'nf_ab=';
var currentMatch = document.cookie.match(/(?:^|; )nf_ab=([^;]+)/);
var current = currentMatch ? decodeURIComponent(currentMatch[1]) : null;
var changed = false;
if (clear) {
document.cookie = 'nf_ab=; Max-Age=0; Path=/; SameSite=Lax' + secure;
changed = true;
} else if (value && value !== current) {
// Long-ish expiration so the choice sticks for developers
var expires = new Date(Date.now() + 365*24*60*60*1000).toUTCString();
document.cookie = cookieBase + encodeURIComponent(value) + '; Expires=' + expires + '; Path=/; SameSite=Lax' + secure;
changed = true;
}
if (changed) {
// Remove our params to avoid reload loops and keep nice URLs
sp.delete('nf_ab');
sp.delete('ab');
var newUrl = u.origin + u.pathname + (sp.toString() ? ('?' + sp.toString()) : '') + u.hash;
window.location.replace(newUrl);
}
} catch (e) {
// Swallow errors to avoid blocking page load in edge cases
}
})();`,
}}
/>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This uses an inline script via dangerouslySetInnerHTML. If a Content Security Policy is enforced without 'unsafe-inline', this will be blocked or force CSP weakening. Consider externalizing this script (served from the same origin) so CSP can remain strict, or threading a nonce through and adding it to the script tag.

Suggestion

Option A (preferred for CSP): move the IIFE into a small static file and reference it:

  • Add public/nf-ab.cookie.js with the current IIFE contents.
  • Replace the inline tag with:
- <script
-   // Intentionally inline and first in <head> to run before framework mount
-   dangerouslySetInnerHTML={{ __html: `(/* IIFE */)` }}
- />
+ <script src="/nf-ab.cookie.js"></script>

Option B: add a CSP nonce to the inline script tag and thread it from the server:

- <script dangerouslySetInnerHTML={{ __html: `(/* IIFE */)` }} />
+ <script nonce={cspNonce} dangerouslySetInnerHTML={{ __html: `(/* IIFE */)` }} />

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with Option A (extracting to public/nf-ab.cookie.js and wiring the <script src>).

@charliecreates charliecreates bot removed the request for review from CharlieHelps August 10, 2025 23:39
@jchris
Copy link
Contributor

jchris commented Aug 10, 2025

@CharlieHelps apply your suggestions

@charliecreates
Copy link
Contributor Author

charliecreates bot commented Aug 10, 2025

Expand this to see my work.
  • Added public/nf-ab.cookie.js implementing improved cookie regex and reload logic (matches optional whitespace, removes URL params, avoids spurious reloads).
  • Replaced inline script with CSP-safe <script src='/nf-ab.cookie.js'></script> in app/root.tsx to ensure pre-mount execution with CSP.
  • Documented host-only nf_ab cookie scoping plus apex domain trade-offs in docs/split-testing.md near usage guidance.
  • Synced README.md to reference updated split testing documentation and highlight default cookie scope.
  • Verified all changes pass formatting, type, and test checks via local pnpm check (no issues with split test or mounting logic).
  • Staged and committed all affected files with: fix(netlify split testing): externalize pre-mount nf_ab script; robust cookie parsing; refined reload + URL cleanup; document host-scoped cookie.
  • Pushed the branch containing all PR feat(netlify): opt-in Split Testing via nf_ab query #207 review adjustments to origin (commit log).
  • Assigned and requested review from jchris on PR #207.

…t cookie parsing; refined reload + URL cleanup; document host-scoped cookie
@charliecreates
Copy link
Contributor Author

Applied. I took your review suggestions and pushed the updates to this PR.

Changes

  • Pre-mount script
    • Externalized the inline IIFE to keep CSP strict: public/nf-ab.cookie.js
    • Kept it first in via <script src="/nf-ab.cookie.js"></script> in app/root.tsx
    • Cookie parsing regex now allows optional whitespace after semicolons: (?:^|;\s*)nf_ab=([^;]+)
    • URL handling refined:
      • Always remove nf_ab and ab query params
      • Only reload when the cookie’s effective value changes (set-to-new or cleared-when-present)
      • If no change, clean the URL via history.replaceState without a reload
  • Docs
    • docs/split-testing.md: added a “Cookie scope (apex vs subdomains)” note explaining the default host-scoped cookie and the option to use a Domain attribute (trade-offs noted)
    • README.md: brief note that nf_ab is host-scoped by default and link to the doc for details

Open question

  • Cookie Domain support in code (optional in review): Want me to add it now? Easiest path is to allow configuring a Domain via a data attribute on the script tag, e.g.: <script src="/nf-ab.cookie.js" data-domain=".vibes.diy"></script> The script would append ; Domain=.vibes.diy when present. Say the word and I’ll wire this up and update docs.

Verification

  • pnpm check
    • format: ok
    • types: ok
    • tests: 362 passed, 4 skipped

Commits

  • fix(netlify split testing): externalize pre-mount nf_ab script; robust cookie parsing; refined reload + URL cleanup; document host-scoped cookie

Assigning and re-requesting review from @jchris.

@jchris jchris requested a review from necrodome August 11, 2025 00:08
@jchris
Copy link
Contributor

jchris commented Aug 11, 2025

@CharlieHelps only document the ab= version, the nf can be in the code if that helps with compatibility but better to have one path

@charliecreates
Copy link
Contributor Author

charliecreates bot commented Aug 11, 2025

Expand this to see my work.
  • Updated docs/split-testing.md to show only ?ab=<branch> for opt-in and clearing, replacing all ?nf_ab= usage in code examples, instructions, and parameter explanations.
  • Ensured docs/split-testing.md retains mention of nf_ab only as the internal cookie, not as a URL parameter, with a clarifying comment on intentional lack of nf_ab documentation.
  • Reviewed all remaining instances of nf_ab in docs/split-testing.md to confirm they describe the cookie (not query param) and align with jchris’s comment on PR #207.
  • Updated the README.md developer previews section to only feature ?ab=<branch> in the usage example and removed all previous ?nf_ab= parameter instructions.
  • Confirmed with full-text search that no public docs (docs/split-testing.md, README.md) contain ?nf_ab as a query parameter.

@charliecreates
Copy link
Contributor Author

Docs updated to document only the ab= path. No runtime changes.

  • docs/split-testing.md
    • Switched all usage/examples to ?ab=<branch> only.
    • Opt-out uses ?ab=clear (value aliases still supported: off, reset, none).
    • Removed any mention of ?nf_ab= as a query param.
    • Kept background references to the Netlify cookie name nf_ab where relevant.
  • README.md
    • “Developer previews…” section now shows a single example using ?ab=<branch> and links to the doc.

Quick usage

# opt-in to a branch
https://vibes.diy/?ab=my-experimental-branch

# opt-out (aliases: off, reset, none)
https://vibes.diy/?ab=clear

Shout if you want any additional wording tweaks.

@jchris jchris merged commit 201c9e2 into main Aug 11, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants