Project Structure
A generated product-card project with two variations looks like this:
my-experiment/
│
├── src/
│ ├── components/
│ │ └── ExperimentCard/
│ │ ├── index.jsx # Preact component — image, title, price, CTA
│ │ └── styles.module.scss # Scoped component styles
│ │
│ ├── js/
│ │ ├── v1/
│ │ │ ├── index.jsx # Variation 1 entry point
│ │ │ └── styles.module.scss # Per-variation style overrides
│ │ └── v2/
│ │ ├── index.jsx # Variation 2 entry point
│ │ └── styles.module.scss
│ │
│ ├── config.js # testName, targetSelector, translations, MODEL_CODE_MAP
│ └── helpers.js # Samsung API fetch, price formatting, uniqueBuild
│
├── lib/
│ └── framework.js # Runtime: runScript, trackAAEvent, waitFor, setupTracking
│
├── scripts/
│ ├── build.js # Vite IIFE builds — one bundle per variation
│ └── start.js # Watch wrapper: pnpm start 0 → -e0 flag
│
├── e2e/ # Only present when E2E is enabled
│ ├── smoke.spec.js
│ ├── config.js # Market URLs
│ └── helpers.js
│
├── experiment.config.js # globalObject, includeEmergencyBrake
├── vite.config.js # IIFE lib mode, Preact plugin, CSS Modules, aliases
├── playwright.config.js # Only present when E2E is enabled
├── biome.json # Biome linter + formatter
├── .stylelintrc # SCSS linting rules
├── tsconfig.json # JSX support for editors
├── .nvmrc # Node 24
├── .editorconfig
├── .gitignore
└── package.jsonKey files explained
experiment.config.js
Top-level runtime configuration. Keep this at the root — it's imported by the build script.
js
export default {
globalObject: 'sgd', // Window namespace for the IIFE bundle
includeEmergencyBrake: true, // Adobe Target kill-switch — disable to remove it
};See Configuration for details.
src/config.js
Experiment-specific values. Edit this file first when setting up a new experiment.
js
export const testName = 'my-experiment';
export const targetSelector = '.target-selector'; // CSS selector for DOM injection
export const locale = window.location.pathname.split('/')[1];
export const MODEL_CODE_MAP = {
default: 'SM-XXXXXXX', // Samsung model code per locale
uk: 'SM-XXXXXXX',
};
export const translationByMarket = {
uk: { title: 'Upgrade today', from: 'From', ctaText: 'Shop now' },
// Add more locales as needed
};src/helpers.js
Samsung-specific utilities. The key exports are:
uniqueBuild(variation)— generates the dedup guard ID and selectorfetchProductCard()— fetches Samsung product data from the search APIformatPrice(price)— formats a price usingIntl.NumberFormatfor the current localemodelCode()— resolves the model code fromMODEL_CODE_MAPbased on locale
lib/framework.js
The experiment runtime. Imported in every variation entry point. Provides:
runScript(fn)— ensures DOM is ready before executingtrackAAEvent(evar, event, data)— fires Adobe Analytics eventswaitFor(selectors, callback)— polls until elements are presentwatchFor(selector, callback, options?)— waits via MutationObserversetupTracking(container, options)— attaches click tracking to a rendered element
See the Framework API for full documentation.
src/js/v1/index.jsx
The variation entry point. Every variation follows the same five-step pattern:
jsx
import { render, h } from 'preact';
import { runScript, setupTracking } from '@lib/framework';
import ExperimentCard from '@components/ExperimentCard';
import { fetchProductCard, formatPrice, uniqueBuild } from '../../helpers';
import { translationByMarket, targetSelector } from '../../config';
runScript(async () => {
// 1. Select target and prevent duplication
const target = document.querySelector(targetSelector);
const { uniqueId, injectedSelector } = uniqueBuild('v1');
if (!target || document.querySelector(injectedSelector)) return;
// 2. Create injection container
const container = document.createElement('div');
container.dataset.injectedExperiment = uniqueId;
target.insertAdjacentElement('afterend', container);
// 3. Fetch data
const { data } = await fetchProductCard();
if (!data) return;
// 4. Render component
render(<ExperimentCard ... />, container);
// 5. Set up tracking — MUST come after render
setupTracking(container, { label: 'my-experiment: v1 cta clicked' });
});Tracking must come after render
setupTracking queries the DOM for the element to attach to. Calling it before render() will silently fail because the element doesn't exist yet.
Import aliases
Vite resolves two aliases in generated projects:
| Alias | Resolves to |
|---|---|
@lib/framework | lib/framework.js |
@components | src/components/ |