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
│ │ └── v2/
│ │ └── index.jsx # Variation 2 entry point
│ │
│ ├── config.js # selectors, translations, MODEL_CODE_MAP
│ └── helpers.js # Samsung API fetch, price formatting (product-card only)
│
├── e2e/ # Only present when E2E is enabled
│ ├── smoke.spec.js
│ ├── config.js # Market URLs
│ └── helpers.js
│
├── experiment.config.js # targetUrl, globalObject, live options
├── 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
├── jsconfig.json # JSX support and aliases for editors
├── .nvmrc # Node 24
├── .editorconfig
├── .gitignore
├── CLAUDE.md # Optional local AI instructions; created by pnpm init-claude
├── AGENTS.md # Optional local AI instructions; created by pnpm init-agents
└── package.jsonCLAUDE.md and AGENTS.md are opt-in files and are not present immediately after scaffolding. The generated .gitignore excludes both files so project-specific AI instructions remain local by default.
Key files explained
experiment.config.js
Top-level runtime configuration. Keep this at the root - it's imported by the build script.
export default {
targetUrl: 'https://www.samsung.com/uk/smartphones/all-smartphones/',
globalObject: 'sgd',
includeEmergencyBrake: true,
live: {
variation: 0,
overlay: 'visible',
profile: 'ephemeral',
},
};See Configuration for details.
src/config.js
Experiment-specific values. Edit this file first when setting up a new experiment.
export const selectors = {
primary: '.target-selector',
fallbacks: ['.alternate-selector', 'body'],
};
export const locale = window.location.pathname.split('/')[1];
export const MODEL_CODE_MAP = {
default: 'SM-XXXXXXX', // Samsung model code per locale
uk: 'SM-XXXXXXX',
};
const translations = {
uk: { title: 'Upgrade today', from: 'From', ctaText: 'Shop now' },
};
export const translationByMarket = translations[locale] || translations.uk;src/helpers.js
Samsung-specific utilities included in the product-card boilerplate. The key exports are:
fetchProductCard()- 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
The minimal boilerplate does not need Samsung API integration.
Runtime helpers
The experiment runtime is imported from create-experiment/framework. Every variation entry point can use:
runScript(fn)- ensures DOM is ready before executingmountExperiment(selector, fallback?, position?)- creates and injects the containerdivtrackAAEvent(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 elementgetPath(),getPathSegments(),getMarket()- resolve browser path and market contextlog(),debug()- development and opt-in diagnostic logging
See the Framework API for full documentation.
src/js/v1/index.jsx
The variation entry point. Every variation follows the same four-step pattern:
import { render } from 'preact';
import { mountExperiment, runScript, setupTracking } from 'create-experiment/framework';
import ExperimentCard from '@components/ExperimentCard';
import { fetchProductCard } from '../../helpers';
import { selectors, translationByMarket } from '../../config';
runScript(async () => {
// 1. Mount container
const container = mountExperiment(selectors.primary, selectors.fallbacks);
if (!container) return;
// 2. Fetch data
const { data } = await fetchProductCard();
if (!data) return;
// 3. Render component
render(<ExperimentCard title={translationByMarket.title} />, container);
// 4. 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 this alias in generated projects:
| Alias | Resolves to |
|---|---|
@components | src/components/ |
The project does not generate per-variation SCSS. Add a co-located CSS Module only when a variation genuinely needs separate styles.