FabricFabricExperiments
Migration

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 --strict

It 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:

MojitoFabric
idid
namename
sampleRatesampleRate
divertTodivertTo
manualExposuremanualExposure
recipes[<key>].namevariants[<key>].name
recipes[<key>].sampleRatevariants[<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:

  1. 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'
  2. 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.js

After 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: true

Edit the trigger to match the original intent, then:

fx validate experiments
fx plan experiments
fx apply experiments

Recipe → 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 featureFabric equivalent
?mojito_<id>=<recipe> URL preview?fxpreview=<expId>:<variantKey>&fxtoken=<jwt> (short-lived, asymmetrically signed, public-key verified, not exposure-tracked)
Sticky cookiesCookie fx.<expId> + localStorage fallback
manualExposure: true + test.trackExposureEvent()manualExposure: true + client.expose(id)
divertTodivertTo
holdback (per-recipe sampleRate < 1)holdback: 0.1 (compiles to audience.sampleRate = 0.9)
Snowplow tracker@fabricorg/experiments-web-adapterssnowplowAdapter()
GA OptimizegaAdapter({ eventName: 'experiment_exposure' })
custom storageAdaptermojitoCompatibilityAdapter(existingAdapter)
onVeilTimeoutonActivationFailure 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). Use gaAdapter to send a custom GA4 event instead.
  • Wave-level state: inactive — Fabric uses the lifecycle (draft → review → approved → running → paused → killed). The importer drops state and leaves you in draft; promote with fx apply.
  • The mojito_* URL preview pattern. Use fx 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.

On this page