Migrating from Mojito
Move a Mojito JS Delivery wave to a Fabric Experiments YAML — automatically, with one command.
Fabric Experiments ships a one-shot importer plus runtime support for the
features Mojito users rely on most: domOps, divertTo, manualExposure,
holdback, and a Trigger DSL.
What's automatic
Run the importer:
fx import-mojito ./waves --out ./experiments --strictIt walks every wave directory and emits one Fabric YAML per wave.
It also writes import-report.json, a machine-readable inventory of converted,
failed, and manual-review waves. --strict returns a non-zero exit code while
any compatibility warning remains, making the importer safe to gate in CI.
1:1 conversion:
| Mojito | Fabric |
|---|---|
id | id |
name | name |
sampleRate | sampleRate |
divertTo | divertTo |
manualExposure | manualExposure |
recipes[<key>].name | variants[<key>].name |
recipes[<key>].sampleRate | variants[<key>].weight (× 10000) |
recipes[<key>].js (file) | variants[<key>].js (inlined) |
recipes[<key>].css (file) | variants[<key>].css (inlined) |
What needs review
trigger.js can run arbitrary code in Mojito. Fabric conservatively
auto-converts the common pathname equality, URL includes/indexOf,
domReady, and waitForElement patterns. The original source remains in a
YAML comment header for review. A trigger that cannot be proven safe is emitted
as auto, reported as manual, and makes --strict fail until you translate
it to one of:
-
The declarative Trigger DSL:
trigger: kind: urlMatch pattern: /cart # or trigger: kind: waitForSelector selector: 'button.add-to-cart' timeoutMs: 3000 # or trigger: kind: event name: 'app:ready' -
A host-supplied trigger override at SDK init time:
createClient(manifest, { subjectId, manifestUrl, triggers: { 'w12-checkout': () => location.pathname.startsWith('/checkout'), }, });
Side-by-side example
Mojito wave (waves/w12-hero/config.yml):
state: live
sampleRate: 0.5
id: w12-hero
name: W12 Hero
recipes:
"0":
name: Control
"1":
name: Treatment
js: 1.js
css: 1.css
trigger: trigger.jsAfter fx import-mojito waves → experiments/w12-hero.yaml:
# Imported from Mojito wave
# Source: waves/w12-hero
# Original state: live
#
# Original trigger.js (auto-converted to urlMatch):
# function trigger(test) {
# if (location.pathname === '/') test.activate();
# }
id: w12-hero
name: W12 Hero
sampleRate: 0.5
variants:
- key: "0"
name: Control
- key: "1"
name: Treatment
js: |
// contents of 1.js
css: |
/* contents of 1.css */
trigger:
kind: urlMatch
pattern: https?://[^/]+/(?:[?#]|$)
regex: trueEdit the trigger to match the original intent, then:
fx validate experiments
fx plan experiments
fx apply experimentsRecipe → variant authoring tips
Prefer domOps over js for new variants — declarative DOM ops are
CSP-friendlier than new Function(variant.js) and survive content security
policy hardening:
variants:
- key: control
name: Control
- key: treatment
name: Treatment
domOps:
- op: replaceText
selector: 'h1.hero'
value: 'Ship faster'
- op: setStyle
selector: 'a.cta'
name: background
value: '#16a34a'The full op list is in Reference / YAML.
Runtime parity table
For the release-level status of Console, analytics, BDD, and Databricks workloads, use the precise compatibility matrix.
| Mojito feature | Fabric equivalent |
|---|---|
?mojito_<id>=<recipe> URL preview | ?fxpreview=<expId>:<variantKey>&fxtoken=<jwt> (short-lived, asymmetrically signed, public-key verified, not exposure-tracked) |
| Sticky cookies | Cookie fx.<expId> + localStorage fallback |
manualExposure: true + test.trackExposureEvent() | manualExposure: true + client.expose(id) |
divertTo | divertTo |
holdback (per-recipe sampleRate < 1) | holdback: 0.1 (compiles to audience.sampleRate = 0.9) |
| Snowplow tracker | @fabricorg/experiments-web-adapters → snowplowAdapter() |
| GA Optimize | gaAdapter({ eventName: 'experiment_exposure' }) |
custom storageAdapter | mojitoCompatibilityAdapter(existingAdapter) |
onVeilTimeout | onActivationFailure or the compatibility bridge |
Reuse a Mojito storage adapter during migration
import { init } from '@fabricorg/experiments-web'
import { mojitoCompatibilityAdapter } from '@fabricorg/experiments-web-adapters'
const legacyTracking = mojitoCompatibilityAdapter(Mojito.options.storageAdapter)
await init({
manifestUrl,
subjectId,
...legacyTracking,
})The bridge supplies a minimal Mojito-shaped test object and maps Fabric recipe
errors and selector-trigger timeouts to the legacy callbacks. The original
Fabric record remains available as test.fabric for incremental modernization.
What's not portable
gaExperimentId(Google Optimize is sunset). UsegaAdapterto send a custom GA4 event instead.- Wave-level
state: inactive— Fabric uses the lifecycle (draft → review → approved → running → paused → killed). The importer dropsstateand leaves you indraft; promote withfx apply. - The
mojito_*URL preview pattern. Usefx preview <expId> --variant <key>or Studio's Preview controls to mint short-lived signed Fabric preview links. Unlike Mojito's unsigned override, Fabric links verify with public JWKS and require no browser-visible preview secret.
Beyond runtime parity: Databricks-native testing
Fabric's compatibility target is broader than browser delivery. The same repository can express executable Gherkin specifications for assignment, aggregation, guardrails, Lakebase state, SQL warehouses, Unity Catalog, Volumes, Jobs, notebooks, dbt, and isolated Auto Loader pipelines.
- Behavior-driven testing provides Behave-style cleanup, userdata, custom parameter types, lifecycle hooks, Scenario Outlines, per-step output capture, structured failure attachments, HTML/JUnit/rerun reports, WIP/pretty workflows, and a generated steps catalog.
- Live artifact checks validate every deployed Databricks surface and produce secret-redacted evidence.
- Local DuckDB scenarios stay fast and parallel; live scenarios are gated, serial, identity-aware, and destructive operations are confined to bundle-managed BDD resources.
Fabric's shipped BDD implementation is TypeScript end to end. Portable
.feature files can still be shared with Behave and pytest-bdd, but Fabric does
not require or ship a Python adapter. The Gherkin wording and observable
workload behavior—not step-language API compatibility—are the portability
contract.