Problem Statement
withExperiments (the @dotcms/experiments HOC used to wrap DotCMSLayoutBody for A/B testing, see core-web/libs/sdk/experiments) can leave the wrapped page permanently hidden (visibility: hidden, never flips to visible) instead of rendering. This affects both real site visitors going through an experiment redirect and content editors previewing an experiment variant inside the Universal Visual Editor (UVE).
The component's DotExperimentHandlingComponent hides content behind a <div style={{ visibility: 'hidden' }}> while useExperimentVariant()'s shouldWaitForVariant is true, expecting it to flip to false shortly after. Three separate, independent bugs could each leave it stuck at true forever:
-
Unhandled promise rejection in DotExperimentsProvider — DotExperiments.getInstance(config).ready().then(() => setInstance(...)) had no .catch. If initialization ever rejected (bad apiKey/server, network failure, indexedDB error), setInstance never ran, the DotExperimentsContext stayed null forever, and useExperimentVariant could never resolve a matching variant.
-
Inconsistent "inside the editor" detection — DotExperimentsProvider.tsx treated any truthy getUVEState()?.mode (EDIT_MODE, PREVIEW_MODE, LIVE) as "inside the editor" and skipped creating a DotExperiments instance entirely. But useExperimentVariant.ts only treated UVE_MODE.EDIT specifically as "inside the editor" — so when UVE was in PREVIEW_MODE (e.g. content editors using dotCMS's "preview this experiment variant" feature), the hook fell through expecting an instance from context that the provider had deliberately never created, waiting on it forever.
-
Example app (examples/nextjs-experiments) not forwarding the variantName query param — src/app/[[...slug]]/page.tsx built the dotCMS page fetch path from route params only and never read searchParams, so the ?variantName=... query param appended by the SDK's client-side redirect (useExperiments' locationChanged) never reached dotCMSClient.page.get(). dotCMS always returned the DEFAULT variant's viewAs.variantId, which never matched the client-assigned variant name, so the match check in useExperimentVariant never succeeded — same permanent-hidden symptom, this time for real (non-UVE) traffic, plus the wrong variant's content/containers being served regardless.
Steps to Reproduce
Scenario A — real visitor redirect flow (bug #1 and #3):
- Set up the
nextjs-experiments example app against a dotCMS instance with a running experiment on the home page.
- Load the site as a normal visitor (no UVE). The client-side SDK detects the assigned variant and redirects to
/?variantName=dotexperiment-<expId>-<variantName>.
- Observe: the redirect succeeds (200 response), but the page body never appears —
pageAsset.viewAs.variantId logs as 'DEFAULT' regardless of the URL's variantName param.
Scenario B — UVE preview mode (bug #2):
- Open the same page inside the dotCMS Universal Visual Editor.
- Use the "preview this experiment variant" feature, which loads the iframe with
?mode=PREVIEW_MODE&variantName=...&experimentId=....
- Observe console log:
DotExperimentsProvider: DotExperiments instance not initialized because it is inside the editor. followed by a blank page — the wrapped component never renders.
Scenario C — init failure (bug #1, harder to reproduce on demand):
- Configure
@dotcms/experiments with an invalid apiKey/server so the experiments-fetch call fails.
- Observe the page stays blank indefinitely with no console error, since the failed
ready() promise is silently swallowed.
Acceptance Criteria
dotCMS Version
latest
Severity
High - Major functionality broken
Links
Problem Statement
withExperiments(the@dotcms/experimentsHOC used to wrapDotCMSLayoutBodyfor A/B testing, seecore-web/libs/sdk/experiments) can leave the wrapped page permanently hidden (visibility: hidden, never flips to visible) instead of rendering. This affects both real site visitors going through an experiment redirect and content editors previewing an experiment variant inside the Universal Visual Editor (UVE).The component's
DotExperimentHandlingComponenthides content behind a<div style={{ visibility: 'hidden' }}>whileuseExperimentVariant()'sshouldWaitForVariantistrue, expecting it to flip tofalseshortly after. Three separate, independent bugs could each leave it stuck attrueforever:Unhandled promise rejection in
DotExperimentsProvider—DotExperiments.getInstance(config).ready().then(() => setInstance(...))had no.catch. If initialization ever rejected (badapiKey/server, network failure, indexedDB error),setInstancenever ran, theDotExperimentsContextstayednullforever, anduseExperimentVariantcould never resolve a matching variant.Inconsistent "inside the editor" detection —
DotExperimentsProvider.tsxtreated any truthygetUVEState()?.mode(EDIT_MODE,PREVIEW_MODE,LIVE) as "inside the editor" and skipped creating aDotExperimentsinstance entirely. ButuseExperimentVariant.tsonly treatedUVE_MODE.EDITspecifically as "inside the editor" — so when UVE was inPREVIEW_MODE(e.g. content editors using dotCMS's "preview this experiment variant" feature), the hook fell through expecting an instance from context that the provider had deliberately never created, waiting on it forever.Example app (
examples/nextjs-experiments) not forwarding thevariantNamequery param —src/app/[[...slug]]/page.tsxbuilt the dotCMS page fetch path from route params only and never readsearchParams, so the?variantName=...query param appended by the SDK's client-side redirect (useExperiments'locationChanged) never reacheddotCMSClient.page.get(). dotCMS always returned the DEFAULT variant'sviewAs.variantId, which never matched the client-assigned variant name, so the match check inuseExperimentVariantnever succeeded — same permanent-hidden symptom, this time for real (non-UVE) traffic, plus the wrong variant's content/containers being served regardless.Steps to Reproduce
Scenario A — real visitor redirect flow (bug #1 and #3):
nextjs-experimentsexample app against a dotCMS instance with a running experiment on the home page./?variantName=dotexperiment-<expId>-<variantName>.pageAsset.viewAs.variantIdlogs as'DEFAULT'regardless of the URL'svariantNameparam.Scenario B — UVE preview mode (bug #2):
?mode=PREVIEW_MODE&variantName=...&experimentId=....DotExperimentsProvider: DotExperiments instance not initialized because it is inside the editor.followed by a blank page — the wrapped component never renders.Scenario C — init failure (bug #1, harder to reproduce on demand):
@dotcms/experimentswith an invalidapiKey/serverso the experiments-fetch call fails.ready()promise is silently swallowed.Acceptance Criteria
mode=PREVIEW_MODE) renders the page instead of staying hidden.DotExperimentsinitialization (bad config, network error) no longer leaves the page permanently hidden.useExperimentVariant.spec.tsx; the example-appsearchParamsforwarding and init-failure fallback are not covered by automated tests yet).dotCMS Version
latest
Severity
High - Major functionality broken
Links