Skip to content

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.json

Key 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 selector
  • fetchProductCard() — fetches Samsung product data from the search API
  • formatPrice(price) — formats a price using Intl.NumberFormat for the current locale
  • modelCode() — resolves the model code from MODEL_CODE_MAP based on locale

lib/framework.js ​

The experiment runtime. Imported in every variation entry point. Provides:

  • runScript(fn) — ensures DOM is ready before executing
  • trackAAEvent(evar, event, data) — fires Adobe Analytics events
  • waitFor(selectors, callback) — polls until elements are present
  • watchFor(selector, callback, options?) — waits via MutationObserver
  • setupTracking(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:

AliasResolves to
@lib/frameworklib/framework.js
@componentssrc/components/

Internal tool — Samsung / Sogody experimentation team